tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 194155f5758c11ef3b8af7504427d167282a305b
parent 9d5a9962ffade21615dab330126a387ee6496fb5
Author: Irene Ni <ini@mozilla.com>
Date:   Fri, 21 Nov 2025 23:08:49 +0000

Bug 1996923 - Update New Tab top sites to use left/right arrows for consistency. r=home-newtab-reviewers,reemhamz

Differential Revision: https://phabricator.services.mozilla.com/D273114

Diffstat:
Mbrowser/extensions/newtab/content-src/components/TopSites/TopSite.jsx | 34++++++++++++++++------------------
Mbrowser/extensions/newtab/data/content/activity-stream.bundle.js | 21++++++++++-----------
Mbrowser/extensions/newtab/test/unit/content-src/components/TopSites.test.jsx | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 78 insertions(+), 29 deletions(-)

diff --git a/browser/extensions/newtab/content-src/components/TopSites/TopSite.jsx b/browser/extensions/newtab/content-src/components/TopSites/TopSite.jsx @@ -922,24 +922,22 @@ export class _TopSiteList extends React.PureComponent { 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(); + if (e.key === "ArrowLeft" || e.key === "ArrowRight") { + // Arrow direction should match visual navigation direction in RTL + const isRTL = document.dir === "rtl"; + const navigateToPrevious = isRTL + ? e.key === "ArrowRight" + : e.key === "ArrowLeft"; + + const targetTopSite = navigateToPrevious + ? this.focusedRef?.previousSibling + : this.focusedRef?.nextSibling; + + const targetAnchor = targetTopSite?.querySelector("a"); + if (targetAnchor) { + targetAnchor.tabIndex = 0; + targetAnchor.focus(); + } } } diff --git a/browser/extensions/newtab/data/content/activity-stream.bundle.js b/browser/extensions/newtab/data/content/activity-stream.bundle.js @@ -9056,17 +9056,16 @@ class _TopSiteList extends (external_React_default()).PureComponent { 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(); + if (e.key === "ArrowLeft" || e.key === "ArrowRight") { + // Arrow direction should match visual navigation direction in RTL + const isRTL = document.dir === "rtl"; + const navigateToPrevious = isRTL ? e.key === "ArrowRight" : e.key === "ArrowLeft"; + const targetTopSite = navigateToPrevious ? this.focusedRef?.previousSibling : this.focusedRef?.nextSibling; + const targetAnchor = targetTopSite?.querySelector("a"); + if (targetAnchor) { + targetAnchor.tabIndex = 0; + targetAnchor.focus(); + } } } onWrapperFocus() { diff --git a/browser/extensions/newtab/test/unit/content-src/components/TopSites.test.jsx b/browser/extensions/newtab/test/unit/content-src/components/TopSites.test.jsx @@ -1804,6 +1804,58 @@ describe("<TopSiteList>", () => { ); assert.lengthOf(wrapper.find("li.hide-for-narrow"), 2); }); + + describe("Keyboard navigation", () => { + let sandbox; + let wrapper; + let instance; + let mockAnchor; + let mockTargetSibling; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + const rows = [ + { url: "https://foo.com" }, + { url: "https://bar.com" }, + { url: "https://baz.com" }, + ]; + wrapper = shallow( + <TopSiteList {...DEFAULT_PROPS} TopSites={{ rows }} App={APP} /> + ); + instance = wrapper.instance(); + + mockAnchor = { focus: sandbox.spy(), tabIndex: -1 }; + mockTargetSibling = { querySelector: sandbox.stub().returns(mockAnchor) }; + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should navigate to next site with ArrowRight", () => { + instance.focusedRef = { nextSibling: mockTargetSibling }; + const mockEvent = { key: "ArrowRight" }; + + instance.onKeyDown(mockEvent); + + assert.calledOnce(mockTargetSibling.querySelector); + assert.calledWith(mockTargetSibling.querySelector, "a"); + assert.calledOnce(mockAnchor.focus); + assert.equal(mockAnchor.tabIndex, 0); + }); + + it("should navigate to previous site with ArrowLeft", () => { + instance.focusedRef = { previousSibling: mockTargetSibling }; + const mockEvent = { key: "ArrowLeft" }; + + instance.onKeyDown(mockEvent); + + assert.calledOnce(mockTargetSibling.querySelector); + assert.calledWith(mockTargetSibling.querySelector, "a"); + assert.calledOnce(mockAnchor.focus); + assert.equal(mockAnchor.tabIndex, 0); + }); + }); }); describe("TopSiteAddButton", () => {