commit ea57fcfbf3664ddfafefdd44359121ce6fec5d49
parent 7549bb9105c06c9d9910f306f4f59e3765131b51
Author: Maxx Crawford <mcrawford@mozilla.com>
Date: Tue, 18 Nov 2025 17:57:56 +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:
5 files changed, 240 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";
@@ -89,6 +89,17 @@ function Widgets() {
// Update the ref to track current state
prevTimerEnabledRef.current = isTimerEnabled;
}, [timerEnabled, timerData, dispatch, timerType]);
+ // Sends a dispatch to disable all widgets
+ const handleHideAllWidgets = e => {
+ // TODO: Need safe way to iterate through all widgets
+ // Handle both click events and keyboard events (Enter/Space)
+ if (!e.key || e.key === "Enter" || e.key === " ") {
+ 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`;
@@ -106,7 +117,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={handleHideAllWidgets}
+ onKeyDown={handleHideAllWidgets}
+ />
+ </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,17 @@ function Widgets() {
// Update the ref to track current state
prevTimerEnabledRef.current = isTimerEnabled;
}, [timerEnabled, timerData, dispatch, timerType]);
+ // Sends a dispatch to disable all widgets
+ const handleHideAllWidgets = e => {
+ // TODO: Need safe way to iterate through all widgets
+ // Handle both click events and keyboard events (Enter/Space)
+ if (!e.key || e.key === "Enter" || e.key === " ") {
+ (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 +13621,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: handleHideAllWidgets,
+ onKeyDown: handleHideAllWidgets
+ })), /*#__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({});
+
+ 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" });
+
+ 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: " " });
+
+ 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}`
+ );
+ }
+ });
+ });
});