Bug 1944447 - Newtab shortcut refresh setting up arrow key navigation r=home-newtab-reviewers,reemhamz

Differential Revision: https://phabricator.services.mozilla.com/D235902
This commit is contained in:
scottdowne
2025-02-04 23:02:05 +00:00
parent 1beb06c53b
commit 78a2184866
4 changed files with 163 additions and 15 deletions

View File

@@ -59,6 +59,8 @@ export class ContextMenuButton extends React.PureComponent {
onKeyDown={this.onKeyDown}
onClick={this.onClick}
ref={refFunction}
tabIndex={this.props.tabIndex || 0}
onFocus={this.props.onFocus}
/>
{showContextMenu
? React.cloneElement(children, {

View File

@@ -349,6 +349,7 @@ export class TopSiteLink extends React.PureComponent {
onDragOver={this.onDragEvent}
onDragEnter={this.onDragEvent}
onDragLeave={this.onDragEvent}
ref={this.props.setRef}
{...draggableProps}
>
<div className="top-site-inner">
@@ -357,12 +358,13 @@ export class TopSiteLink extends React.PureComponent {
<a
className="top-site-button"
href={link.searchTopSite ? undefined : link.url}
tabIndex="0"
tabIndex={this.props.tabIndex}
onKeyPress={this.onKeyPress}
onClick={onClick}
draggable={true}
data-is-sponsored-link={!!link.sponsored_tile_id}
title={title}
onFocus={this.props.onFocus}
>
<div className="tile" aria-hidden={true}>
<div
@@ -619,12 +621,17 @@ export class TopSite extends React.PureComponent {
isContextMenuOpen ? " active" : ""
}`}
title={title}
setPref={this.props.setPref}
tabIndex={this.props.tabIndex}
onFocus={this.props.onFocus}
>
<div>
<ContextMenuButton
tooltip="newtab-menu-content-tooltip"
tooltipArgs={{ title }}
onUpdate={this.onMenuUpdate}
tabIndex={this.props.tabIndex}
onFocus={this.props.onFocus}
>
<LinkMenu
dispatch={props.dispatch}
@@ -676,7 +683,9 @@ export class TopSitePlaceholder extends React.PureComponent {
className={`placeholder ${this.props.className || ""} ${
this.props.isAddButton ? "add-button" : ""
}`}
setPref={this.props.setPref}
isDraggable={false}
tabIndex={this.props.tabIndex}
/>
);
}
@@ -690,6 +699,7 @@ export class _TopSiteList extends React.PureComponent {
draggedSite: null,
draggedTitle: null,
topSitesPreview: null,
focusedIndex: 0,
};
}
@@ -698,6 +708,10 @@ export class _TopSiteList extends React.PureComponent {
this.state = _TopSiteList.DEFAULT_STATE;
this.onDragEvent = this.onDragEvent.bind(this);
this.onActivate = this.onActivate.bind(this);
this.onWrapperFocus = this.onWrapperFocus.bind(this);
this.onTopsiteFocus = this.onTopsiteFocus.bind(this);
this.onWrapperBlur = this.onWrapperBlur.bind(this);
this.onKeyDown = this.onKeyDown.bind(this);
}
componentWillReceiveProps(nextProps) {
@@ -860,6 +874,44 @@ export class _TopSiteList extends React.PureComponent {
this.setState({ activeIndex: index });
}
onKeyDown(e) {
if (this.state.activeIndex || this.state.activeIndex === 0) {
return;
}
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
// prevent the page from scrolling up/down while navigating.
e.preventDefault();
}
if (
this.focusedRef?.nextSibling?.querySelector("a") &&
e.key === "ArrowDown"
) {
this.focusedRef.nextSibling.querySelector("a").tabIndex = 0;
this.focusedRef.nextSibling.querySelector("a").focus();
}
if (
this.focusedRef?.previousSibling?.querySelector("a") &&
e.key === "ArrowUp"
) {
this.focusedRef.previousSibling.querySelector("a").tabIndex = 0;
this.focusedRef.previousSibling.querySelector("a").focus();
}
}
onWrapperFocus() {
this.focusRef?.addEventListener("keydown", this.onKeyDown);
}
onWrapperBlur() {
this.focusRef?.removeEventListener("keydown", this.onKeyDown);
}
onTopsiteFocus(focusIndex) {
this.setState(() => ({
focusedIndex: focusIndex,
}));
}
render() {
const { props } = this;
const prefs = props.Prefs.values;
@@ -909,6 +961,17 @@ export class _TopSiteList extends React.PureComponent {
{...slotProps}
{...commonProps}
isAddButton={topSites[i] && topSites[i].isAddButton}
setRef={
i === this.state.focusedIndex
? el => {
this.focusedRef = el;
}
: () => {}
}
tabIndex={i === this.state.focusedIndex ? 0 : -1}
onFocus={() => {
this.onTopsiteFocus(i);
}}
/>
);
}
@@ -922,6 +985,17 @@ export class _TopSiteList extends React.PureComponent {
{...commonProps}
colors={props.colors}
shortcutsRefresh={shortcutsRefresh}
setRef={
i === this.state.focusedIndex
? el => {
this.focusedRef = el;
}
: () => {}
}
tabIndex={i === this.state.focusedIndex ? 0 : -1}
onFocus={() => {
this.onTopsiteFocus(i);
}}
/>
);
}
@@ -931,6 +1005,13 @@ export class _TopSiteList extends React.PureComponent {
return (
<div className="top-sites-list-wrapper">
<ul
role="group"
aria-label="Shortcuts"
onFocus={this.onWrapperFocus}
onBlur={this.onWrapperBlur}
ref={el => {
this.focusRef = el;
}}
className={`top-sites-list${
this.state.draggedSite ? " dnd-active" : ""
}`}

View File

@@ -2265,7 +2265,9 @@ class ContextMenuButton extends (external_React_default()).PureComponent {
className: "context-menu-button icon",
onKeyDown: this.onKeyDown,
onClick: this.onClick,
ref: refFunction
ref: refFunction,
tabIndex: this.props.tabIndex || 0,
onFocus: this.props.onFocus
}), showContextMenu ? /*#__PURE__*/external_React_default().cloneElement(children, {
keyboardAccess: contextMenuKeyboard,
onUpdate: this.onUpdate
@@ -8081,18 +8083,20 @@ class TopSiteLink extends (external_React_default()).PureComponent {
onDrop: this.onDragEvent,
onDragOver: this.onDragEvent,
onDragEnter: this.onDragEvent,
onDragLeave: this.onDragEvent
onDragLeave: this.onDragEvent,
ref: this.props.setRef
}, draggableProps), /*#__PURE__*/external_React_default().createElement("div", {
className: "top-site-inner"
}, /*#__PURE__*/external_React_default().createElement("a", {
className: "top-site-button",
href: link.searchTopSite ? undefined : link.url,
tabIndex: "0",
tabIndex: this.props.tabIndex,
onKeyPress: this.onKeyPress,
onClick: onClick,
draggable: true,
"data-is-sponsored-link": !!link.sponsored_tile_id,
title: title
title: title,
onFocus: this.props.onFocus
}, /*#__PURE__*/external_React_default().createElement("div", {
className: "tile",
"aria-hidden": true
@@ -8312,13 +8316,18 @@ class TopSite extends (external_React_default()).PureComponent {
onClick: this.onLinkClick,
onDragEvent: this.props.onDragEvent,
className: `${props.className || ""}${isContextMenuOpen ? " active" : ""}`,
title: title
title: title,
setPref: this.props.setPref,
tabIndex: this.props.tabIndex,
onFocus: this.props.onFocus
}), /*#__PURE__*/external_React_default().createElement("div", null, /*#__PURE__*/external_React_default().createElement(ContextMenuButton, {
tooltip: "newtab-menu-content-tooltip",
tooltipArgs: {
title
},
onUpdate: this.onMenuUpdate
onUpdate: this.onMenuUpdate,
tabIndex: this.props.tabIndex,
onFocus: this.props.onFocus
}, /*#__PURE__*/external_React_default().createElement(LinkMenu, {
dispatch: props.dispatch,
index: props.index,
@@ -8360,7 +8369,9 @@ class TopSitePlaceholder extends (external_React_default()).PureComponent {
...addButtonProps
} : {}, {
className: `placeholder ${this.props.className || ""} ${this.props.isAddButton ? "add-button" : ""}`,
isDraggable: false
setPref: this.props.setPref,
isDraggable: false,
tabIndex: this.props.tabIndex
}));
}
}
@@ -8371,7 +8382,8 @@ class _TopSiteList extends (external_React_default()).PureComponent {
draggedIndex: null,
draggedSite: null,
draggedTitle: null,
topSitesPreview: null
topSitesPreview: null,
focusedIndex: 0
};
}
constructor(props) {
@@ -8379,6 +8391,10 @@ class _TopSiteList extends (external_React_default()).PureComponent {
this.state = _TopSiteList.DEFAULT_STATE;
this.onDragEvent = this.onDragEvent.bind(this);
this.onActivate = this.onActivate.bind(this);
this.onWrapperFocus = this.onWrapperFocus.bind(this);
this.onTopsiteFocus = this.onTopsiteFocus.bind(this);
this.onWrapperBlur = this.onWrapperBlur.bind(this);
this.onKeyDown = this.onKeyDown.bind(this);
}
componentWillReceiveProps(nextProps) {
if (this.state.draggedSite) {
@@ -8520,6 +8536,34 @@ class _TopSiteList extends (external_React_default()).PureComponent {
activeIndex: index
});
}
onKeyDown(e) {
if (this.state.activeIndex || this.state.activeIndex === 0) {
return;
}
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
// prevent the page from scrolling up/down while navigating.
e.preventDefault();
}
if (this.focusedRef?.nextSibling?.querySelector("a") && e.key === "ArrowDown") {
this.focusedRef.nextSibling.querySelector("a").tabIndex = 0;
this.focusedRef.nextSibling.querySelector("a").focus();
}
if (this.focusedRef?.previousSibling?.querySelector("a") && e.key === "ArrowUp") {
this.focusedRef.previousSibling.querySelector("a").tabIndex = 0;
this.focusedRef.previousSibling.querySelector("a").focus();
}
}
onWrapperFocus() {
this.focusRef?.addEventListener("keydown", this.onKeyDown);
}
onWrapperBlur() {
this.focusRef?.removeEventListener("keydown", this.onKeyDown);
}
onTopsiteFocus(focusIndex) {
this.setState(() => ({
focusedIndex: focusIndex
}));
}
render() {
const {
props
@@ -8558,7 +8602,14 @@ class _TopSiteList extends (external_React_default()).PureComponent {
if (!link || props.App.isForStartupCache && isSponsored(link) || topSites[i]?.isAddButton) {
if (link) {
topSiteLink = /*#__PURE__*/external_React_default().createElement(TopSitePlaceholder, TopSite_extends({}, slotProps, commonProps, {
isAddButton: topSites[i] && topSites[i].isAddButton
isAddButton: topSites[i] && topSites[i].isAddButton,
setRef: i === this.state.focusedIndex ? el => {
this.focusedRef = el;
} : () => {},
tabIndex: i === this.state.focusedIndex ? 0 : -1,
onFocus: () => {
this.onTopsiteFocus(i);
}
}));
}
} else {
@@ -8568,7 +8619,14 @@ class _TopSiteList extends (external_React_default()).PureComponent {
onActivate: this.onActivate
}, slotProps, commonProps, {
colors: props.colors,
shortcutsRefresh: shortcutsRefresh
shortcutsRefresh: shortcutsRefresh,
setRef: i === this.state.focusedIndex ? el => {
this.focusedRef = el;
} : () => {},
tabIndex: i === this.state.focusedIndex ? 0 : -1,
onFocus: () => {
this.onTopsiteFocus(i);
}
}));
}
topSitesUI.push(topSiteLink);
@@ -8576,6 +8634,13 @@ class _TopSiteList extends (external_React_default()).PureComponent {
return /*#__PURE__*/external_React_default().createElement("div", {
className: "top-sites-list-wrapper"
}, /*#__PURE__*/external_React_default().createElement("ul", {
role: "group",
"aria-label": "Shortcuts",
onFocus: this.onWrapperFocus,
onBlur: this.onWrapperBlur,
ref: el => {
this.focusRef = el;
},
className: `top-sites-list${this.state.draggedSite ? " dnd-active" : ""}`
}, topSitesUI));
}

View File

@@ -92,7 +92,7 @@ add_task(async function test_newtab_last_LinkMenu() {
content.document.querySelector(
".top-site-outer:nth-child(2n) .context-menu-button"
),
"Wait for the Pocket card and button"
"Wait for the topsite card and button"
);
const topsiteOuter = content.document.querySelector(
".top-site-outer:nth-child(2n)"
@@ -105,7 +105,7 @@ add_task(async function test_newtab_last_LinkMenu() {
await ContentTaskUtils.waitForCondition(
() => topsiteOuter.classList.contains("active"),
"Wait for the menu to be active"
"Wait for the topsite menu to be active"
);
is(
@@ -124,7 +124,7 @@ add_task(async function test_newtab_last_LinkMenu() {
content.document.querySelector(
".ds-card:nth-child(1n) .context-menu-button"
),
"Wait for the Pocket card and button"
"Wait for the story card and button"
);
const dsCard = content.document.querySelector(".ds-card:nth-child(1n)");
@@ -134,7 +134,7 @@ add_task(async function test_newtab_last_LinkMenu() {
await ContentTaskUtils.waitForCondition(
() => dsCard.classList.contains("active"),
"Wait for the menu to be active"
"Wait for the story menu to be active"
);
is(