tor-browser

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

commit aa49ecf770e7800d10d440e119fa9bcb2fcc2f09
parent 8d14e444492dc12aaa86fb7c9cbfa3a2cdc2a156
Author: mimi <nsauermann@mozilla.com>
Date:   Thu,  6 Nov 2025 15:58:20 +0000

Bug 1998540 - Revert content tiles normalization r=omc-reviewers,mviar

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

Diffstat:
Mbrowser/components/aboutwelcome/content-src/components/ContentTiles.jsx | 57+++++++++++++++++++--------------------------------------
Mbrowser/components/aboutwelcome/content-src/lib/aboutwelcome-utils.mjs | 43-------------------------------------------
Mbrowser/components/aboutwelcome/content/aboutwelcome.bundle.js | 80++++++++++++-------------------------------------------------------------------
Mbrowser/components/aboutwelcome/tests/unit/ContentTiles.test.jsx | 223-------------------------------------------------------------------------------
4 files changed, 31 insertions(+), 372 deletions(-)

diff --git a/browser/components/aboutwelcome/content-src/components/ContentTiles.jsx b/browser/components/aboutwelcome/content-src/components/ContentTiles.jsx @@ -59,18 +59,11 @@ export const ContentTiles = props => { return null; } - const { tile_items, container } = - AboutWelcomeUtils.normalizeContentTiles(content); - - if (!tile_items.length) { - return null; - } - // eslint-disable-next-line react-hooks/rules-of-hooks useEffect(() => { // Run once when ContentTiles mounts to prefill activeMultiSelect if (!props.activeMultiSelect) { - const tilesArray = Array.isArray(tile_items) ? tile_items : [tile_items]; + const tilesArray = Array.isArray(tiles) ? tiles : [tiles]; tilesArray.forEach((tile, index) => { if (tile.type !== "multiselect" || !tile.data) { @@ -91,7 +84,7 @@ export const ContentTiles = props => { } }); } - }, [tile_items]); // eslint-disable-line react-hooks/exhaustive-deps + }, [tiles]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { /** @@ -341,36 +334,24 @@ export const ContentTiles = props => { }; const renderContentTiles = () => { - const hasHeader = !!container?.header; - const hasContainerStyle = !!Object.keys(container?.style || {}).length; - - // Legacy rule: tiles as a single object renders without a container. - // Arrays (even length 1) render inside a container. - // Normalize helper will detect original input shape (object vs array) before normalizing to preserve intent. - const isArrayInput = Array.isArray(content.tiles); - if ( - !isArrayInput && - tile_items.length === 1 && - !hasHeader && - !hasContainerStyle - ) { - return renderContentTile(tile_items[0], 0); + if (Array.isArray(tiles)) { + return ( + <div + id="content-tiles-container" + style={AboutWelcomeUtils.getValidStyle( + content?.contentTilesContainer?.style, + CONTAINER_STYLES + )} + > + {tiles.map((tile, index) => renderContentTile(tile, index))} + </div> + ); } - - return ( - <div - id="content-tiles-container" - style={AboutWelcomeUtils.getValidStyle( - container?.style, - CONTAINER_STYLES - )} - > - {tile_items.map((tile, index) => renderContentTile(tile, index))} - </div> - ); + // If tiles is not an array render the tile alone without a container + return renderContentTile(tiles, 0); }; - if (container?.header) { + if (content.tiles_header) { return ( <React.Fragment> <button @@ -379,7 +360,7 @@ export const ContentTiles = props => { aria-expanded={tilesHeaderExpanded} aria-controls={`content-tiles-container`} > - <Localized text={container.header?.title}> + <Localized text={content.tiles_header.title}> <span className="header-title" /> </Localized> <div className="arrow-icon"></div> @@ -388,5 +369,5 @@ export const ContentTiles = props => { </React.Fragment> ); } - return renderContentTiles(tile_items); + return renderContentTiles(tiles); }; diff --git a/browser/components/aboutwelcome/content-src/lib/aboutwelcome-utils.mjs b/browser/components/aboutwelcome/content-src/lib/aboutwelcome-utils.mjs @@ -100,47 +100,4 @@ export const AboutWelcomeUtils = { true ); }, - - /** - * Normalize content.tiles into a single shape: - * tiles: { tile_items: Array<Tile> | Tile, container?: { style?: Object, header?: Object } } - * - * Supports legacy tiles array and single tile object and consumes - * legacy container `content.contentTilesContainer.style` and - * legacy header `content.tiles_header` properties. - */ - normalizeContentTiles(content) { - const { tiles } = content; - const legacyContainer = content?.contentTilesContainer; - const legacyHeader = content?.tiles_header; - - // Prefer tiles.container styles, fallback to legacy style, default {} - const style = tiles?.container?.style ?? legacyContainer?.style ?? {}; - - // Prefer tiles.container.header, fall back to legacy header - const header = tiles?.container?.header ?? legacyHeader; - - let items; - // New shape - if (tiles?.tile_items !== undefined) { - items = Array.isArray(tiles.tile_items) - ? tiles.tile_items - : [tiles.tile_items]; - } - // Legacy tiles array - else if (Array.isArray(tiles)) { - items = tiles; - } - // Legacy tiles object - else if (tiles && typeof tiles === "object" && tiles.type) { - items = [tiles]; - } else { - items = []; - } - - // Omit header when absent - const container = header ? { style, header } : { style }; - - return { tile_items: items, container }; - }, }; diff --git a/browser/components/aboutwelcome/content/aboutwelcome.bundle.js b/browser/components/aboutwelcome/content/aboutwelcome.bundle.js @@ -129,49 +129,6 @@ const AboutWelcomeUtils = { true ); }, - - /** - * Normalize content.tiles into a single shape: - * tiles: { tile_items: Array<Tile> | Tile, container?: { style?: Object, header?: Object } } - * - * Supports legacy tiles array and single tile object and consumes - * legacy container `content.contentTilesContainer.style` and - * legacy header `content.tiles_header` properties. - */ - normalizeContentTiles(content) { - const { tiles } = content; - const legacyContainer = content?.contentTilesContainer; - const legacyHeader = content?.tiles_header; - - // Prefer tiles.container styles, fallback to legacy style, default {} - const style = tiles?.container?.style ?? legacyContainer?.style ?? {}; - - // Prefer tiles.container.header, fall back to legacy header - const header = tiles?.container?.header ?? legacyHeader; - - let items; - // New shape - if (tiles?.tile_items !== undefined) { - items = Array.isArray(tiles.tile_items) - ? tiles.tile_items - : [tiles.tile_items]; - } - // Legacy tiles array - else if (Array.isArray(tiles)) { - items = tiles; - } - // Legacy tiles object - else if (tiles && typeof tiles === "object" && tiles.type) { - items = [tiles]; - } else { - items = []; - } - - // Omit header when absent - const container = header ? { style, header } : { style }; - - return { tile_items: items, container }; - }, }; @@ -2386,19 +2343,12 @@ const ContentTiles = props => { if (!tiles) { return null; } - const { - tile_items, - container - } = _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_11__.AboutWelcomeUtils.normalizeContentTiles(content); - if (!tile_items.length) { - return null; - } // eslint-disable-next-line react-hooks/rules-of-hooks (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => { // Run once when ContentTiles mounts to prefill activeMultiSelect if (!props.activeMultiSelect) { - const tilesArray = Array.isArray(tile_items) ? tile_items : [tile_items]; + const tilesArray = Array.isArray(tiles) ? tiles : [tiles]; tilesArray.forEach((tile, index) => { if (tile.type !== "multiselect" || !tile.data) { return; @@ -2418,7 +2368,7 @@ const ContentTiles = props => { } }); } - }, [tile_items]); // eslint-disable-line react-hooks/exhaustive-deps + }, [tiles]); // eslint-disable-line react-hooks/exhaustive-deps (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => { /** @@ -2611,36 +2561,30 @@ const ContentTiles = props => { })) : null); }; const renderContentTiles = () => { - const hasHeader = !!container?.header; - const hasContainerStyle = !!Object.keys(container?.style || {}).length; - - // Legacy rule: tiles as a single object renders without a container. - // Arrays (even length 1) render inside a container. - // Normalize helper will detect original input shape (object vs array) before normalizing to preserve intent. - const isArrayInput = Array.isArray(content.tiles); - if (!isArrayInput && tile_items.length === 1 && !hasHeader && !hasContainerStyle) { - return renderContentTile(tile_items[0], 0); + if (Array.isArray(tiles)) { + return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { + id: "content-tiles-container", + style: _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_11__.AboutWelcomeUtils.getValidStyle(content?.contentTilesContainer?.style, CONTAINER_STYLES) + }, tiles.map((tile, index) => renderContentTile(tile, index))); } - return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { - id: "content-tiles-container", - style: _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_11__.AboutWelcomeUtils.getValidStyle(container?.style, CONTAINER_STYLES) - }, tile_items.map((tile, index) => renderContentTile(tile, index))); + // If tiles is not an array render the tile alone without a container + return renderContentTile(tiles, 0); }; - if (container?.header) { + if (content.tiles_header) { return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("button", { className: "content-tiles-header secondary", onClick: toggleTiles, "aria-expanded": tilesHeaderExpanded, "aria-controls": `content-tiles-container` }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, { - text: container.header?.title + text: content.tiles_header.title }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "header-title" })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "arrow-icon" })), tilesHeaderExpanded && renderContentTiles()); } - return renderContentTiles(tile_items); + return renderContentTiles(tiles); }; /***/ }), diff --git a/browser/components/aboutwelcome/tests/unit/ContentTiles.test.jsx b/browser/components/aboutwelcome/tests/unit/ContentTiles.test.jsx @@ -1062,227 +1062,4 @@ describe("ContentTiles component", () => { mounted.unmount(); }); - - it("renders container when single tile has container.style", () => { - const SINGLE_TILE_WITH_STYLE = { - tiles: { - container: { style: { marginBlock: "40px" } }, - tile_items: { - type: "mobile_downloads", - data: { email: { link_text: "Email!!" } }, - }, - }, - }; - - const mounted = mount( - <ContentTiles - content={SINGLE_TILE_WITH_STYLE} - handleAction={() => {}} - activeMultiSelect={null} - setActiveMultiSelect={setActiveMultiSelect} - /> - ); - - const container = mounted.find("#content-tiles-container"); - assert.isTrue(container.exists(), "container should be rendered"); - - const el = container.getDOMNode(); - assert.match( - el.style.cssText, - "margin-block: 40px", - "container style applied" - ); - - mounted.unmount(); - }); - - it("renders container when single tile has header", () => { - const SINGLE_TILE_WITH_HEADER = { - tiles: { - container: { - header: { title: "Toggle tiles header" }, - }, - tile_items: { - type: "mobile_downloads", - data: { email: { link_text: "Email!!" } }, - }, - }, - }; - - const mounted = mount( - <ContentTiles - content={SINGLE_TILE_WITH_HEADER} - handleAction={() => {}} - activeMultiSelect={null} - setActiveMultiSelect={setActiveMultiSelect} - /> - ); - - const headerBtn = mounted.find(".content-tiles-header"); - assert.isTrue(headerBtn.exists(), "header button should render"); - - assert.isFalse( - mounted.find("#content-tiles-container").exists(), - "container should be hidden initially" - ); - - headerBtn.simulate("click"); - assert.isTrue( - mounted.find("#content-tiles-container").exists(), - "container should render after toggle" - ); - - mounted.unmount(); - }); - - it("supports legacy content.tiles_header as container header", () => { - const LEGACY_HEADER_SINGLE_TILE = { - tiles_header: { title: "Legacy tile header" }, - tiles: { - type: "multiselect", - header: { - title: "Multiselect header", - }, - data: [{ id: "option1", defaultValue: true }], - }, - }; - - const mounted = mount( - <ContentTiles - content={LEGACY_HEADER_SINGLE_TILE} - handleAction={() => {}} - activeMultiSelect={null} - setActiveMultiSelect={setActiveMultiSelect} - /> - ); - - const headerBtn = mounted.find(".content-tiles-header"); - assert.isTrue(headerBtn.exists(), "legacy header button should render"); - - assert.isFalse( - mounted.find("#content-tiles-container").exists(), - "container hidden initially" - ); - - headerBtn.simulate("click"); - assert.isTrue( - mounted.find("#content-tiles-container").exists(), - "container visible after toggle" - ); - - mounted.unmount(); - }); - - it("supports legacy content.contentTilesContainer.style as container style", () => { - const LEGACY_CONTAINER_STYLE = { - contentTilesContainer: { style: { marginBlock: "6px" } }, - tiles: [ - { - type: "multiselect", - header: { - title: "Multiselect Header", - }, - data: [{ id: "option1", defaultValue: true }], - }, - ], - }; - - const mounted = mount( - <ContentTiles - content={LEGACY_CONTAINER_STYLE} - handleAction={() => {}} - activeMultiSelect={null} - setActiveMultiSelect={setActiveMultiSelect} - setScreenMultiSelects={sandbox.stub()} - /> - ); - - const container = mounted.find("#content-tiles-container"); - assert.isTrue(container.exists(), "container should be rendered"); - - const el = container.getDOMNode(); - assert.match( - el.style.cssText, - "margin-block: 6px", - "legacy container style applied" - ); - - mounted.unmount(); - }); - - it("renders header toggle when only header.subtitle is provided", () => { - const HEADER_WITH_SUBTITLE_ONLY = { - tiles: { - container: { - header: { subtitle: "Only a subtitle" }, - }, - tile_items: [ - { - type: "mobile_downloads", - data: { email: { link_text: "Email yourself a link" } }, - }, - ], - }, - }; - - const mounted = mount( - <ContentTiles - content={HEADER_WITH_SUBTITLE_ONLY} - handleAction={() => {}} - activeMultiSelect={null} - setActiveMultiSelect={setActiveMultiSelect} - /> - ); - - const headerBtn = mounted.find(".content-tiles-header"); - assert.isTrue( - headerBtn.exists(), - "header toggle renders with subtitle only" - ); - - headerBtn.simulate("click"); - assert.isTrue( - mounted.find("#content-tiles-container").exists(), - "container renders after toggle" - ); - - mounted.unmount(); - }); - - it("multiple tiles render inside container and count matches", () => { - const NEW_SHAPE_MULTIPLE = { - tiles: { - tile_items: [ - { - type: "mobile_downloads", - data: { email: { link_text: "Email" } }, - }, - { - type: "multiselect", - header: { - title: "Multiselect header", - }, - data: [{ id: "option1", defaultValue: true }], - }, - ], - }, - }; - - const mounted = mount( - <ContentTiles - content={NEW_SHAPE_MULTIPLE} - handleAction={() => {}} - activeMultiSelect={null} - setActiveMultiSelect={setActiveMultiSelect} - /> - ); - - assert.isTrue( - mounted.find("#content-tiles-container").exists(), - "container present" - ); - assert.equal(mounted.find(".content-tile").length, 2, "renders both tiles"); - - mounted.unmount(); - }); });