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:
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();
+ });
+ });
});