1+ import type { SerializedStyles } from '@emotion/react' ;
12import { css } from '@emotion/react' ;
3+ import type { Breakpoint } from '@guardian/source/foundations' ;
24import { from , space , until } from '@guardian/source/foundations' ;
35import { useEffect , useRef , useState } from 'react' ;
46import { nestedOphanComponents } from '../lib/ophan-helpers' ;
57import { palette } from '../palette' ;
68import { CarouselNavigationButtons } from './CarouselNavigationButtons' ;
79
8- type GapSize = 'small' | 'medium' | 'large' ;
10+ type GapSize = 'small' | 'medium' | 'large' | 'none' ;
911type 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 */
2552const gridGap = 20 ;
2653const 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+
93124const 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+
100142const 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 */
229296export 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+ ) ;
0 commit comments