Skip to content

Commit 59030e6

Browse files
Allow the ScrollableCarousel.tsx to define card width by a fraction of the card using Visable slides on either mobile or tablet. Remove the css in the carousel that is front specific and put that behind the !isArticle boolean
1 parent 48f8df7 commit 59030e6

File tree

5 files changed

+150
-45
lines changed

5 files changed

+150
-45
lines changed

dotcom-rendering/src/components/ScrollableCarousel.tsx

Lines changed: 141 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,60 @@
1+
import type { SerializedStyles } from '@emotion/react';
12
import { css } from '@emotion/react';
3+
import type { Breakpoint } from '@guardian/source/foundations';
24
import { from, space, until } from '@guardian/source/foundations';
35
import { useEffect, useRef, useState } from 'react';
46
import { nestedOphanComponents } from '../lib/ophan-helpers';
57
import { palette } from '../palette';
68
import { CarouselNavigationButtons } from './CarouselNavigationButtons';
79

8-
type GapSize = 'small' | 'medium' | 'large';
10+
type GapSize = 'small' | 'medium' | 'large' | 'none';
911
type GapSizes = { row: GapSize; column: GapSize };
10-
11-
type Props = {
12-
children: React.ReactNode;
13-
carouselLength: number;
14-
visibleCarouselSlidesOnMobile: number;
15-
visibleCarouselSlidesOnTablet: number;
16-
sectionId?: string;
17-
shouldStackCards?: { desktop: boolean; mobile: boolean };
18-
gapSizes?: GapSizes;
12+
export type FixedSlideWidth = {
13+
defaultWidth: number;
14+
widthFromBreakpoints: { breakpoint: Breakpoint; width: number }[];
1915
};
2016

17+
export enum CarouselKind {
18+
'VisibleSlides',
19+
'FixedWidthSlides',
20+
}
21+
22+
type Props =
23+
| {
24+
kind: CarouselKind.VisibleSlides;
25+
children: React.ReactNode;
26+
isArticle?: boolean;
27+
carouselLength: number;
28+
visibleCarouselSlidesOnMobile: number;
29+
visibleCarouselSlidesOnTablet: number;
30+
fixedSlideWidth?: never;
31+
sectionId?: string;
32+
shouldStackCards?: { desktop: boolean; mobile: boolean };
33+
gapSizes?: GapSizes;
34+
}
35+
| {
36+
kind: CarouselKind.FixedWidthSlides;
37+
isArticle?: boolean;
38+
children: React.ReactNode;
39+
carouselLength: number;
40+
visibleCarouselSlidesOnMobile?: never;
41+
visibleCarouselSlidesOnTablet?: never;
42+
fixedSlideWidth: FixedSlideWidth;
43+
sectionId?: string;
44+
shouldStackCards?: never;
45+
gapSizes?: GapSizes;
46+
};
47+
2148
/**
2249
* Grid sizing values to calculate negative margin used to pull navigation
2350
* buttons outside of container into the outer grid column at wide breakpoint.
2451
*/
2552
const gridGap = 20;
2653
const gridGapMobile = 10;
2754

55+
const baseContainerStyles = css`
56+
position: relative;
57+
`;
2858
/**
2959
* On mobile the carousel extends into the outer margins to use the full width
3060
* of the screen. From tablet onwards the carousel sits within the page grid.
@@ -35,8 +65,7 @@ const gridGapMobile = 10;
3565
* left side so that the carousel extends into the the middle of the gutter
3666
* between the grid columns to meet the dividing line.
3767
*/
38-
const containerStyles = css`
39-
position: relative;
68+
const frontContainerStyles = css`
4069
margin-left: -${gridGapMobile}px;
4170
margin-right: -${gridGapMobile}px;
4271
${from.mobileLandscape} {
@@ -52,25 +81,7 @@ const containerStyles = css`
5281
}
5382
`;
5483

55-
const carouselStyles = css`
56-
display: grid;
57-
width: 100%;
58-
grid-auto-columns: 1fr;
59-
grid-auto-flow: column;
60-
overflow-x: auto;
61-
overflow-y: hidden;
62-
scroll-snap-type: x mandatory;
63-
scroll-behavior: smooth;
64-
overscroll-behavior: contain auto;
65-
/**
66-
* Hide scrollbars
67-
* See: https://stackoverflow.com/a/38994837
68-
*/
69-
::-webkit-scrollbar {
70-
display: none; /* Safari and Chrome */
71-
}
72-
scrollbar-width: none; /* Firefox */
73-
84+
const frontCarouselStyles = css`
7485
padding-left: ${gridGapMobile}px;
7586
padding-right: ${gridGapMobile}px;
7687
scroll-padding-left: ${gridGapMobile}px;
@@ -90,13 +101,44 @@ const carouselStyles = css`
90101
}
91102
`;
92103

104+
const baseCarouselStyles = css`
105+
display: grid;
106+
width: 100%;
107+
grid-auto-columns: 1fr;
108+
grid-auto-flow: column;
109+
overflow-x: auto;
110+
overflow-y: hidden;
111+
scroll-snap-type: x mandatory;
112+
scroll-behavior: smooth;
113+
overscroll-behavior: contain auto;
114+
/**
115+
* Hide scrollbars
116+
* See: https://stackoverflow.com/a/38994837
117+
*/
118+
::-webkit-scrollbar {
119+
display: none; /* Safari and Chrome */
120+
}
121+
scrollbar-width: none; /* Firefox */
122+
`;
123+
93124
const carouselGapStyles = (column: number, row: number) => {
94125
return css`
95126
column-gap: ${column}px;
96127
row-gap: ${row}px;
97128
`;
98129
};
99130

131+
const subgridStyles = ({ subgridRows }: { subgridRows: number }) => css`
132+
scroll-snap-align: start;
133+
position: relative;
134+
display: grid;
135+
@supports (grid-template-rows: subgrid) {
136+
grid-column: span 1;
137+
grid-row: span ${subgridRows};
138+
grid-template-rows: subgrid;
139+
}
140+
`;
141+
100142
const itemStyles = css`
101143
display: flex;
102144
scroll-snap-align: start;
@@ -151,6 +193,29 @@ const stackedCardRowsStyles = ({
151193
}
152194
`;
153195

196+
const generateFixedWidthColumStyles = ({
197+
fixedSlideWidth,
198+
carouselLength,
199+
}: {
200+
fixedSlideWidth: FixedSlideWidth;
201+
carouselLength: number;
202+
}) => {
203+
const fixedWidths: SerializedStyles[] = [];
204+
fixedWidths.push(css`
205+
grid-template-columns: repeat(
206+
${carouselLength},
207+
${fixedSlideWidth.defaultWidth}px
208+
);
209+
`);
210+
for (const { breakpoint, width } of fixedSlideWidth.widthFromBreakpoints) {
211+
fixedWidths.push(css`
212+
${from[breakpoint]} {
213+
grid-template-columns: repeat(${carouselLength}, ${width}px);
214+
}
215+
`);
216+
}
217+
return fixedWidths;
218+
};
154219
/**
155220
* Generates CSS styles for a grid layout used in a carousel.
156221
*
@@ -163,7 +228,7 @@ const generateCarouselColumnStyles = (
163228
totalCards: number,
164229
visibleCarouselSlidesOnMobile: number,
165230
visibleCarouselSlidesOnTablet: number,
166-
) => {
231+
): SerializedStyles => {
167232
const peepingCardWidth = space[8];
168233
const cardGap = 20;
169234
const offsetPeepingCardWidth =
@@ -220,17 +285,22 @@ const getGapSize = (gap: GapSize) => {
220285
return space[4];
221286
case 'large':
222287
return space[5];
288+
case 'none':
289+
return 0;
223290
}
224291
};
225292

226293
/**
227294
* A component used in the carousel fronts containers (e.g. small/medium/feature)
228295
*/
229296
export const ScrollableCarousel = ({
297+
kind,
230298
children,
299+
isArticle = false,
231300
carouselLength,
232301
visibleCarouselSlidesOnMobile,
233302
visibleCarouselSlidesOnTablet,
303+
fixedSlideWidth,
234304
sectionId,
235305
shouldStackCards = { desktop: false, mobile: false },
236306
gapSizes = { column: 'large', row: 'large' },
@@ -239,7 +309,10 @@ export const ScrollableCarousel = ({
239309
const [previousButtonEnabled, setPreviousButtonEnabled] = useState(false);
240310
const [nextButtonEnabled, setNextButtonEnabled] = useState(true);
241311

242-
const showNavigation = carouselLength > visibleCarouselSlidesOnTablet;
312+
const showNavigation =
313+
kind === CarouselKind.VisibleSlides
314+
? carouselLength > visibleCarouselSlidesOnTablet
315+
: false;
243316

244317
const scrollTo = (direction: 'left' | 'right') => {
245318
if (!carouselRef.current) return;
@@ -368,17 +441,25 @@ export const ScrollableCarousel = ({
368441
}, []);
369442

370443
return (
371-
<div css={containerStyles}>
444+
<div css={[baseContainerStyles, !isArticle && frontContainerStyles]}>
372445
<ol
373446
ref={carouselRef}
374447
css={[
375-
carouselStyles,
448+
baseCarouselStyles,
449+
!isArticle && frontCarouselStyles,
376450
carouselGapStyles(columnGap, rowGap),
377-
generateCarouselColumnStyles(
378-
carouselLength,
379-
visibleCarouselSlidesOnMobile,
380-
visibleCarouselSlidesOnTablet,
381-
),
451+
kind === CarouselKind.VisibleSlides &&
452+
generateCarouselColumnStyles(
453+
carouselLength,
454+
visibleCarouselSlidesOnMobile,
455+
visibleCarouselSlidesOnTablet,
456+
),
457+
...(kind === CarouselKind.FixedWidthSlides
458+
? generateFixedWidthColumStyles({
459+
carouselLength,
460+
fixedSlideWidth,
461+
})
462+
: []),
382463
stackedCardRowsStyles(shouldStackCards),
383464
]}
384465
data-heatphan-type="carousel"
@@ -428,3 +509,23 @@ ScrollableCarousel.Item = ({
428509
{children}
429510
</li>
430511
);
512+
513+
ScrollableCarousel.SubgridItem = ({
514+
subgridRows,
515+
children,
516+
borderColour = palette('--card-border-top'),
517+
}: {
518+
subgridRows: number;
519+
children: React.ReactNode;
520+
borderColour?: string;
521+
}) => (
522+
<li
523+
css={[
524+
itemStyles,
525+
subgridStyles({ subgridRows }),
526+
singleRowLeftBorderStyles(borderColour),
527+
]}
528+
>
529+
{children}
530+
</li>
531+
);

dotcom-rendering/src/components/ScrollableFeature.importable.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type {
55
DCRFrontCard,
66
} from '../types/front';
77
import { FeatureCard } from './FeatureCard';
8-
import { ScrollableCarousel } from './ScrollableCarousel';
8+
import { CarouselKind, ScrollableCarousel } from './ScrollableCarousel';
99

1010
type Props = {
1111
trails: DCRFrontCard[];
@@ -33,6 +33,7 @@ export const ScrollableFeature = ({
3333
}: Props) => {
3434
return (
3535
<ScrollableCarousel
36+
kind={CarouselKind.VisibleSlides}
3637
carouselLength={trails.length}
3738
visibleCarouselSlidesOnMobile={1}
3839
visibleCarouselSlidesOnTablet={3}

dotcom-rendering/src/components/ScrollableMedium.importable.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type {
55
DCRFrontCard,
66
} from '../types/front';
77
import { FrontCard } from './FrontCard';
8-
import { ScrollableCarousel } from './ScrollableCarousel';
8+
import { CarouselKind, ScrollableCarousel } from './ScrollableCarousel';
99

1010
type Props = {
1111
trails: DCRFrontCard[];
@@ -35,6 +35,7 @@ export const ScrollableMedium = ({
3535
}: Props) => {
3636
return (
3737
<ScrollableCarousel
38+
kind={CarouselKind.VisibleSlides}
3839
carouselLength={trails.length}
3940
visibleCarouselSlidesOnMobile={2}
4041
visibleCarouselSlidesOnTablet={4}

dotcom-rendering/src/components/ScrollableSmall.importable.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type {
44
DCRFrontCard,
55
} from '../types/front';
66
import { FrontCard } from './FrontCard';
7-
import { ScrollableCarousel } from './ScrollableCarousel';
7+
import { CarouselKind, ScrollableCarousel } from './ScrollableCarousel';
88

99
type Props = {
1010
trails: DCRFrontCard[];
@@ -63,6 +63,7 @@ export const ScrollableSmall = ({
6363

6464
return (
6565
<ScrollableCarousel
66+
kind={CarouselKind.VisibleSlides}
6667
carouselLength={Math.ceil(trails.length / 2)}
6768
visibleCarouselSlidesOnMobile={1}
6869
visibleCarouselSlidesOnTablet={2}

dotcom-rendering/src/components/ScrollableSmallOnwards.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ import type { ArticleFormat } from '../lib/articleFormat';
1313
import { palette } from '../palette';
1414
import { type OnwardsSource } from '../types/onwards';
1515
import { type TrailType } from '../types/trails';
16-
import { Card } from './Card/Card';
1716
import type { Props as CardProps } from './Card/Card';
18-
import { ScrollableCarousel } from './ScrollableCarousel';
17+
import { Card } from './Card/Card';
18+
import { CarouselKind, ScrollableCarousel } from './ScrollableCarousel';
1919

2020
type Props = {
2121
serverTime?: number;
@@ -94,6 +94,7 @@ export const ScrollableSmallOnwards = (props: Props) => {
9494
<Title title={props.heading} headingUrl={props.headingUrl} />
9595
<div css={cardsContainerStyles}>
9696
<ScrollableCarousel
97+
kind={CarouselKind.VisibleSlides}
9798
carouselLength={Math.ceil(trails.length / 2)}
9899
visibleCarouselSlidesOnMobile={1}
99100
visibleCarouselSlidesOnTablet={2}

0 commit comments

Comments
 (0)