Skip to content

Commit 6015e44

Browse files
ScottDowneireneni
authored andcommitted
Bug 2061828 - Add support for Newtab five column content feed layouts r=ini
Differential Revision: https://phabricator.services.mozilla.com/D318551
1 parent c37f623 commit 6015e44

9 files changed

Lines changed: 236 additions & 17 deletions

File tree

browser/extensions/newtab/content-src/components/Base/Base.jsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1095,6 +1095,21 @@ export class BaseContent extends React.PureComponent {
10951095
.join(" ");
10961096
const logoShouldBeCentered =
10971097
noFeedOrContentWidgets && !hasManyTopSitesRows;
1098+
// The 5-column story grid is driven by the layout data alone: the content
1099+
// band only widens when every section has a columnCount: 5 entry. Sections
1100+
// share one subgrid track count, so a layout set where only some sections
1101+
// define 5 columns has to stay at 4 — widening it would leave the others
1102+
// with no tile for the active breakpoint, and nothing to render.
1103+
const sectionsWithLayouts = Object.values(
1104+
props.DiscoveryStream.feeds?.data ?? {}
1105+
).find(feed => feed?.data?.sections?.length)?.data?.sections;
1106+
const hasFiveColumnLayout =
1107+
!!sectionsWithLayouts?.length &&
1108+
sectionsWithLayouts.every(section =>
1109+
section.layout?.responsiveLayouts?.some(
1110+
layout => layout.columnCount === 5
1111+
)
1112+
);
10981113
// Rendered as a direct child of .container unless the logo is centered,
10991114
// so position: sticky is bounded by .container (which spans the whole
11001115
// page) rather than .content (which now ends above the content band).
@@ -1147,7 +1162,7 @@ export class BaseContent extends React.PureComponent {
11471162
className={`nova-outer-wrapper${this.state.fixedSearch ? " stuck-search" : ""}`}
11481163
>
11491164
<div
1150-
className={`container nova-enabled${logoShouldBeCentered ? " logo-in-content" : ""}`}
1165+
className={`container nova-enabled${logoShouldBeCentered ? " logo-in-content" : ""}${hasFiveColumnLayout ? " sections-5-col" : ""}`}
11511166
>
11521167
<aside className="sidebar-inline-start">
11531168
{!prefs.hideLogo && !logoShouldBeCentered && !isPageEmpty && (

browser/extensions/newtab/content-src/components/Base/_Grid.scss

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,17 @@
4040
--col-span-3: #{--col-span(3)};
4141
--col-span-12: #{--col-span(12)};
4242
--col-span-16: #{--col-span(16)};
43+
--col-span-20: #{--col-span(20)};
4344

4445
// Maximum width of .content: 16 columns + 15 gaps
4546
--content-max-width: var(--col-span-16);
4647
}
4748

49+
// A 5-column story grid needs 20.
50+
.nova-enabled.container.sections-5-col {
51+
--content-max-width: var(--col-span-20);
52+
}
53+
4854
.nova-enabled.container {
4955
container-type: inline-size;
5056
container-name: outer-grid;
@@ -106,8 +112,8 @@
106112
}
107113

108114
// Spans all three columns, so it reclaims both side gutters and their gaps
109-
// (324px) at widths where .content is still bounded by them. Capped at the
110-
// same 16 columns .content is, so wide viewports are unchanged.
115+
// (324px) at widths where .content is still bounded by them. Capped at the same
116+
// columns .content is, so the two stay aligned at every width.
111117
// TODO(Bug 2061740): at the smallest widths this now runs under
112118
// .personalizeButtonWrapper (position: fixed, inset-inline-end: --space-xlarge,
113119
// z-index 5), which used to sit clear of .content in the side gutter. Needs the

browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/CardSections/_CardSections.scss

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -514,7 +514,8 @@
514514
.ds-image {
515515
&.image-2,
516516
&.image-3,
517-
&.image-4 {
517+
&.image-4,
518+
&.image-5 {
518519
display: none;
519520
}
520521

@@ -602,7 +603,8 @@
602603
.ds-image {
603604
&.image-1,
604605
&.image-3,
605-
&.image-4 {
606+
&.image-4,
607+
&.image-5 {
606608
display: none;
607609
}
608610

@@ -670,7 +672,8 @@
670672
.ds-image {
671673
&.image-1,
672674
&.image-2,
673-
&.image-4 {
675+
&.image-4,
676+
&.image-5 {
674677
display: none;
675678
}
676679

@@ -738,7 +741,8 @@
738741
.ds-image {
739742
&.image-1,
740743
&.image-2,
741-
&.image-3 {
744+
&.image-3,
745+
&.image-5 {
742746
display: none;
743747
}
744748

@@ -832,7 +836,7 @@
832836
grid-column: span 2;
833837
}
834838

835-
.ds-image.image-1, .ds-image.image-3, .ds-image.image-4 { display: none; }
839+
.ds-image.image-1, .ds-image.image-3, .ds-image.image-4, .ds-image.image-5 { display: none; }
836840
.ds-image.image-2 { display: block; }
837841
}
838842

@@ -866,11 +870,11 @@
866870
grid-column: span 2;
867871
}
868872

869-
.ds-image.image-1, .ds-image.image-2, .ds-image.image-4 { display: none; }
873+
.ds-image.image-1, .ds-image.image-2, .ds-image.image-4, .ds-image.image-5 { display: none; }
870874
.ds-image.image-3 { display: block; }
871875
}
872876

873-
@include at-content-cols(16) {
877+
@include at-content-cols-range(16, 20) {
874878
--sections-col-count: 4;
875879

876880
@for $i from 0 through 16 {
@@ -900,9 +904,43 @@
900904
grid-column: span 2;
901905
}
902906

903-
.ds-image.image-1, .ds-image.image-2, .ds-image.image-3 { display: none; }
907+
.ds-image.image-1, .ds-image.image-2, .ds-image.image-3, .ds-image.image-5 { display: none; }
904908
.ds-image.image-4 { display: block; }
905909
}
910+
911+
@include at-content-cols(20) {
912+
--sections-col-count: 5;
913+
914+
@for $i from 0 through 16 {
915+
.col-5-position-#{$i} { order: $i; }
916+
}
917+
918+
.col-5-hidden { display: none; }
919+
920+
.col-5-small {
921+
@include section-card-small;
922+
}
923+
924+
.col-5-medium {
925+
@include section-card-medium;
926+
}
927+
928+
.col-5-large {
929+
@include section-card-large;
930+
}
931+
932+
.col-5-small,
933+
.col-5-medium {
934+
grid-column: span 1;
935+
}
936+
937+
.col-5-large {
938+
grid-column: span 2;
939+
}
940+
941+
.ds-image.image-1, .ds-image.image-2, .ds-image.image-3, .ds-image.image-4 { display: none; }
942+
.ds-image.image-5 { display: block; }
943+
}
906944
}
907945

908946
// @nova-cleanup(remove-conditional): Remove .nova-enabled wrapper, make default

browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -537,7 +537,9 @@ export class _DSCard extends React.PureComponent {
537537
renderSectionCardImages() {
538538
const { sectionsCardImageSizes } = this.props;
539539

540-
const columns = ["1", "2", "3", "4"];
540+
// Derived from the layout's breakpoints rather than a fixed list, so a
541+
// 5-column layout renders a 5th variant.
542+
const columns = Object.keys(sectionsCardImageSizes);
541543

542544
return (
543545
<>

browser/extensions/newtab/content-src/components/Widgets/Widgets.jsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -651,7 +651,7 @@ function Widgets() {
651651
// CSS container queries on the widgets section decide whether the toggle
652652
// button is shown — see _Widgets.scss. The collapsed row holds one widget
653653
// per card-column slot regardless of size, so for each card-column count
654-
// (1–4) anything past the first N positions overflows. This keeps mediums
654+
// (1–5) anything past the first N positions overflows. This keeps mediums
655655
// to a single (shorter) row rather than stacking them two-deep to fill a
656656
// large-height band. The matching `data-overflow-N` attribute is read by
657657
// the @container rules in CSS.
@@ -681,12 +681,14 @@ function Widgets() {
681681
2: hiddenIndicesAt(2),
682682
3: hiddenIndicesAt(3),
683683
4: hiddenIndicesAt(4),
684+
5: hiddenIndicesAt(5),
684685
};
685686
const overflowAttrs = {
686687
"data-overflow-1": overflowsAt(1) ? "" : undefined,
687688
"data-overflow-2": overflowsAt(2) ? "" : undefined,
688689
"data-overflow-3": overflowsAt(3) ? "" : undefined,
689690
"data-overflow-4": overflowsAt(4) ? "" : undefined,
691+
"data-overflow-5": overflowsAt(5) ? "" : undefined,
690692
};
691693
const isCollapsed = novaEnabled && !rowExpanded;
692694

@@ -744,6 +746,9 @@ function Widgets() {
744746
"data-hidden-4": hiddenAtCols[4].has(renderIdx)
745747
? ""
746748
: undefined,
749+
"data-hidden-5": hiddenAtCols[5].has(renderIdx)
750+
? ""
751+
: undefined,
747752
};
748753
const wrapperClassName = [
749754
size && `${size}-widget`,

browser/extensions/newtab/content-src/components/Widgets/_Widgets.scss

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -288,8 +288,9 @@
288288
// that viewport — combined with `data-row-collapsed`, drives
289289
// `display: none` so the container shrinks naturally (no clip
290290
// needed) and the widget tabs out of focus / a11y order.
291-
// One IAB card spans 4 grid columns, so the ranges below mirror 1-, 2-,
292-
// 3-, and 4-card viewports.
291+
// One IAB card spans 4 grid columns, so the ranges below mirror 1- through
292+
// 5-card viewports. The 5-card range is only reachable when the content band is
293+
// widened for a 5-column story grid; see .sections-5-col in _Grid.scss.
293294
.nova-enabled {
294295
@include at-content-cols-range(0, 8) {
295296
.widgets-section-container[data-overflow-1] .widgets-row-toggle {
@@ -321,7 +322,7 @@
321322
}
322323
}
323324

324-
@include at-content-cols(16) {
325+
@include at-content-cols-range(16, 20) {
325326
.widgets-section-container[data-overflow-4] .widgets-row-toggle {
326327
display: block;
327328
}
@@ -330,6 +331,16 @@
330331
display: none;
331332
}
332333
}
334+
335+
@include at-content-cols(20) {
336+
.widgets-section-container[data-overflow-5] .widgets-row-toggle {
337+
display: block;
338+
}
339+
340+
.widgets-container[data-row-collapsed] .widget-wrapper[data-hidden-5] {
341+
display: none;
342+
}
343+
}
333344
}
334345

335346
.nova-enabled {
@@ -358,6 +369,12 @@
358369
display: flex;
359370
}
360371
}
372+
373+
@include at-content-cols(20) {
374+
&:has(> :nth-child(5)):not(:has(> :nth-child(6))) > .widgets-add-button {
375+
display: flex;
376+
}
377+
}
361378
}
362379

363380
.widgets-add-button {

browser/extensions/newtab/content-src/styles/nova/_CardSections.scss

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,9 +256,15 @@
256256
.col-3-large { @include nova-section-card-large; }
257257
}
258258

259-
@include at-content-cols(16) {
259+
@include at-content-cols-range(16, 20) {
260260
.col-4-small { @include nova-section-card-small; }
261261
.col-4-medium { @include nova-section-card-medium; }
262262
.col-4-large { @include nova-section-card-large; }
263263
}
264+
265+
@include at-content-cols(20) {
266+
.col-5-small { @include nova-section-card-small; }
267+
.col-5-medium { @include nova-section-card-medium; }
268+
.col-5-large { @include nova-section-card-large; }
269+
}
264270
}

browser/extensions/newtab/test/jest/content-src/components/Base.test.jsx

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2016,3 +2016,72 @@ describe("<BaseContent> Highlights wrapper", () => {
20162016
).toBeTruthy();
20172017
});
20182018
});
2019+
2020+
describe("<BaseContent> five column gate", () => {
2021+
const DOCUMENT_STUB = {
2022+
visibilityState: "visible",
2023+
addEventListener: jest.fn(),
2024+
removeEventListener: jest.fn(),
2025+
};
2026+
2027+
const sectionWithColumns = columnCounts => ({
2028+
layout: {
2029+
responsiveLayouts: columnCounts.map(columnCount => ({
2030+
columnCount,
2031+
tiles: [],
2032+
})),
2033+
},
2034+
});
2035+
2036+
const renderWithSections = sections =>
2037+
renderBaseContentWithFeed({
2038+
store: { getState: () => {} },
2039+
App: { initialized: true },
2040+
Prefs: {
2041+
values: { "nova.enabled": true, "feeds.topsites": true },
2042+
},
2043+
Sections: [],
2044+
DiscoveryStream: {
2045+
config: { enabled: true },
2046+
spocs: {},
2047+
feeds: {
2048+
loaded: true,
2049+
data: { "https://example.com/feed": { data: { sections } } },
2050+
},
2051+
showTopicSelection: false,
2052+
},
2053+
dispatch: () => {},
2054+
document: DOCUMENT_STUB,
2055+
});
2056+
2057+
it("widens the content band when every section defines 5 columns", () => {
2058+
const { container } = renderWithSections([
2059+
sectionWithColumns([1, 2, 3, 4, 5]),
2060+
sectionWithColumns([1, 2, 3, 4, 5]),
2061+
]);
2062+
2063+
expect(container.querySelector(".container")).toHaveClass("sections-5-col");
2064+
});
2065+
2066+
// Sections share one subgrid track count, so a partial layout set has to stay
2067+
// at 4 columns; widening would leave the 4-column section with no tile for the
2068+
// active breakpoint and nothing to render.
2069+
it("stays at 4 columns when any section is missing a 5-column layout", () => {
2070+
const { container } = renderWithSections([
2071+
sectionWithColumns([1, 2, 3, 4, 5]),
2072+
sectionWithColumns([1, 2, 3, 4]),
2073+
]);
2074+
2075+
expect(container.querySelector(".container")).not.toHaveClass(
2076+
"sections-5-col"
2077+
);
2078+
});
2079+
2080+
it("stays at 4 columns before any sections have loaded", () => {
2081+
const { container } = renderWithSections([]);
2082+
2083+
expect(container.querySelector(".container")).not.toHaveClass(
2084+
"sections-5-col"
2085+
);
2086+
});
2087+
});

0 commit comments

Comments
 (0)