@@ -19,14 +19,16 @@ import throttle from '~utils/lodashButBetter/throttle';
19
19
import debounce from '~utils/lodashButBetter/debounce' ;
20
20
import { Box } from '~components/Box' ;
21
21
import BaseBox from '~components/Box/BaseBox' ;
22
- import { castWebType , makeMotionTime , useInterval , usePrevious } from '~utils' ;
22
+ import { castWebType , makeMotionTime , useInterval } from '~utils' ;
23
23
import { useId } from '~utils/useId' ;
24
24
import { makeAccessible } from '~utils/makeAccessible' ;
25
25
import { metaAttribute , MetaConstants } from '~utils/metaAttribute' ;
26
26
import { useVerifyAllowedChildren } from '~utils/useVerifyAllowedChildren/useVerifyAllowedChildren' ;
27
27
import { useTheme } from '~components/BladeProvider' ;
28
- import { useFirstRender } from '~utils/useFirstRender' ;
29
28
import { getStyledProps } from '~components/Box/styledProps' ;
29
+ import { useControllableState } from '~utils/useControllable' ;
30
+ import { useIsomorphicLayoutEffect } from '~utils/useIsomorphicLayoutEffect' ;
31
+ import { useDidUpdate } from '~utils/useDidUpdate' ;
30
32
31
33
type ControlsProp = Required <
32
34
Pick <
@@ -226,29 +228,6 @@ const CarouselBody = React.forwardRef<HTMLDivElement, CarouselBodyProps>(
226
228
} ,
227
229
) ;
228
230
229
- /**
230
- * A custom hook which syncs an effect with a state
231
- * While ignoring the first render & only running the effect when the state changes
232
- */
233
- function useSyncUpdateEffect < T > (
234
- effect : React . EffectCallback ,
235
- stateToSyncWith : T ,
236
- deps : React . DependencyList ,
237
- ) {
238
- const isFirst = useFirstRender ( ) ;
239
- const prevState = usePrevious < T > ( stateToSyncWith ) ;
240
-
241
- React . useEffect ( ( ) => {
242
- if ( ! isFirst ) {
243
- // if the state is the same as the previous state
244
- // we don't want to run the effect
245
- if ( prevState === stateToSyncWith ) return ;
246
- return effect ( ) ;
247
- }
248
- // eslint-disable-next-line react-hooks/exhaustive-deps
249
- } , [ stateToSyncWith , ...deps ] ) ;
250
- }
251
-
252
231
const Carousel = ( {
253
232
autoPlay,
254
233
visibleItems = 1 ,
@@ -264,16 +243,25 @@ const Carousel = ({
264
243
navigationButtonVariant = 'filled' ,
265
244
carouselItemAlignment = 'start' ,
266
245
height,
246
+ defaultActiveSlide,
247
+ activeSlide : activeSlideProp ,
267
248
...props
268
249
} : CarouselProps ) : React . ReactElement => {
269
250
const { platform } = useTheme ( ) ;
270
- const [ activeSlide , setActiveSlide ] = React . useState ( 0 ) ;
271
251
const [ activeIndicator , setActiveIndicator ] = React . useState ( 0 ) ;
252
+ const [ activeSlide , setActiveSlide ] = useControllableState ( {
253
+ defaultValue : defaultActiveSlide ?? 0 ,
254
+ value : activeSlideProp ,
255
+ onChange : ( value ) => {
256
+ onChange ?.( value ) ;
257
+ } ,
258
+ } ) ;
272
259
const [ shouldPauseAutoplay , setShouldPauseAutoplay ] = React . useState ( false ) ;
273
260
const [ startEndMargin , setStartEndMargin ] = React . useState ( 0 ) ;
274
261
const containerRef = React . useRef < HTMLDivElement > ( null ) ;
275
262
const isMobile = platform === 'onMobile' ;
276
- const id = useId ( 'carousel' ) ;
263
+ const id = useId ( ) ;
264
+ const carouselId = `carousel-${ id } ` ;
277
265
278
266
useVerifyAllowedChildren ( {
279
267
componentName : 'Carousel' ,
@@ -316,25 +304,25 @@ const Carousel = ({
316
304
317
305
// calculate the start/end margin so that we can
318
306
// deduct that margin when scrolling to a carousel item with goToSlideIndex
319
- React . useLayoutEffect ( ( ) => {
307
+ useIsomorphicLayoutEffect ( ( ) => {
320
308
// Do not calculate if not needed
321
309
if ( ! isResponsive && ! shouldAddStartEndSpacing ) return ;
322
310
if ( ! containerRef . current ) return ;
323
311
324
- const carouselItemId = getCarouselItemId ( id , 0 ) ;
312
+ const carouselItemId = getCarouselItemId ( carouselId , 0 ) ;
325
313
const carouselItem = containerRef . current . querySelector ( carouselItemId ) ;
326
314
if ( ! carouselItem ) return ;
327
315
328
316
const carouselItemLeft = carouselItem . getBoundingClientRect ( ) . left ?? 0 ;
329
317
const carouselContainerLeft = containerRef . current . getBoundingClientRect ( ) . left ?? 0 ;
330
318
331
319
setStartEndMargin ( carouselItemLeft - carouselContainerLeft ) ;
332
- } , [ id , isResponsive , shouldAddStartEndSpacing ] ) ;
320
+ } , [ carouselId , isResponsive , shouldAddStartEndSpacing ] ) ;
333
321
334
- const goToSlideIndex = ( slideIndex : number ) => {
322
+ const scrollToSlide = ( slideIndex : number , shouldAnimate = true ) => {
335
323
if ( ! containerRef . current ) return ;
336
324
337
- const carouselItemId = getCarouselItemId ( id , slideIndex * _visibleItems ) ;
325
+ const carouselItemId = getCarouselItemId ( carouselId , slideIndex * _visibleItems ) ;
338
326
const carouselItem = containerRef . current . querySelector ( carouselItemId ) ;
339
327
if ( ! carouselItem ) return ;
340
328
@@ -345,9 +333,12 @@ const Carousel = ({
345
333
346
334
containerRef . current . scroll ( {
347
335
left : left - startEndMargin ,
348
- behavior : 'smooth' ,
336
+ behavior : shouldAnimate ? 'smooth' : 'auto ',
349
337
} ) ;
350
- setActiveSlide ( slideIndex ) ;
338
+ } ;
339
+
340
+ const goToSlideIndex = ( slideIndex : number ) => {
341
+ setActiveSlide ( ( ) => slideIndex ) ;
351
342
setActiveIndicator ( slideIndex ) ;
352
343
} ;
353
344
@@ -431,15 +422,16 @@ const Carousel = ({
431
422
432
423
const slideIndex = Number ( carouselItem ?. getAttribute ( 'data-slide-index' ) ) ;
433
424
const goTo = Math . ceil ( slideIndex / _visibleItems ) ;
425
+ setActiveSlide ( ( ) => goTo ) ;
434
426
setActiveIndicator ( goTo ) ;
435
- setActiveSlide ( goTo ) ;
436
427
} , 50 ) ;
437
428
438
429
carouselContainer . addEventListener ( 'scroll' , handleScroll ) ;
439
430
440
431
return ( ) => {
441
432
carouselContainer ?. removeEventListener ( 'scroll' , handleScroll ) ;
442
433
} ;
434
+ // eslint-disable-next-line react-hooks/exhaustive-deps
443
435
} , [ _visibleItems , isMobile , isResponsive , shouldAddStartEndSpacing ] ) ;
444
436
445
437
// auto play
@@ -454,21 +446,33 @@ const Carousel = ({
454
446
} ,
455
447
) ;
456
448
449
+ // set initial active slide on mount
450
+ useIsomorphicLayoutEffect ( ( ) => {
451
+ if ( ! id ) return ;
452
+ goToSlideIndex ( activeSlide ) ;
453
+ scrollToSlide ( activeSlide , false ) ;
454
+ } , [ id ] ) ;
455
+
456
+ // Scroll the carousel to the active slide
457
+ useDidUpdate ( ( ) => {
458
+ scrollToSlide ( activeSlide ) ;
459
+ } , [ activeSlide ] ) ;
460
+
457
461
const carouselContext = React . useMemo < CarouselContextProps > ( ( ) => {
458
462
return {
459
463
isResponsive,
460
464
visibleItems : _visibleItems ,
461
465
carouselItemWidth,
462
466
carouselContainerRef : containerRef ,
463
467
setActiveIndicator,
464
- carouselId : id ,
468
+ carouselId,
465
469
totalNumberOfSlides,
466
470
activeSlide,
467
471
startEndMargin,
468
472
shouldAddStartEndSpacing,
469
473
} ;
470
474
} , [
471
- id ,
475
+ carouselId ,
472
476
startEndMargin ,
473
477
isResponsive ,
474
478
_visibleItems ,
@@ -478,14 +482,6 @@ const Carousel = ({
478
482
shouldAddStartEndSpacing ,
479
483
] ) ;
480
484
481
- useSyncUpdateEffect (
482
- ( ) => {
483
- onChange ?.( activeSlide ) ;
484
- } ,
485
- activeSlide ,
486
- [ onChange ] ,
487
- ) ;
488
-
489
485
return (
490
486
< CarouselContext . Provider value = { carouselContext } >
491
487
< BaseBox
@@ -546,7 +542,7 @@ const Carousel = ({
546
542
/>
547
543
) : null }
548
544
< CarouselBody
549
- idPrefix = { id }
545
+ idPrefix = { carouselId }
550
546
startEndMargin = { startEndMargin }
551
547
totalSlides = { totalNumberOfSlides }
552
548
shouldAddStartEndSpacing = { shouldAddStartEndSpacing }
0 commit comments