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