Bug 1941807 - Add ability to toggle AboutWelcome screen content by clicking a configurable header r=omc-reviewers,jprickett

Support a second variant of the preonboarding modal design by adding the ability to configure a header that toggles visiblity of content tiles.

- See design for [[ https://docs.google.com/presentation/d/1dDC9M9y-W3q90mkVRp4gq3p-x3u7F6qgy0jVeE4MwUw/edit#slide=id.g325219d6753_0_95 | modal Variant B ]]

Differential Revision: https://phabricator.services.mozilla.com/D235325
This commit is contained in:
Meg Viar
2025-01-24 13:30:07 +00:00
parent b216db3f05
commit 7dca8efc49
6 changed files with 264 additions and 103 deletions

View File

@@ -1963,7 +1963,68 @@ html {
} }
} }
.content-tiles-container { .onboardingContainer .main-content.no-steps:has(button.content-tiles-header[aria-expanded='false']) {
padding-bottom: 0;
}
#content-tiles-container button.tile-header,
button.content-tiles-header {
border: 1px solid var(--in-content-border-color);
width: 100%;
margin: 0;
padding: 12px 16px;
display: flex;
flex-direction: row;
align-items: center;
border-radius: 0;
background-color: var(--in-content-page-background);
cursor: pointer;
// ensures focus ring is fully visible
outline-offset: -12px;
.arrow-icon {
width: 1em;
height: 1.5em;
-moz-context-properties: fill;
fill: currentColor;
background: url('chrome://global/skin/icons/arrow-down.svg') center center / 100% no-repeat;
}
&[aria-expanded='true'] {
border-end-start-radius: 0;
border-end-end-radius: 0;
.arrow-icon {
background: url('chrome://global/skin/icons/arrow-up.svg') center center / 100% no-repeat;
}
}
}
button.content-tiles-header {
margin: 0.5em 0 0;
font-size: 11px;
font-weight: 400;
justify-content: center;
border-width: 1px 0;
@media (prefers-contrast: no-preference) {
color: #5B5B66;
}
@media (prefers-contrast: no-preference) and (prefers-color-scheme: dark) {
color: #CFCFD8;
}
@media (prefers-contrast) {
color: var(--in-content-page-color);
}
.arrow-icon {
margin-inline: 1em
}
}
#content-tiles-container {
--tiles-container-border-radius: 8px; --tiles-container-border-radius: 8px;
margin: 24px 48px; margin: 24px 48px;
@@ -2005,18 +2066,8 @@ html {
} }
button.tile-header { button.tile-header {
border: 1px solid var(--in-content-border-color);
width: 100%;
margin: 0;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
border-radius: 0;
font-size: 13px; font-size: 13px;
padding: 12px 16px; justify-content: space-between;
background-color: var(--in-content-page-background);
cursor: pointer;
& + .tile-content { & + .tile-content {
border-inline-start: 1px solid var(--in-content-border-color); border-inline-start: 1px solid var(--in-content-border-color);
@@ -2037,25 +2088,6 @@ html {
line-height: 1.5; line-height: 1.5;
} }
} }
.arrow-icon {
background-repeat: no-repeat;
background-position: center;
width: 1em;
height: 1.5em;
-moz-context-properties: fill;
fill: currentColor;
background-image: url('chrome://global/skin/icons/arrow-down.svg');
}
&[aria-expanded='true'] {
border-end-start-radius: 0;
border-end-end-radius: 0;
.arrow-icon {
background-image: url('chrome://global/skin/icons/arrow-up.svg');
}
}
} }
.multi-select-container { .multi-select-container {

View File

@@ -32,6 +32,8 @@ const TILE_STYLES = [
export const ContentTiles = props => { export const ContentTiles = props => {
const { content } = props; const { content } = props;
const [expandedTileIndex, setExpandedTileIndex] = useState(null); const [expandedTileIndex, setExpandedTileIndex] = useState(null);
// State for header that toggles showing and hiding all tiles, if applicable
const [tilesHeaderExpanded, setTilesHeaderExpanded] = useState(false);
const { tiles } = content; const { tiles } = content;
if (!tiles) { if (!tiles) {
return null; return null;
@@ -43,6 +45,14 @@ export const ContentTiles = props => {
AboutWelcomeUtils.sendActionTelemetry(props.messageId, tileId); AboutWelcomeUtils.sendActionTelemetry(props.messageId, tileId);
}; };
const toggleTiles = () => {
setTilesHeaderExpanded(prev => !prev);
AboutWelcomeUtils.sendActionTelemetry(
props.messageId,
"content_tiles_header"
);
};
const renderContentTile = (tile, index = 0) => { const renderContentTile = (tile, index = 0) => {
const isExpanded = expandedTileIndex === index; const isExpanded = expandedTileIndex === index;
const { header } = tile; const { header } = tile;
@@ -126,13 +136,35 @@ export const ContentTiles = props => {
); );
}; };
if (Array.isArray(content.tiles)) { const renderContentTiles = () => {
if (Array.isArray(tiles)) {
return (
<div id="content-tiles-container">
{tiles.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 (content.tiles_header) {
return ( return (
<div className="content-tiles-container"> <React.Fragment>
{content.tiles.map((tile, index) => renderContentTile(tile, index))} <button
</div> className="content-tiles-header"
onClick={toggleTiles}
aria-expanded={tilesHeaderExpanded}
aria-controls={`content-tiles-container`}
>
<Localized text={content.tiles_header.title}>
<span className="header-title" />
</Localized>
<div className="arrow-icon"></div>
</button>
{tilesHeaderExpanded && renderContentTiles()}
</React.Fragment>
); );
} }
// If tiles is not an array render the tile alone without a container return renderContentTiles(tiles);
return renderContentTile(tiles, 0);
}; };

View File

@@ -2026,6 +2026,8 @@ const ContentTiles = props => {
content content
} = props; } = props;
const [expandedTileIndex, setExpandedTileIndex] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(null); const [expandedTileIndex, setExpandedTileIndex] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(null);
// State for header that toggles showing and hiding all tiles, if applicable
const [tilesHeaderExpanded, setTilesHeaderExpanded] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
const { const {
tiles tiles
} = content; } = content;
@@ -2037,6 +2039,10 @@ const ContentTiles = props => {
setExpandedTileIndex(prevIndex => prevIndex === index ? null : index); setExpandedTileIndex(prevIndex => prevIndex === index ? null : index);
_lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_9__.AboutWelcomeUtils.sendActionTelemetry(props.messageId, tileId); _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_9__.AboutWelcomeUtils.sendActionTelemetry(props.messageId, tileId);
}; };
const toggleTiles = () => {
setTilesHeaderExpanded(prev => !prev);
_lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_9__.AboutWelcomeUtils.sendActionTelemetry(props.messageId, "content_tiles_header");
};
const renderContentTile = (tile, index = 0) => { const renderContentTile = (tile, index = 0) => {
const isExpanded = expandedTileIndex === index; const isExpanded = expandedTileIndex === index;
const { const {
@@ -2106,13 +2112,30 @@ const ContentTiles = props => {
style: tile.data.style style: tile.data.style
})) : null); })) : null);
}; };
if (Array.isArray(content.tiles)) { const renderContentTiles = () => {
return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { if (Array.isArray(tiles)) {
className: "content-tiles-container" return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
}, content.tiles.map((tile, index) => renderContentTile(tile, index))); id: "content-tiles-container"
}, tiles.map((tile, index) => renderContentTile(tile, index)));
}
// If tiles is not an array render the tile alone without a container
return renderContentTile(tiles, 0);
};
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",
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: 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());
} }
// If tiles is not an array render the tile alone without a container return renderContentTiles(tiles);
return renderContentTile(tiles, 0);
}; };
/***/ }), /***/ }),

