tor-browser

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

commit 3a73002df38dd58107e82e58226d8eaf08bc18fe
parent f71aa40659e07cafa692ab77ac81a1e6edbfbeb0
Author: Maxx Crawford <mcrawford@mozilla.com>
Date:   Wed, 19 Nov 2025 16:22:32 +0000

Bug 1996309 - Add button to Widget container to turn off all widgets r=home-newtab-reviewers,reemhamz

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

Diffstat:
Mbrowser/extensions/newtab/content-src/components/Widgets/Widgets.jsx | 35+++++++++++++++++++++++++++++++++--
Mbrowser/extensions/newtab/content-src/components/Widgets/_Widgets.scss | 12+++++++++++-
Mbrowser/extensions/newtab/css/activity-stream.css | 9+++++++++
Mbrowser/extensions/newtab/data/content/activity-stream.bundle.js | 30+++++++++++++++++++++++++++++-
Mbrowser/extensions/newtab/test/unit/content-src/components/Widgets.test.jsx | 173+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 255 insertions(+), 4 deletions(-)

diff --git a/browser/extensions/newtab/content-src/components/Widgets/Widgets.jsx b/browser/extensions/newtab/content-src/components/Widgets/Widgets.jsx @@ -3,7 +3,7 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ import React, { useEffect, useRef } from "react"; -import { useDispatch, useSelector } from "react-redux"; +import { useDispatch, useSelector, batch } from "react-redux"; import { Lists } from "./Lists/Lists"; import { FocusTimer } from "./FocusTimer/FocusTimer"; import { MessageWrapper } from "content-src/components/MessageWrapper/MessageWrapper"; @@ -90,6 +90,25 @@ function Widgets() { prevTimerEnabledRef.current = isTimerEnabled; }, [timerEnabled, timerData, dispatch, timerType]); + // Sends a dispatch to disable all widgets + function handleHideAllWidgetsClick(e) { + e.preventDefault(); + batch(() => { + dispatch(ac.SetPref(PREF_WIDGETS_LISTS_ENABLED, false)); + dispatch(ac.SetPref(PREF_WIDGETS_TIMER_ENABLED, false)); + }); + } + + function handleHideAllWidgetsKeyDown(e) { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + batch(() => { + dispatch(ac.SetPref(PREF_WIDGETS_LISTS_ENABLED, false)); + dispatch(ac.SetPref(PREF_WIDGETS_TIMER_ENABLED, false)); + }); + } + } + function handleUserInteraction(widgetName) { const prefName = `widgets.${widgetName}.interaction`; const hasInteracted = prefs[prefName]; @@ -106,7 +125,19 @@ function Widgets() { return ( <div className="widgets-wrapper"> <div className="widgets-section-container"> - <h1 data-l10n-id="newtab-widget-section-title"></h1> + <div className="widgets-title-container"> + <h1 data-l10n-id="newtab-widget-section-title"></h1> + + <moz-button + id="hide-all-widgets-button" + type="icon ghost" + size="small" + data-l10n-id="newtab-widget-section-hide-all-button" + iconsrc="chrome://global/skin/icons/close.svg" + onClick={handleHideAllWidgetsClick} + onKeyDown={handleHideAllWidgetsKeyDown} + /> + </div> <div className="widgets-container"> {listsEnabled && ( <Lists diff --git a/browser/extensions/newtab/content-src/components/Widgets/_Widgets.scss b/browser/extensions/newtab/content-src/components/Widgets/_Widgets.scss @@ -17,7 +17,6 @@ padding-inline: var(--space-large); background-color: var(--button-background-color); border-radius: var(--border-radius-large); - // Bug 1908010 - This overwrites the design system color because of a // known transparency issue with color-mix syntax when a wallpaper is set .lightWallpaper &, @@ -29,6 +28,17 @@ } } + .widgets-title-container { + display: flex; + justify-content: flex-end; + align-items: center; + gap: var(--space-medium); + + h1 { + margin-inline-end: auto; + } + } + // Mirrors the grid-gap spacing used on // .ds-outer-wrapper-breakpoint-override .ds-card-grid @media(min-width: $break-point-widest) { diff --git a/browser/extensions/newtab/css/activity-stream.css b/browser/extensions/newtab/css/activity-stream.css @@ -4386,6 +4386,15 @@ dialog:dir(rtl)::after { background-color: var(--background-color-box); } } +.widgets-section-container .widgets-title-container { + display: flex; + justify-content: flex-end; + align-items: center; + gap: var(--space-medium); +} +.widgets-section-container .widgets-title-container h1 { + margin-inline-end: auto; +} @media (min-width: 1122px) { .widgets-section-container { padding-block-end: var(--space-xlarge); diff --git a/browser/extensions/newtab/data/content/activity-stream.bundle.js b/browser/extensions/newtab/data/content/activity-stream.bundle.js @@ -13595,6 +13595,24 @@ function Widgets() { // Update the ref to track current state prevTimerEnabledRef.current = isTimerEnabled; }, [timerEnabled, timerData, dispatch, timerType]); + + // Sends a dispatch to disable all widgets + function handleHideAllWidgetsClick(e) { + e.preventDefault(); + (0,external_ReactRedux_namespaceObject.batch)(() => { + dispatch(actionCreators.SetPref(PREF_WIDGETS_LISTS_ENABLED, false)); + dispatch(actionCreators.SetPref(PREF_WIDGETS_TIMER_ENABLED, false)); + }); + } + function handleHideAllWidgetsKeyDown(e) { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + (0,external_ReactRedux_namespaceObject.batch)(() => { + dispatch(actionCreators.SetPref(PREF_WIDGETS_LISTS_ENABLED, false)); + dispatch(actionCreators.SetPref(PREF_WIDGETS_TIMER_ENABLED, false)); + }); + } + } function handleUserInteraction(widgetName) { const prefName = `widgets.${widgetName}.interaction`; const hasInteracted = prefs[prefName]; @@ -13610,9 +13628,19 @@ function Widgets() { className: "widgets-wrapper" }, /*#__PURE__*/external_React_default().createElement("div", { className: "widgets-section-container" + }, /*#__PURE__*/external_React_default().createElement("div", { + className: "widgets-title-container" }, /*#__PURE__*/external_React_default().createElement("h1", { "data-l10n-id": "newtab-widget-section-title" - }), /*#__PURE__*/external_React_default().createElement("div", { + }), /*#__PURE__*/external_React_default().createElement("moz-button", { + id: "hide-all-widgets-button", + type: "icon ghost", + size: "small", + "data-l10n-id": "newtab-widget-section-hide-all-button", + iconsrc: "chrome://global/skin/icons/close.svg", + onClick: handleHideAllWidgetsClick, + onKeyDown: handleHideAllWidgetsKeyDown + })), /*#__PURE__*/external_React_default().createElement("div", { className: "widgets-container" }, listsEnabled && /*#__PURE__*/external_React_default().createElement(Lists, { dispatch: dispatch, diff --git a/browser/extensions/newtab/test/unit/content-src/components/Widgets.test.jsx b/browser/extensions/newtab/test/unit/content-src/components/Widgets.test.jsx @@ -132,4 +132,177 @@ describe("<Widgets>", () => { assert.equal(resetCall.args[0].data.timerType, "break"); }); }); + + describe("handleHideAllWidgets", () => { + let wrapper; + let state; + let store; + + beforeEach(() => { + state = { + ...INITIAL_STATE, + Prefs: { + ...INITIAL_STATE.Prefs, + values: { + ...INITIAL_STATE.Prefs.values, + [PREF_WIDGETS_LISTS_ENABLED]: true, + [PREF_WIDGETS_SYSTEM_LISTS_ENABLED]: true, + [PREF_WIDGETS_TIMER_ENABLED]: true, + [PREF_WIDGETS_SYSTEM_TIMER_ENABLED]: true, + }, + }, + }; + store = createStore(combineReducers(reducers), state); + sinon.spy(store, "dispatch"); + wrapper = mount( + <Provider store={store}> + <Widgets /> + </Provider> + ); + }); + + afterEach(() => { + store.dispatch.restore(); + }); + + it("should dispatch SetPref actions when hide button is clicked", () => { + const hideButton = wrapper.find("#hide-all-widgets-button"); + assert.ok(hideButton.exists(), "hide all button should exist"); + + // Get the onClick handler and call it + const onClickHandler = hideButton.prop("onClick"); + assert.ok(onClickHandler, "onClick handler should exist"); + onClickHandler({ preventDefault: () => {} }); + + const allCalls = store.dispatch.getCalls(); + const setPrefCalls = allCalls.filter( + call => call.args[0]?.type === at.SET_PREF + ); + + assert.equal( + setPrefCalls.length, + 2, + `should dispatch two SetPref actions, got ${setPrefCalls.length}.` + ); + + const listsPrefCall = setPrefCalls.find( + call => call.args[0].data?.name === PREF_WIDGETS_LISTS_ENABLED + ); + const timerPrefCall = setPrefCalls.find( + call => call.args[0].data?.name === PREF_WIDGETS_TIMER_ENABLED + ); + + assert.ok(listsPrefCall, "should dispatch SetPref for lists"); + assert.equal( + listsPrefCall.args[0].data.value, + false, + "should set lists pref to false" + ); + + assert.ok(timerPrefCall, "should dispatch SetPref for timer"); + assert.equal( + timerPrefCall.args[0].data.value, + false, + "should set timer pref to false" + ); + }); + + it("should dispatch SetPref actions when Enter key is pressed on hide button", () => { + const hideButton = wrapper.find("#hide-all-widgets-button"); + + // Trigger onKeyDown handler directly with Enter key + hideButton.prop("onKeyDown")({ key: "Enter", preventDefault: () => {} }); + + const setPrefCalls = store.dispatch + .getCalls() + .filter(call => call.args[0]?.type === at.SET_PREF); + + assert.equal( + setPrefCalls.length, + 2, + "should dispatch two SetPref actions" + ); + + const listsPrefCall = setPrefCalls.find( + call => call.args[0].data?.name === PREF_WIDGETS_LISTS_ENABLED + ); + const timerPrefCall = setPrefCalls.find( + call => call.args[0].data?.name === PREF_WIDGETS_TIMER_ENABLED + ); + + assert.ok(listsPrefCall, "should dispatch SetPref for lists"); + assert.equal( + listsPrefCall.args[0].data.value, + false, + "should set lists pref to false" + ); + + assert.ok(timerPrefCall, "should dispatch SetPref for timer"); + assert.equal( + timerPrefCall.args[0].data.value, + false, + "should set timer pref to false" + ); + }); + + it("should dispatch SetPref actions when Space key is pressed on hide button", () => { + const hideButton = wrapper.find("#hide-all-widgets-button"); + + // Trigger onKeyDown handler directly with Space key + hideButton.prop("onKeyDown")({ key: " ", preventDefault: () => {} }); + + const setPrefCalls = store.dispatch + .getCalls() + .filter(call => call.args[0]?.type === at.SET_PREF); + + assert.equal( + setPrefCalls.length, + 2, + "should dispatch two SetPref actions" + ); + + const listsPrefCall = setPrefCalls.find( + call => call.args[0].data?.name === PREF_WIDGETS_LISTS_ENABLED + ); + const timerPrefCall = setPrefCalls.find( + call => call.args[0].data?.name === PREF_WIDGETS_TIMER_ENABLED + ); + + assert.ok(listsPrefCall, "should dispatch SetPref for lists"); + assert.equal( + listsPrefCall.args[0].data.value, + false, + "should set lists pref to false" + ); + + assert.ok(timerPrefCall, "should dispatch SetPref for timer"); + assert.equal( + timerPrefCall.args[0].data.value, + false, + "should set timer pref to false" + ); + }); + + it("should not dispatch SetPref actions when other keys are pressed", () => { + const hideButton = wrapper.find("#hide-all-widgets-button"); + + const testKeys = ["Escape", "Tab", "a", "ArrowDown"]; + + for (const key of testKeys) { + store.dispatch.resetHistory(); + // Trigger onKeyDown handler directly + hideButton.prop("onKeyDown")({ key }); + + const setPrefCalls = store.dispatch + .getCalls() + .filter(call => call.args[0]?.type === at.SET_PREF); + + assert.equal( + setPrefCalls.length, + 0, + `should not dispatch SetPref for key: ${key}` + ); + } + }); + }); });