11import type { SerializedStyles } from '@emotion/react' ;
22import { css } from '@emotion/react' ;
33import type { Breakpoint } from '@guardian/source/foundations' ;
4- import { from , space } from '@guardian/source/foundations' ;
5- import { useEffect , useRef , useState } from 'react' ;
4+ import { from , space , textSans14 } from '@guardian/source/foundations' ;
5+ import { useCallback , useEffect , useMemo , useRef , useState } from 'react' ;
66import type { ArticleFormat } from '../lib/articleFormat' ;
77import { nestedOphanComponents } from '../lib/ophan-helpers' ;
88import { palette } from '../palette' ;
99import type { ProductBlockElement } from '../types/content' ;
10+ import { CarouselCount } from './CarouselCount' ;
1011import { CarouselNavigationButtons } from './CarouselNavigationButtons' ;
1112import { ProductCarouselCard } from './ProductCarouselCard' ;
1213import { Subheading } from './Subheading' ;
@@ -20,6 +21,15 @@ const carouselHeader = css`
2021 justify-content : space-between;
2122` ;
2223
24+ const countStyles = css `
25+ ${ textSans14 } ;
26+ color : ${ palette ( '--card-trail-text' ) } ;
27+ dis play: block;
28+ ${ from . tablet } {
29+ dis play: none;
30+ }
31+ ` ;
32+
2333export type FixedSlideWidth = {
2434 defaultWidth : number ;
2535 widthFromBreakpoints : { breakpoint : Breakpoint ; width : number } [ ] ;
@@ -110,6 +120,7 @@ export const ScrollableProduct = ({ products, format }: Props) => {
110120 const carouselRef = useRef < HTMLOListElement | null > ( null ) ;
111121 const [ previousButtonEnabled , setPreviousButtonEnabled ] = useState ( false ) ;
112122 const [ nextButtonEnabled , setNextButtonEnabled ] = useState ( true ) ;
123+ const [ cardCount , setCardCount ] = useState ( 1 ) ;
113124
114125 const carouselLength = products . length ;
115126 const fixedSlideWidth : FixedSlideWidth = {
@@ -157,27 +168,6 @@ export const ScrollableProduct = ({ products, format }: Props) => {
157168 } ;
158169 } ;
159170
160- /**
161- * Updates state of navigation buttons based on carousel's scroll position.
162- *
163- * This function checks the current scroll position of the carousel and sets
164- * the styles of the previous and next buttons accordingly. The button state
165- * is toggled when the midpoint of the first or last card has been scrolled
166- * in or out of view.
167- */
168- const updateButtonVisibilityOnScroll = ( ) => {
169- const carouselElement = carouselRef . current ;
170- if ( ! carouselElement ) return ;
171-
172- const scrollLeft = carouselElement . scrollLeft ;
173- const maxScrollLeft =
174- carouselElement . scrollWidth - carouselElement . clientWidth ;
175- const cardWidth = carouselElement . querySelector ( 'li' ) ?. offsetWidth ?? 0 ;
176-
177- setPreviousButtonEnabled ( scrollLeft > cardWidth / 2 ) ;
178- setNextButtonEnabled ( scrollLeft < maxScrollLeft - cardWidth / 2 ) ;
179- } ;
180-
181171 /**
182172 * Scrolls the carousel to a certain position when a card gains focus.
183173 *
@@ -232,22 +222,73 @@ export const ScrollableProduct = ({ products, format }: Props) => {
232222 }
233223 } ;
234224
235- useEffect ( ( ) => {
225+ /**
226+ * Update the count of the first card / how far scrolled the carousel is
227+ *
228+ * This function checks how far along the carousel is scrolled and then
229+ * updates the state of cardCount. we use the half of a card because at
230+ * this scroll amount the carousel will snap to that card.
231+ */
232+ const updateCardCountOnScroll = useCallback ( ( ) => {
236233 const carouselElement = carouselRef . current ;
237234 if ( ! carouselElement ) return ;
238235
239- carouselElement . addEventListener (
240- 'scroll' ,
241- throttleEvent ( updateButtonVisibilityOnScroll ) ,
236+ const cardWidth = carouselElement . querySelector ( 'li' ) ?. offsetWidth ?? 0 ;
237+ if ( ! cardWidth ) return ;
238+
239+ const count = Math . ceil (
240+ ( carouselElement . scrollLeft + cardWidth / 2 ) / cardWidth ,
242241 ) ;
243242
243+ setCardCount ( ( prev ) => ( prev === count ? prev : count ) ) ;
244+ } , [ ] ) ;
245+
246+ /**
247+ * Updates state of navigation buttons based on carousel's scroll position.
248+ *
249+ * This function checks the current scroll position of the carousel and sets
250+ * the styles of the previous and next buttons accordingly. The button state
251+ * is toggled when the midpoint of the first or last card has been scrolled
252+ * in or out of view.
253+ */
254+ const updateButtonVisibilityOnScroll = useCallback ( ( ) => {
255+ const carouselElement = carouselRef . current ;
256+ if ( ! carouselElement ) return ;
257+
258+ const scrollLeft = carouselElement . scrollLeft ;
259+ const maxScrollLeft =
260+ carouselElement . scrollWidth - carouselElement . clientWidth ;
261+ const cardWidth = carouselElement . querySelector ( 'li' ) ?. offsetWidth ?? 0 ;
262+
263+ setPreviousButtonEnabled ( scrollLeft > cardWidth / 2 ) ;
264+ setNextButtonEnabled ( scrollLeft < maxScrollLeft - cardWidth / 2 ) ;
265+ } , [ ] ) ;
266+
267+ const throttledCardCount = useMemo (
268+ ( ) => throttleEvent ( updateCardCountOnScroll ) ,
269+ [ updateCardCountOnScroll ] ,
270+ ) ;
271+
272+ const throttledButtonVisibility = useMemo (
273+ ( ) => throttleEvent ( updateButtonVisibilityOnScroll ) ,
274+ [ updateButtonVisibilityOnScroll ] ,
275+ ) ;
276+
277+ useEffect ( ( ) => {
278+ const carouselElement = carouselRef . current ;
279+ if ( ! carouselElement ) return ;
280+
281+ carouselElement . addEventListener ( 'scroll' , throttledCardCount ) ;
282+ carouselElement . addEventListener ( 'scroll' , throttledButtonVisibility ) ;
283+
244284 return ( ) => {
285+ carouselElement . removeEventListener ( 'scroll' , throttledCardCount ) ;
245286 carouselElement . removeEventListener (
246287 'scroll' ,
247- throttleEvent ( updateButtonVisibilityOnScroll ) ,
288+ throttledButtonVisibility ,
248289 ) ;
249290 } ;
250- } , [ ] ) ;
291+ } , [ throttledCardCount , throttledButtonVisibility ] ) ;
251292
252293 return (
253294 < >
@@ -263,6 +304,7 @@ export const ScrollableProduct = ({ products, format }: Props) => {
263304 css = { navigationStyles }
264305 id = { 'at-a-glance-carousel-navigation' }
265306 > </ div >
307+ < div css = { countStyles } id = { 'at-a-glance-carousel-count' } > </ div >
266308 </ div >
267309 < div css = { [ baseContainerStyles ] } >
268310 < ol
@@ -300,6 +342,11 @@ export const ScrollableProduct = ({ products, format }: Props) => {
300342 'next-button' ,
301343 ) }
302344 />
345+ < CarouselCount
346+ sectionId = { 'at-a-glance' }
347+ count = { cardCount }
348+ total = { carouselLength }
349+ />
303350 </ div >
304351 </ >
305352 ) ;
0 commit comments