View File

@@ -2925,92 +2925,125 @@ html {
.onboardingContainer .mobile-download-buttons li:not(:first-child) { .onboardingContainer .mobile-download-buttons li:not(:first-child) {
margin-inline: 5px 0; margin-inline: 5px 0;
} }
.onboardingContainer .content-tiles-container { .onboardingContainer .onboardingContainer .main-content.no-steps:has(button.content-tiles-header[aria-expanded=false]) {
--tiles-container-border-radius: 8px; padding-bottom: 0;
margin: 24px 48px;
} }
.onboardingContainer .content-tiles-container .content-tile { .onboardingContainer #content-tiles-container button.tile-header,
display: flex; .onboardingContainer button.content-tiles-header {
flex-direction: column;
}
.onboardingContainer .content-tiles-container .content-tile.has-header:first-of-type button.tile-header, .onboardingContainer .content-tiles-container .content-tile:not(.has-header) + .content-tile.has-header button.tile-header {
border-start-start-radius: var(--tiles-container-border-radius);
border-start-end-radius: var(--tiles-container-border-radius);
}
.onboardingContainer .content-tiles-container .content-tile.has-header:not(:has(+ .content-tile.has-header)) button.tile-header {
border-end-start-radius: var(--tiles-container-border-radius);
border-end-end-radius: var(--tiles-container-border-radius);
}
.onboardingContainer .content-tiles-container .content-tile.has-header:not(:has(+ .content-tile.has-header)) button.tile-header[aria-expanded=true] {
border-end-start-radius: 0;
border-end-end-radius: 0;
}
.onboardingContainer .content-tiles-container .content-tile.has-header:not(:has(+ .content-tile.has-header)) button.tile-header[aria-expanded=true] + .tile-content {
border: 1px solid var(--in-content-border-color);
border-top: none;
border-radius: 0 0 var(--tiles-container-border-radius) var(--tiles-container-border-radius);
}
.onboardingContainer .content-tiles-container .content-tile.has-header:has(+ .content-tile.has-header) button.tile-header[aria-expanded=false] {
border-bottom: none;
}
.onboardingContainer .content-tiles-container .content-tile button.tile-header {
border: 1px solid var(--in-content-border-color); border: 1px solid var(--in-content-border-color);
width: 100%; width: 100%;
margin: 0; margin: 0;
padding: 12px 16px;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between;
align-items: center; align-items: center;
border-radius: 0; border-radius: 0;
font-size: 13px;
padding: 12px 16px;
background-color: var(--in-content-page-background); background-color: var(--in-content-page-background);
cursor: pointer; cursor: pointer;
outline-offset: -12px;
} }
.onboardingContainer .content-tiles-container .content-tile button.tile-header + .tile-content { .onboardingContainer #content-tiles-container button.tile-header .arrow-icon,
border-inline-start: 1px solid var(--in-content-border-color); .onboardingContainer button.content-tiles-header .arrow-icon {
border-inline-end: 1px solid var(--in-content-border-color);
}
.onboardingContainer .content-tiles-container .content-tile button.tile-header .header-text-container {
display: flex;
flex-direction: column;
}
.onboardingContainer .content-tiles-container .content-tile button.tile-header .header-text-container .header-title {
font-weight: 590;
line-height: 1.5;
}
.onboardingContainer .content-tiles-container .content-tile button.tile-header .header-text-container .header-subtitle {
font-weight: 400;
line-height: 1.5;
}
.onboardingContainer .content-tiles-container .content-tile button.tile-header .arrow-icon {
background-repeat: no-repeat;
background-position: center;
width: 1em; width: 1em;
height: 1.5em; height: 1.5em;
-moz-context-properties: fill; -moz-context-properties: fill;
fill: currentColor; fill: currentColor;
background-image: url("chrome://global/skin/icons/arrow-down.svg"); background: url("chrome://global/skin/icons/arrow-down.svg") center center/100% no-repeat;
} }
.onboardingContainer .content-tiles-container .content-tile button.tile-header[aria-expanded=true] { .onboardingContainer #content-tiles-container button.tile-header[aria-expanded=true],
.onboardingContainer button.content-tiles-header[aria-expanded=true] {
border-end-start-radius: 0; border-end-start-radius: 0;
border-end-end-radius: 0; border-end-end-radius: 0;
} }
.onboardingContainer .content-tiles-container .content-tile button.tile-header[aria-expanded=true] .arrow-icon { .onboardingContainer #content-tiles-container button.tile-header[aria-expanded=true] .arrow-icon,
background-image: url("chrome://global/skin/icons/arrow-up.svg"); .onboardingContainer button.content-tiles-header[aria-expanded=true] .arrow-icon {
background: url("chrome://global/skin/icons/arrow-up.svg") center center/100% no-repeat;
} }
.onboardingContainer .content-tiles-container .content-tile .multi-select-container { .onboardingContainer button.content-tiles-header {
margin: 0.5em 0 0;
font-size: 11px;
font-weight: 400;
justify-content: center;
border-width: 1px 0;
}
@media (prefers-contrast: no-preference) {
.onboardingContainer button.content-tiles-header {
color: #5B5B66;
}
}
@media (prefers-contrast: no-preference) and (prefers-color-scheme: dark) {
.onboardingContainer button.content-tiles-header {
color: #CFCFD8;
}
}
@media (prefers-contrast) {
.onboardingContainer button.content-tiles-header {
color: var(--in-content-page-color);
}
}
.onboardingContainer button.content-tiles-header .arrow-icon {
margin-inline: 1em;
}
.onboardingContainer #content-tiles-container {
--tiles-container-border-radius: 8px;
margin: 24px 48px;
}
.onboardingContainer #content-tiles-container .content-tile {
display: flex;
flex-direction: column;
}
.onboardingContainer #content-tiles-container .content-tile.has-header:first-of-type button.tile-header, .onboardingContainer #content-tiles-container .content-tile:not(.has-header) + .content-tile.has-header button.tile-header {
border-start-start-radius: var(--tiles-container-border-radius);
border-start-end-radius: var(--tiles-container-border-radius);
}
.onboardingContainer #content-tiles-container .content-tile.has-header:not(:has(+ .content-tile.has-header)) button.tile-header {
border-end-start-radius: var(--tiles-container-border-radius);
border-end-end-radius: var(--tiles-container-border-radius);
}
.onboardingContainer #content-tiles-container .content-tile.has-header:not(:has(+ .content-tile.has-header)) button.tile-header[aria-expanded=true] {
border-end-start-radius: 0;
border-end-end-radius: 0;
}
.onboardingContainer #content-tiles-container .content-tile.has-header:not(:has(+ .content-tile.has-header)) button.tile-header[aria-expanded=true] + .tile-content {
border: 1px solid var(--in-content-border-color);
border-top: none;
border-radius: 0 0 var(--tiles-container-border-radius) var(--tiles-container-border-radius);
}
.onboardingContainer #content-tiles-container .content-tile.has-header:has(+ .content-tile.has-header) button.tile-header[aria-expanded=false] {
border-bottom: none;
}
.onboardingContainer #content-tiles-container .content-tile button.tile-header {
font-size: 13px;
justify-content: space-between;
}
.onboardingContainer #content-tiles-container .content-tile button.tile-header + .tile-content {
border-inline-start: 1px solid var(--in-content-border-color);
border-inline-end: 1px solid var(--in-content-border-color);
}
.onboardingContainer #content-tiles-container .content-tile button.tile-header .header-text-container {
display: flex;
flex-direction: column;
}
.onboardingContainer #content-tiles-container .content-tile button.tile-header .header-text-container .header-title {
font-weight: 590;
line-height: 1.5;
}
.onboardingContainer #content-tiles-container .content-tile button.tile-header .header-text-container .header-subtitle {
font-weight: 400;
line-height: 1.5;
}
.onboardingContainer #content-tiles-container .content-tile .multi-select-container {
padding: 24px; padding: 24px;
margin: 0; margin: 0;
} }
.onboardingContainer .content-tiles-container .content-tile .multi-select-container .checkbox-container { .onboardingContainer #content-tiles-container .content-tile .multi-select-container .checkbox-container {
display: grid; display: grid;
} }
.onboardingContainer .content-tiles-container .content-tile .multi-select-container .checkbox-container label, .onboardingContainer #content-tiles-container .content-tile .multi-select-container .checkbox-container label,
.onboardingContainer .content-tiles-container .content-tile .multi-select-container .checkbox-container p { .onboardingContainer #content-tiles-container .content-tile .multi-select-container .checkbox-container p {
grid-column: 2; grid-column: 2;
} }
.onboardingContainer .content-tiles-container .content-tile .multi-select-container .checkbox-container p { .onboardingContainer #content-tiles-container .content-tile .multi-select-container .checkbox-container p {
margin-block: 0.5em 0; margin-block: 0.5em 0;
} }
.onboardingContainer .dismiss-button { .onboardingContainer .dismiss-button {

View File

@@ -96,7 +96,7 @@ describe("ContentTiles component", () => {
assert.equal(telemetrySpy.firstCall.args[1], tileId); assert.equal(telemetrySpy.firstCall.args[1], tileId);
}); });
it("should only expand on tiles at a time", () => { it("should only expand one tile at a time", () => {
const firstTileButton = wrapper.find(".tile-header").at(0); const firstTileButton = wrapper.find(".tile-header").at(0);
firstTileButton.simulate("click"); firstTileButton.simulate("click");
const secondTileButton = wrapper.find(".tile-header").at(1); const secondTileButton = wrapper.find(".tile-header").at(1);
@@ -109,6 +109,44 @@ describe("ContentTiles component", () => {
); );
}); });
it("should toggle all tiles and send telemetry when the tiles header is clicked", () => {
const TEST_CONTENT_HEADER = {
tiles: [CHECKLIST_TILE, MOBILE_TILE],
tiles_header: {
title: "Toggle Tiles Header",
},
};
wrapper = mount(
<ContentTiles content={TEST_CONTENT_HEADER} handleAction={handleAction} />
);
let telemetrySpy = sandbox.spy(AboutWelcomeUtils, "sendActionTelemetry");
const tilesHeaderButton = wrapper.find(".content-tiles-header");
assert.ok(tilesHeaderButton.exists(), "Tiles header button should exist");
tilesHeaderButton.simulate("click");
assert.equal(
wrapper.find("#content-tiles-container").exists(),
true,
"Content tiles container should be visible after toggle"
);
assert.calledOnce(telemetrySpy);
assert.equal(
telemetrySpy.firstCall.args[1],
"content_tiles_header",
"Telemetry should be sent for tiles header toggle"
);
tilesHeaderButton.simulate("click");
assert.equal(
wrapper.find("#content-tiles-container").exists(),
false,
"Content tiles container should not be visible after second toggle"
);
});
it("should apply configured styles to the header buttons", () => { it("should apply configured styles to the header buttons", () => {
const mountedWrapper = mount( const mountedWrapper = mount(
<ContentTiles content={TEST_CONTENT} handleAction={() => {}} /> <ContentTiles content={TEST_CONTENT} handleAction={() => {}} />

View File

@@ -88,6 +88,7 @@ const MESSAGES = () => [
display: "block", display: "block",
padding: "20px 0 0 0", padding: "20px 0 0 0",
width: "560px", width: "560px",
overflow: "auto",
}, },
logo: { logo: {
height: "40px", height: "40px",
@@ -100,6 +101,7 @@ const MESSAGES = () => [
raw: "Review the content below before continuing.", raw: "Review the content below before continuing.",
fontSize: "15px", fontSize: "15px",
}, },
tiles_header: { title: "Click to toggle content tiles." },
tiles: [ tiles: [
{ {
type: "embedded_browser", type: "embedded_browser",
@@ -130,6 +132,7 @@ const MESSAGES = () => [
}, },
{ {
type: "multiselect", type: "multiselect",
style: { marginBlock: "18px" },
data: [ data: [
{ {
id: "checkbox-test-1", id: "checkbox-test-1",