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;
margin: 24px 48px;
@@ -2005,18 +2066,8 @@ html {
}
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;
padding: 12px 16px;
background-color: var(--in-content-page-background);
cursor: pointer;
justify-content: space-between;
& + .tile-content {
border-inline-start: 1px solid var(--in-content-border-color);
@@ -2037,25 +2088,6 @@ html {
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 {

View File

@@ -32,6 +32,8 @@ const TILE_STYLES = [
export const ContentTiles = props => {
const { content } = props;
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;
if (!tiles) {
return null;
@@ -43,6 +45,14 @@ export const ContentTiles = props => {
AboutWelcomeUtils.sendActionTelemetry(props.messageId, tileId);
};
const toggleTiles = () => {
setTilesHeaderExpanded(prev => !prev);
AboutWelcomeUtils.sendActionTelemetry(
props.messageId,
"content_tiles_header"
);
};
const renderContentTile = (tile, index = 0) => {
const isExpanded = expandedTileIndex === index;
const { header } = tile;
@@ -126,13 +136,35 @@ export const ContentTiles = props => {
);
};
if (Array.isArray(content.tiles)) {
const renderContentTiles = () => {
if (Array.isArray(tiles)) {
return (
<div className="content-tiles-container">
{content.tiles.map((tile, index) => renderContentTile(tile, index))}
<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 (
<React.Fragment>
<button
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>
);
}
return renderContentTiles(tiles);
};

View File

@@ -2026,6 +2026,8 @@ const ContentTiles = props => {
content
} = props;
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 {
tiles
} = content;
@@ -2037,6 +2039,10 @@ const ContentTiles = props => {
setExpandedTileIndex(prevIndex => prevIndex === index ? null : index);
_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 isExpanded = expandedTileIndex === index;
const {
@@ -2106,13 +2112,30 @@ const ContentTiles = props => {
style: tile.data.style
})) : null);
};
if (Array.isArray(content.tiles)) {
const renderContentTiles = () => {
if (Array.isArray(tiles)) {
return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
className: "content-tiles-container"
}, 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());
}
return renderContentTiles(tiles);
};
/***/ }),

View File

@@ -2925,92 +2925,125 @@ html {
.onboardingContainer .mobile-download-buttons li:not(:first-child) {
margin-inline: 5px 0;
}
.onboardingContainer .content-tiles-container {
--tiles-container-border-radius: 8px;
margin: 24px 48px;
.onboardingContainer .onboardingContainer .main-content.no-steps:has(button.content-tiles-header[aria-expanded=false]) {
padding-bottom: 0;
}
.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 {
.onboardingContainer #content-tiles-container button.tile-header,
.onboardingContainer button.content-tiles-header {
border: 1px solid var(--in-content-border-color);
width: 100%;
margin: 0;
padding: 12px 16px;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
border-radius: 0;
font-size: 13px;
padding: 12px 16px;
background-color: var(--in-content-page-background);
cursor: pointer;
outline-offset: -12px;
}
.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 button.tile-header .arrow-icon {
background-repeat: no-repeat;
background-position: center;
.onboardingContainer #content-tiles-container button.tile-header .arrow-icon,
.onboardingContainer button.content-tiles-header .arrow-icon {
width: 1em;
height: 1.5em;
-moz-context-properties: fill;
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-end-radius: 0;
}
.onboardingContainer .content-tiles-container .content-tile button.tile-header[aria-expanded=true] .arrow-icon {
background-image: url("chrome://global/skin/icons/arrow-up.svg");
.onboardingContainer #content-tiles-container button.tile-header[aria-expanded=true] .arrow-icon,
.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;
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;
}
.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 label,
.onboardingContainer #content-tiles-container .content-tile .multi-select-container .checkbox-container p {
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;
}
.onboardingContainer .dismiss-button {

View File

@@ -96,7 +96,7 @@ describe("ContentTiles component", () => {
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);
firstTileButton.simulate("click");
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", () => {
const mountedWrapper = mount(
<ContentTiles content={TEST_CONTENT} handleAction={() => {}} />

View File

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