tor-browser

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

commit 1614dd7ebbd368f0ad9563268c15cbb89bd9fe8e
parent a33c0e85699e439db041ed10d49309ac7beab990
Author: Maxx Crawford <mcrawford@mozilla.com>
Date:   Tue, 18 Nov 2025 17:57:56 +0000

Bug 1996310 - Add maximize all widgets logic button to widget container r=home-newtab-reviewers,ini

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

Diffstat:
Mbrowser/extensions/newtab/content-src/components/Widgets/FocusTimer/FocusTimer.jsx | 8++++++--
Mbrowser/extensions/newtab/content-src/components/Widgets/Lists/Lists.jsx | 4++--
Mbrowser/extensions/newtab/content-src/components/Widgets/Widgets.jsx | 39+++++++++++++++++++++++++++++++++++++--
Mbrowser/extensions/newtab/data/content/activity-stream.bundle.js | 43++++++++++++++++++++++++++++++++++++-------
Mbrowser/extensions/newtab/lib/ActivityStream.sys.mjs | 14++++++++++++++
Mbrowser/extensions/newtab/test/unit/content-src/components/Widgets.test.jsx | 193+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 288 insertions(+), 13 deletions(-)

diff --git a/browser/extensions/newtab/content-src/components/Widgets/FocusTimer/FocusTimer.jsx b/browser/extensions/newtab/content-src/components/Widgets/FocusTimer/FocusTimer.jsx @@ -104,7 +104,11 @@ export const getClipPath = progress => { return `polygon(${points.join(", ")})`; }; -export const FocusTimer = ({ dispatch, handleUserInteraction }) => { +export const FocusTimer = ({ + dispatch, + handleUserInteraction, + isMaximized, +}) => { const [timeLeft, setTimeLeft] = useState(0); // calculated value for the progress circle; 1 = 100% const [progress, setProgress] = useState(0); @@ -530,7 +534,7 @@ export const FocusTimer = ({ dispatch, handleUserInteraction }) => { return timerData ? ( <article - className="focus-timer" + className={`focus-timer ${isMaximized ? "is-maximized" : ""}`} ref={el => { timerRef.current = [el]; }} diff --git a/browser/extensions/newtab/content-src/components/Widgets/Lists/Lists.jsx b/browser/extensions/newtab/content-src/components/Widgets/Lists/Lists.jsx @@ -34,7 +34,7 @@ const PREF_WIDGETS_LISTS_MAX_LISTITEMS = "widgets.lists.maxListItems"; const PREF_WIDGETS_LISTS_BADGE_ENABLED = "widgets.lists.badge.enabled"; const PREF_WIDGETS_LISTS_BADGE_LABEL = "widgets.lists.badge.label"; -function Lists({ dispatch, handleUserInteraction }) { +function Lists({ dispatch, handleUserInteraction, isMaximized }) { const prefs = useSelector(state => state.Prefs.values); const { selected, lists } = useSelector(state => state.ListsWidget); const [newTask, setNewTask] = useState(""); @@ -591,7 +591,7 @@ function Lists({ dispatch, handleUserInteraction }) { return ( <article - className="lists" + className={`lists ${isMaximized ? "is-maximized" : ""}`} ref={el => { listsRef.current = [el]; }} diff --git a/browser/extensions/newtab/content-src/components/Widgets/Widgets.jsx b/browser/extensions/newtab/content-src/components/Widgets/Widgets.jsx @@ -14,6 +14,8 @@ const PREF_WIDGETS_LISTS_ENABLED = "widgets.lists.enabled"; const PREF_WIDGETS_SYSTEM_LISTS_ENABLED = "widgets.system.lists.enabled"; const PREF_WIDGETS_TIMER_ENABLED = "widgets.focusTimer.enabled"; const PREF_WIDGETS_SYSTEM_TIMER_ENABLED = "widgets.system.focusTimer.enabled"; +const PREF_WIDGETS_MAXIMIZED = "widgets.maximized"; +const PREF_WIDGETS_SYSTEM_MAXIMIZED = "widgets.system.maximized"; // resets timer to default values (exported for testing) // In practice, this logic runs inside a useEffect when @@ -52,6 +54,7 @@ function Widgets() { const { messageData } = useSelector(state => state.Messages); const timerType = useSelector(state => state.TimerWidget.timerType); const timerData = useSelector(state => state.TimerWidget); + const isMaximized = prefs[PREF_WIDGETS_MAXIMIZED]; const dispatch = useDispatch(); const nimbusListsEnabled = prefs.widgetsConfig?.listsEnabled; @@ -109,6 +112,19 @@ function Widgets() { } } + // Toggles the maximized state of widgets + function handleToggleMaximizeClick(e) { + e.preventDefault(); + dispatch(ac.SetPref(PREF_WIDGETS_MAXIMIZED, !isMaximized)); + } + + function handleToggleMaximizeKeyDown(e) { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + dispatch(ac.SetPref(PREF_WIDGETS_MAXIMIZED, !isMaximized)); + } + } + function handleUserInteraction(widgetName) { const prefName = `widgets.${widgetName}.interaction`; const hasInteracted = prefs[prefName]; @@ -127,7 +143,22 @@ function Widgets() { <div className="widgets-section-container"> <div className="widgets-title-container"> <h1 data-l10n-id="newtab-widget-section-title"></h1> - + {prefs[PREF_WIDGETS_SYSTEM_MAXIMIZED] && ( + <moz-button + id="toggle-widgets-size-button" + type="icon ghost" + size="small" + // Toggle the icon and hover text + data-l10n-id={ + isMaximized + ? "newtab-widget-section-maximize" + : "newtab-widget-section-minimize" + } + iconsrc={`chrome://global/skin/icons/${isMaximized ? "fullscreen" : "fullscreen-exit"}.svg`} + onClick={handleToggleMaximizeClick} + onKeyDown={handleToggleMaximizeKeyDown} + /> + )} <moz-button id="hide-all-widgets-button" type="icon ghost" @@ -138,17 +169,21 @@ function Widgets() { onKeyDown={handleHideAllWidgetsKeyDown} /> </div> - <div className="widgets-container"> + <div + className={`widgets-container ${isMaximized ? "is-maximized" : ""}`} + > {listsEnabled && ( <Lists dispatch={dispatch} handleUserInteraction={handleUserInteraction} + isMaximized={isMaximized} /> )} {timerEnabled && ( <FocusTimer dispatch={dispatch} handleUserInteraction={handleUserInteraction} + isMaximized={isMaximized} /> )} </div> diff --git a/browser/extensions/newtab/data/content/activity-stream.bundle.js b/browser/extensions/newtab/data/content/activity-stream.bundle.js @@ -12149,7 +12149,8 @@ const PREF_WIDGETS_LISTS_BADGE_ENABLED = "widgets.lists.badge.enabled"; const PREF_WIDGETS_LISTS_BADGE_LABEL = "widgets.lists.badge.label"; function Lists({ dispatch, - handleUserInteraction + handleUserInteraction, + isMaximized }) { const prefs = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.Prefs.values); const { @@ -12614,7 +12615,7 @@ function Lists({ const badgeEnabled = (nimbusBadgeEnabled || nimbusBadgeTrainhopEnabled) ?? prefs[PREF_WIDGETS_LISTS_BADGE_ENABLED] ?? false; const badgeLabel = (nimbusBadgeLabel || nimbusBadgeTrainhopLabel) ?? prefs[PREF_WIDGETS_LISTS_BADGE_LABEL] ?? ""; return /*#__PURE__*/external_React_default().createElement("article", { - className: "lists", + className: `lists ${isMaximized ? "is-maximized" : ""}`, ref: el => { listsRef.current = [el]; } @@ -13015,7 +13016,8 @@ const getClipPath = progress => { }; const FocusTimer = ({ dispatch, - handleUserInteraction + handleUserInteraction, + isMaximized }) => { const [timeLeft, setTimeLeft] = (0,external_React_namespaceObject.useState)(0); // calculated value for the progress circle; 1 = 100% @@ -13365,7 +13367,7 @@ const FocusTimer = ({ handleTimerInteraction(); } return timerData ? /*#__PURE__*/external_React_default().createElement("article", { - className: "focus-timer", + className: `focus-timer ${isMaximized ? "is-maximized" : ""}`, ref: el => { timerRef.current = [el]; } @@ -13536,6 +13538,8 @@ const PREF_WIDGETS_LISTS_ENABLED = "widgets.lists.enabled"; const PREF_WIDGETS_SYSTEM_LISTS_ENABLED = "widgets.system.lists.enabled"; const PREF_WIDGETS_TIMER_ENABLED = "widgets.focusTimer.enabled"; const PREF_WIDGETS_SYSTEM_TIMER_ENABLED = "widgets.system.focusTimer.enabled"; +const PREF_WIDGETS_MAXIMIZED = "widgets.maximized"; +const PREF_WIDGETS_SYSTEM_MAXIMIZED = "widgets.system.maximized"; // resets timer to default values (exported for testing) // In practice, this logic runs inside a useEffect when @@ -13571,6 +13575,7 @@ function Widgets() { } = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.Messages); const timerType = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.TimerWidget.timerType); const timerData = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.TimerWidget); + const isMaximized = prefs[PREF_WIDGETS_MAXIMIZED]; const dispatch = (0,external_ReactRedux_namespaceObject.useDispatch)(); const nimbusListsEnabled = prefs.widgetsConfig?.listsEnabled; const nimbusTimerEnabled = prefs.widgetsConfig?.timerEnabled; @@ -13613,6 +13618,18 @@ function Widgets() { }); } } + + // Toggles the maximized state of widgets + function handleToggleMaximizeClick(e) { + e.preventDefault(); + dispatch(actionCreators.SetPref(PREF_WIDGETS_MAXIMIZED, !isMaximized)); + } + function handleToggleMaximizeKeyDown(e) { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + dispatch(actionCreators.SetPref(PREF_WIDGETS_MAXIMIZED, !isMaximized)); + } + } function handleUserInteraction(widgetName) { const prefName = `widgets.${widgetName}.interaction`; const hasInteracted = prefs[prefName]; @@ -13632,6 +13649,16 @@ function Widgets() { className: "widgets-title-container" }, /*#__PURE__*/external_React_default().createElement("h1", { "data-l10n-id": "newtab-widget-section-title" + }), prefs[PREF_WIDGETS_SYSTEM_MAXIMIZED] && /*#__PURE__*/external_React_default().createElement("moz-button", { + id: "toggle-widgets-size-button", + type: "icon ghost", + size: "small" + // Toggle the icon and hover text + , + "data-l10n-id": isMaximized ? "newtab-widget-section-maximize" : "newtab-widget-section-minimize", + iconsrc: `chrome://global/skin/icons/${isMaximized ? "fullscreen" : "fullscreen-exit"}.svg`, + onClick: handleToggleMaximizeClick, + onKeyDown: handleToggleMaximizeKeyDown }), /*#__PURE__*/external_React_default().createElement("moz-button", { id: "hide-all-widgets-button", type: "icon ghost", @@ -13641,13 +13668,15 @@ function Widgets() { onClick: handleHideAllWidgetsClick, onKeyDown: handleHideAllWidgetsKeyDown })), /*#__PURE__*/external_React_default().createElement("div", { - className: "widgets-container" + className: `widgets-container ${isMaximized ? "is-maximized" : ""}` }, listsEnabled && /*#__PURE__*/external_React_default().createElement(Lists, { dispatch: dispatch, - handleUserInteraction: handleUserInteraction + handleUserInteraction: handleUserInteraction, + isMaximized: isMaximized }), timerEnabled && /*#__PURE__*/external_React_default().createElement(FocusTimer, { dispatch: dispatch, - handleUserInteraction: handleUserInteraction + handleUserInteraction: handleUserInteraction, + isMaximized: isMaximized }))), messageData?.content?.messageType === "WidgetMessage" && /*#__PURE__*/external_React_default().createElement(MessageWrapper, { dispatch: dispatch }, /*#__PURE__*/external_React_default().createElement(WidgetsFeatureHighlight, { diff --git a/browser/extensions/newtab/lib/ActivityStream.sys.mjs b/browser/extensions/newtab/lib/ActivityStream.sys.mjs @@ -1053,6 +1053,20 @@ export const PREFS_CONFIG = new Map([ }, ], [ + "widgets.maximized", + { + title: "Toggles maximized state for all widgets in the widgets section", + value: false, + }, + ], + [ + "widgets.system.maximized", + { + title: "Enables the maximize widget feature experiment in Nimbus", + value: false, + }, + ], + [ "widgets.focusTimer.enabled", { title: "Enables the focus timer widget", 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 @@ -305,4 +305,197 @@ describe("<Widgets>", () => { } }); }); + + describe("handleToggleMaximize", () => { + 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, + "widgets.maximized": false, + "widgets.system.maximized": 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 action when toggle button is clicked", () => { + const toggleButton = wrapper.find("#toggle-widgets-size-button"); + assert.ok(toggleButton.exists(), "toggle button should exist"); + + // Get the onClick handler and call it + const onClickHandler = toggleButton.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, + 1, + `should dispatch one SetPref action, got ${setPrefCalls.length}.` + ); + + const maximizedPrefCall = setPrefCalls.find( + call => call.args[0].data?.name === "widgets.maximized" + ); + + assert.ok(maximizedPrefCall, "should dispatch SetPref for maximized"); + assert.equal( + maximizedPrefCall.args[0].data.value, + true, + "should toggle maximized pref to true" + ); + }); + + it("should dispatch SetPref action when Enter key is pressed on toggle button", () => { + const toggleButton = wrapper.find("#toggle-widgets-size-button"); + + // Trigger onKeyDown handler directly with Enter key + toggleButton.prop("onKeyDown")({ + key: "Enter", + preventDefault: () => {}, + }); + + const setPrefCalls = store.dispatch + .getCalls() + .filter(call => call.args[0]?.type === at.SET_PREF); + + assert.equal( + setPrefCalls.length, + 1, + "should dispatch one SetPref action" + ); + + const maximizedPrefCall = setPrefCalls.find( + call => call.args[0].data?.name === "widgets.maximized" + ); + + assert.ok(maximizedPrefCall, "should dispatch SetPref for maximized"); + assert.equal( + maximizedPrefCall.args[0].data.value, + true, + "should toggle maximized pref to true" + ); + }); + + it("should dispatch SetPref action when Space key is pressed on toggle button", () => { + const toggleButton = wrapper.find("#toggle-widgets-size-button"); + + // Trigger onKeyDown handler directly with Space key + toggleButton.prop("onKeyDown")({ key: " ", preventDefault: () => {} }); + + const setPrefCalls = store.dispatch + .getCalls() + .filter(call => call.args[0]?.type === at.SET_PREF); + + assert.equal( + setPrefCalls.length, + 1, + "should dispatch one SetPref action" + ); + + const maximizedPrefCall = setPrefCalls.find( + call => call.args[0].data?.name === "widgets.maximized" + ); + + assert.ok(maximizedPrefCall, "should dispatch SetPref for maximized"); + assert.equal( + maximizedPrefCall.args[0].data.value, + true, + "should toggle maximized pref to true" + ); + }); + + it("should not dispatch SetPref actions when other keys are pressed", () => { + const toggleButton = wrapper.find("#toggle-widgets-size-button"); + + const testKeys = ["Escape", "Tab", "a", "ArrowDown"]; + + for (const key of testKeys) { + store.dispatch.resetHistory(); + // Trigger onKeyDown handler directly + toggleButton.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}` + ); + } + }); + + it("should toggle from maximized to minimized state", () => { + // Update state to start with maximized = true + const maximizedState = { + ...INITIAL_STATE, + Prefs: { + ...INITIAL_STATE.Prefs, + values: { + ...INITIAL_STATE.Prefs.values, + [PREF_WIDGETS_LISTS_ENABLED]: true, + [PREF_WIDGETS_SYSTEM_LISTS_ENABLED]: true, + "widgets.maximized": true, + "widgets.system.maximized": true, + }, + }, + }; + const maximizedStore = createStore( + combineReducers(reducers), + maximizedState + ); + sinon.spy(maximizedStore, "dispatch"); + const maximizedWrapper = mount( + <Provider store={maximizedStore}> + <Widgets /> + </Provider> + ); + + const toggleButton = maximizedWrapper.find("#toggle-widgets-size-button"); + toggleButton.prop("onClick")({ preventDefault: () => {} }); + + const setPrefCalls = maximizedStore.dispatch + .getCalls() + .filter(call => call.args[0]?.type === at.SET_PREF); + + const maximizedPrefCall = setPrefCalls.find( + call => call.args[0].data?.name === "widgets.maximized" + ); + + assert.ok(maximizedPrefCall, "should dispatch SetPref for maximized"); + assert.equal( + maximizedPrefCall.args[0].data.value, + false, + "should toggle maximized pref to false" + ); + + maximizedStore.dispatch.restore(); + }); + }); });