From f42717354655a39c508830860a869eab5faa081f Mon Sep 17 00:00:00 2001 From: Mitch-At-Work Date: Mon, 18 Nov 2024 14:36:47 -0800 Subject: [PATCH 01/10] Update carousel autoplay --- ...-806c25e9-e23b-4c65-b072-007f894a236a.json | 7 + package.json | 6 +- .../react-carousel/library/package.json | 6 +- .../src/components/Carousel/Carousel.types.ts | 12 +- .../src/components/Carousel/useCarousel.ts | 2 + .../useCarouselAutoplayButton.tsx | 8 +- .../src/components/CarouselContext.types.ts | 7 + .../src/components/useEmblaCarousel.ts | 147 +++++++++++------- .../Carousel/CarouselControlled.stories.tsx | 100 ++++++------ .../src/Carousel/CarouselEventing.stories.tsx | 7 +- yarn.lock | 55 ++----- 11 files changed, 205 insertions(+), 152 deletions(-) create mode 100644 change/@fluentui-react-carousel-806c25e9-e23b-4c65-b072-007f894a236a.json diff --git a/change/@fluentui-react-carousel-806c25e9-e23b-4c65-b072-007f894a236a.json b/change/@fluentui-react-carousel-806c25e9-e23b-4c65-b072-007f894a236a.json new file mode 100644 index 00000000000000..d07af7f3e2ce69 --- /dev/null +++ b/change/@fluentui-react-carousel-806c25e9-e23b-4c65-b072-007f894a236a.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: Add autoplay index change callback and fix autoplay pause on interaction", + "packageName": "@fluentui/react-carousel", + "email": "mifraser@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/package.json b/package.json index db0ed7153a91eb..5448fa691d0b4d 100644 --- a/package.json +++ b/package.json @@ -218,9 +218,9 @@ "doctrine": "3.0.0", "dotparser": "1.1.1", "ejs": "3.1.10", - "embla-carousel": "8.3.0", - "embla-carousel-autoplay": "8.3.0", - "embla-carousel-fade": "8.3.0", + "embla-carousel": "8.4.0", + "embla-carousel-autoplay": "8.4.0", + "embla-carousel-fade": "8.4.0", "enquirer": "2.3.6", "enzyme": "3.10.0", "enzyme-to-json": "3.6.2", diff --git a/packages/react-components/react-carousel/library/package.json b/packages/react-components/react-carousel/library/package.json index 1b566c4500af1b..340142f22b7a8e 100644 --- a/packages/react-components/react-carousel/library/package.json +++ b/packages/react-components/react-carousel/library/package.json @@ -36,9 +36,9 @@ "@fluentui/react-utilities": "^9.18.17", "@griffel/react": "^1.5.22", "@swc/helpers": "^0.5.1", - "embla-carousel": "^8.3.0", - "embla-carousel-autoplay": "^8.3.0", - "embla-carousel-fade": "^8.3.0" + "embla-carousel": "^8.4.0", + "embla-carousel-autoplay": "^8.4.0", + "embla-carousel-fade": "^8.4.0" }, "peerDependencies": { "@types/react": ">=16.14.0 <19.0.0", diff --git a/packages/react-components/react-carousel/library/src/components/Carousel/Carousel.types.ts b/packages/react-components/react-carousel/library/src/components/Carousel/Carousel.types.ts index 13503bb2f9b95b..4cb2fd0fdc8e78 100644 --- a/packages/react-components/react-carousel/library/src/components/Carousel/Carousel.types.ts +++ b/packages/react-components/react-carousel/library/src/components/Carousel/Carousel.types.ts @@ -1,5 +1,9 @@ import type { ComponentProps, ComponentState, EventHandler, Slot } from '@fluentui/react-utilities'; -import type { CarouselContextValue, CarouselIndexChangeData } from '../CarouselContext.types'; +import type { + CarouselAutoplayIndexChangeData, + CarouselContextValue, + CarouselIndexChangeData, +} from '../CarouselContext.types'; export type CarouselSlots = { root: Slot<'div'>; @@ -39,6 +43,12 @@ export type CarouselProps = ComponentProps & { */ onActiveIndexChange?: EventHandler; + /** + * Callback to notify a page change. + */ + // eslint-disable-next-line @nx/workspace-consistent-callback-type -- EventHandler does not support "null" + onAutoplayIndexChange?: (ev: null, data: CarouselAutoplayIndexChangeData) => void; + /** * Circular enables the carousel to loop back around on navigation past trailing index. */ diff --git a/packages/react-components/react-carousel/library/src/components/Carousel/useCarousel.ts b/packages/react-components/react-carousel/library/src/components/Carousel/useCarousel.ts index 89e7228e1cf430..d71b5ff7d661d3 100644 --- a/packages/react-components/react-carousel/library/src/components/Carousel/useCarousel.ts +++ b/packages/react-components/react-carousel/library/src/components/Carousel/useCarousel.ts @@ -34,6 +34,7 @@ export function useCarousel_unstable(props: CarouselProps, ref: React.Ref { diff --git a/packages/react-components/react-carousel/library/src/components/CarouselAutoplayButton/useCarouselAutoplayButton.tsx b/packages/react-components/react-carousel/library/src/components/CarouselAutoplayButton/useCarouselAutoplayButton.tsx index ad3a8e838eeeb2..f6eed7b912a307 100644 --- a/packages/react-components/react-carousel/library/src/components/CarouselAutoplayButton/useCarouselAutoplayButton.tsx +++ b/packages/react-components/react-carousel/library/src/components/CarouselAutoplayButton/useCarouselAutoplayButton.tsx @@ -36,15 +36,13 @@ export const useCarouselAutoplayButton_unstable = ( const enableAutoplay = useCarouselContext(ctx => ctx.enableAutoplay); React.useEffect(() => { + // Update carousel autoplay based on button state + enableAutoplay(autoplay); + return () => { // We disable autoplay if the button gets unmounted. enableAutoplay(false); }; - }, [enableAutoplay]); - - useIsomorphicLayoutEffect(() => { - // Enable/disable autoplay on state change - enableAutoplay(autoplay); }, [autoplay, enableAutoplay]); const handleClick = (event: React.MouseEvent) => { diff --git a/packages/react-components/react-carousel/library/src/components/CarouselContext.types.ts b/packages/react-components/react-carousel/library/src/components/CarouselContext.types.ts index be6ee9911d55b9..1cb4bff33b32dc 100644 --- a/packages/react-components/react-carousel/library/src/components/CarouselContext.types.ts +++ b/packages/react-components/react-carousel/library/src/components/CarouselContext.types.ts @@ -14,6 +14,13 @@ export type CarouselIndexChangeData = ( index: number; }; +export type CarouselAutoplayIndexChangeData = EventData<'autoplay', null> & { + /** + * The index to be set after event has occurred. + */ + index: number; +}; + export type CarouselContextValue = { activeIndex: number; circular: boolean; diff --git a/packages/react-components/react-carousel/library/src/components/useEmblaCarousel.ts b/packages/react-components/react-carousel/library/src/components/useEmblaCarousel.ts index 1ad4203fc4b93a..3c82489d1ad1b7 100644 --- a/packages/react-components/react-carousel/library/src/components/useEmblaCarousel.ts +++ b/packages/react-components/react-carousel/library/src/components/useEmblaCarousel.ts @@ -1,10 +1,16 @@ -import { type EventHandler, useControllableState, useEventCallback } from '@fluentui/react-utilities'; +import { + type EventHandler, + useAnimationFrame, + useControllableState, + useEventCallback, + useTimeout, +} from '@fluentui/react-utilities'; import EmblaCarousel, { EmblaPluginType, type EmblaCarouselType, type EmblaOptionsType } from 'embla-carousel'; import * as React from 'react'; import { carouselCardClassNames } from './CarouselCard/useCarouselCardStyles.styles'; import { carouselSliderClassNames } from './CarouselSlider/useCarouselSliderStyles.styles'; -import { CarouselMotion, CarouselUpdateData, CarouselVisibilityEventDetail } from '../Carousel'; +import { CarouselMotion, CarouselProps, CarouselUpdateData, CarouselVisibilityEventDetail } from '../Carousel'; import Autoplay from 'embla-carousel-autoplay'; import Fade from 'embla-carousel-fade'; import { pointerEventPlugin } from './pointerEvents'; @@ -43,9 +49,20 @@ export function useEmblaCarousel( activeIndex: number | undefined; motion?: CarouselMotion; onDragIndexChange?: EventHandler; + onAutoplayIndexChange?: CarouselProps['onAutoplayIndexChange']; }, ) { - const { align, direction, loop, slidesToScroll, watchDrag, containScroll, motion, onDragIndexChange } = options; + const { + align, + direction, + loop, + slidesToScroll, + watchDrag, + containScroll, + motion, + onDragIndexChange, + onAutoplayIndexChange, + } = options; const [activeIndex, setActiveIndex] = useControllableState({ defaultState: options.defaultActiveIndex, state: options.activeIndex, @@ -67,52 +84,68 @@ export function useEmblaCarousel( }); const emblaApi = React.useRef(null); + /* We store the autoplay as both a ref and as state: + * State: Used to trigger a re-init on the carousel engine itself + * Ref: Used to prevent getPlugin dependencies from recreating embla carousel + */ + const [autoplay, setAutoplay] = React.useState(false); const autoplayRef = React.useRef(false); const resetAutoplay = React.useCallback(() => { - emblaApi.current?.plugins().autoplay.reset(); + emblaApi.current?.plugins().autoplay?.reset(); }, []); /* Our autoplay button, which is required by standards for autoplay to be enabled, will handle controlled state */ const enableAutoplay = React.useCallback( - (autoplay: boolean) => { - autoplayRef.current = autoplay; - if (autoplay) { - emblaApi.current?.plugins().autoplay.play(); + (_autoplay: boolean) => { + autoplayRef.current = _autoplay; + setAutoplay(_autoplay); + + if (_autoplay) { + emblaApi.current?.plugins().autoplay?.play(); // Reset after play to ensure timing and any focus/mouse pause state is reset. resetAutoplay(); } else { - emblaApi.current?.plugins().autoplay.stop(); + emblaApi.current?.plugins().autoplay?.stop(); } }, [resetAutoplay], ); - const getPlugins = React.useCallback(() => { - const plugins: EmblaPluginType[] = [ - Autoplay({ - playOnInit: autoplayRef.current, - stopOnInteraction: !autoplayRef.current, - stopOnMouseEnter: true, - stopOnFocusIn: true, - }), - ]; - - // Optionally add Fade plugin - if (motion === 'fade') { - plugins.push(Fade()); - } + const getPlugins = React.useCallback( + (initAutoplay: boolean) => { + const plugins: EmblaPluginType[] = []; + + if (initAutoplay) { + plugins.push( + Autoplay({ + playOnInit: true, + /* stopOnInteraction: false causes autoplay to restart on interaction end*/ + /* we must remove/re-add plugin on autoplay state change*/ + stopOnInteraction: false, + stopOnMouseEnter: true, + stopOnFocusIn: true, + }), + ); + } - if (watchDrag) { - plugins.push( - pointerEventPlugin({ - onSelectViaDrag: onDragEvent, - }), - ); - } + // Optionally add Fade plugin + if (motion === 'fade') { + plugins.push(Fade()); + } - return plugins; - }, [motion, onDragEvent, watchDrag]); + if (watchDrag) { + plugins.push( + pointerEventPlugin({ + onSelectViaDrag: onDragEvent, + }), + ); + } + + return plugins; + }, + [motion, onDragEvent, watchDrag], + ); // Listeners contains callbacks for UI elements that may require state update based on embla changes const listeners = React.useRef(new Set<(data: CarouselUpdateData) => void>()); @@ -142,22 +175,27 @@ export function useEmblaCarousel( } }); + const handleIndexChange = React.useCallback(() => { + const newIndex = emblaApi.current?.selectedScrollSnap() ?? 0; + const slides = emblaApi.current?.slideNodes(); + const actualIndex = emblaApi.current?.internalEngine().slideRegistry[newIndex][0] ?? 0; + + // We set the active or first index of group on-screen as the selected tabster index + slides?.forEach((slide, slideIndex) => { + setTabsterDefault(slide, slideIndex === actualIndex); + }); + setActiveIndex(newIndex); + }, [setActiveIndex]); + + const handleAutoplayIndexChange = useEventCallback(() => { + handleIndexChange(); + const newIndex = emblaApi.current?.selectedScrollSnap() ?? 0; + onAutoplayIndexChange?.(null, { event: null, type: 'autoplay', index: newIndex }); + }); + const viewportRef: React.RefObject = React.useRef(null); + const currentElementRef = React.useRef(); const containerRef: React.RefObject = React.useMemo(() => { - let currentElement: HTMLDivElement | null = null; - - const handleIndexChange = () => { - const newIndex = emblaApi.current?.selectedScrollSnap() ?? 0; - const slides = emblaApi.current?.slideNodes(); - const actualIndex = emblaApi.current?.internalEngine().slideRegistry[newIndex][0] ?? 0; - - // We set the active or first index of group on-screen as the selected tabster index - slides?.forEach((slide, slideIndex) => { - setTabsterDefault(slide, slideIndex === actualIndex); - }); - setActiveIndex(newIndex); - }; - const handleVisibilityChange = () => { const cardElements = emblaApi.current?.slideNodes(); const visibleIndexes = emblaApi.current?.slidesInView() ?? []; @@ -172,21 +210,23 @@ export function useEmblaCarousel( }); }; - const plugins = getPlugins(); + // Get plugins using autoplayRef to prevent state change recreating EmblaCarousel + const plugins = getPlugins(autoplayRef.current); return { set current(newElement: HTMLDivElement | null) { - if (currentElement) { + if (currentElementRef.current) { emblaApi.current?.off('slidesInView', handleVisibilityChange); emblaApi.current?.off('select', handleIndexChange); emblaApi.current?.off('reInit', handleReinit); + emblaApi.current?.off('autoplay:select', handleAutoplayIndexChange); emblaApi.current?.destroy(); } // Use direct viewport if available, else fallback to container (includes Carousel controls). const wrapperElement = viewportRef.current ?? newElement; + currentElementRef.current = wrapperElement; if (wrapperElement) { - currentElement = wrapperElement; emblaApi.current = EmblaCarousel( wrapperElement, { @@ -199,10 +239,11 @@ export function useEmblaCarousel( emblaApi.current?.on('reInit', handleReinit); emblaApi.current?.on('slidesInView', handleVisibilityChange); emblaApi.current?.on('select', handleIndexChange); + emblaApi.current?.on('autoplay:select', handleAutoplayIndexChange); } }, }; - }, [getPlugins, setActiveIndex, handleReinit]); + }, [getPlugins, handleAutoplayIndexChange, handleIndexChange, handleReinit]); const carouselApi = React.useMemo( () => ({ @@ -246,7 +287,8 @@ export function useEmblaCarousel( }, [activeIndex]); React.useEffect(() => { - const plugins = getPlugins(); + // Get plugins with autoplay state to trigger re-init when nessecary + const plugins = getPlugins(autoplay); emblaOptions.current = { startIndex: emblaOptions.current.startIndex, @@ -257,6 +299,7 @@ export function useEmblaCarousel( watchDrag, containScroll, }; + emblaApi.current?.reInit( { ...DEFAULT_EMBLA_OPTIONS, @@ -264,7 +307,7 @@ export function useEmblaCarousel( }, plugins, ); - }, [align, direction, loop, slidesToScroll, watchDrag, containScroll, getPlugins]); + }, [align, direction, loop, slidesToScroll, watchDrag, containScroll, getPlugins, autoplay]); return { activeIndex, diff --git a/packages/react-components/react-carousel/stories/src/Carousel/CarouselControlled.stories.tsx b/packages/react-components/react-carousel/stories/src/Carousel/CarouselControlled.stories.tsx index 8080d76da36066..642db687426b4a 100644 --- a/packages/react-components/react-carousel/stories/src/Carousel/CarouselControlled.stories.tsx +++ b/packages/react-components/react-carousel/stories/src/Carousel/CarouselControlled.stories.tsx @@ -9,6 +9,7 @@ import { Toolbar, ToolbarButton, CarouselSlider, + CarouselAutoplayButton, } from '@fluentui/react-components'; import { Carousel, @@ -40,6 +41,7 @@ const useClasses = makeStyles({ gap: '10px', alignSelf: 'center', + justifySelf: 'center', width: 'max-content', border: `${tokens.strokeWidthThin} solid ${tokens.colorNeutralStroke1}`, @@ -125,58 +127,62 @@ export const Controlled = () => {
setActiveIndex(data.index)} + onAutoplayIndexChange={(e, data) => setActiveIndex(data.index)} announcement={getAnnouncement} > - - - - - - - - - - - - - - - - - - - - - - - - - - - +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + {JSON.stringify({ activeIndex }, null, 2)} + + + {new Array(5).fill(null).map((_, index) => ( + setActiveIndex(index)} + > + {index} + + ))} + +
- -
- {JSON.stringify({ activeIndex }, null, 2)} - - - {new Array(5).fill(null).map((_, index) => ( - setActiveIndex(index)} - > - {index} - - ))} - -
); }; diff --git a/packages/react-components/react-carousel/stories/src/Carousel/CarouselEventing.stories.tsx b/packages/react-components/react-carousel/stories/src/Carousel/CarouselEventing.stories.tsx index 81a150bb0ba3c1..c08bd7b221ebc2 100644 --- a/packages/react-components/react-carousel/stories/src/Carousel/CarouselEventing.stories.tsx +++ b/packages/react-components/react-carousel/stories/src/Carousel/CarouselEventing.stories.tsx @@ -139,7 +139,7 @@ export const Eventing = () => { const [activeIndex, setActiveIndex] = React.useState(0); const [statusLog, setStatusLog] = React.useState< - [number, { type: 'click' | 'focus' | 'drag' | undefined; index: number }][] + [number, { type: 'click' | 'focus' | 'drag' | 'autoplay' | undefined; index: number }][] >([]); return ( @@ -155,6 +155,10 @@ export const Eventing = () => { setActiveIndex(data.index); setStatusLog(prev => [[Date.now(), { type: data.type, index: data.index }], ...prev]); }} + onAutoplayIndexChange={(ev, data) => { + setActiveIndex(data.index); + setStatusLog(prev => [[Date.now(), { type: data.type, index: data.index }], ...prev]); + }} > @@ -209,6 +213,7 @@ export const Eventing = () => { layout="inline" next={{ 'aria-label': 'go to next' }} prev={{ 'aria-label': 'go to prev' }} + autoplay={{ 'aria-label': 'Carousel autoplay' }} > {index => } diff --git a/yarn.lock b/yarn.lock index 63990c4e70db99..936c72fdee01c9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10267,20 +10267,20 @@ electron-to-chromium@^1.4.820: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.4.tgz#cd477c830dd6fca41fbd5465c1ff6ce08ac22343" integrity sha512-orzA81VqLyIGUEA77YkVA1D+N+nNfl2isJVjjmOyrlxuooZ19ynb+dOlaDTqd/idKRS9lDCSBmtzM+kyCsMnkA== -embla-carousel-autoplay@8.3.0, embla-carousel-autoplay@^8.3.0: - version "8.3.0" - resolved "https://registry.yarnpkg.com/embla-carousel-autoplay/-/embla-carousel-autoplay-8.3.0.tgz#2878e7c67c7c6f5c4cb0a06a8cb06e53d8f32f2f" - integrity sha512-h7DFJLf9uQD+XDxr1NwA3/oFIjsnj/iED2RjET5u6/svMec46IbF1CYPhmB5Q/1Fc0WkcvhPpsEsrtVXQLxNzA== +embla-carousel-autoplay@8.4.0, embla-carousel-autoplay@^8.4.0: + version "8.4.0" + resolved "https://registry.yarnpkg.com/embla-carousel-autoplay/-/embla-carousel-autoplay-8.4.0.tgz#7f92c113a67ee54eb616a90ef750fe20af4d865e" + integrity sha512-AJHXrnaY+Tf4tb/+oItmJSpz4P0WvS62GrW5Z4iFY3zsH0mkKcijzd04LIkj0P4DkTazIBEuXple+nUVmuMsrQ== -embla-carousel-fade@8.3.0, embla-carousel-fade@^8.3.0: - version "8.3.0" - resolved "https://registry.yarnpkg.com/embla-carousel-fade/-/embla-carousel-fade-8.3.0.tgz#44be8f2c00a771828bd02078fed26bce005d1f7a" - integrity sha512-m0NbkNPTAr6ghINhJrCnI0BRgWWoGRIGUd1tYCxTK00Exm9+kzOVL5KBPkrMVzXRXHe6TRgkmsCkb/7npfwRFQ== +embla-carousel-fade@8.4.0, embla-carousel-fade@^8.4.0: + version "8.4.0" + resolved "https://registry.yarnpkg.com/embla-carousel-fade/-/embla-carousel-fade-8.4.0.tgz#089c8aee4ddb06d0b7d5730c0a4fa543354387f9" + integrity sha512-d2/Pk/gHnlLCwE0MuwjLxLn22ngTf1rS17KT+TsYctVCApvDvxwgn5bDrwSpwg4BZhO4+xIrWw293rAHjCDewQ== -embla-carousel@8.3.0, embla-carousel@^8.3.0: - version "8.3.0" - resolved "https://registry.yarnpkg.com/embla-carousel/-/embla-carousel-8.3.0.tgz#dc27c63c405aa98320cb36893e4be2fbdc787ee1" - integrity sha512-Ve8dhI4w28qBqR8J+aMtv7rLK89r1ZA5HocwFz6uMB/i5EiC7bGI7y+AM80yAVUJw3qqaZYK7clmZMUR8kM3UA== +embla-carousel@8.4.0, embla-carousel@^8.4.0: + version "8.4.0" + resolved "https://registry.yarnpkg.com/embla-carousel/-/embla-carousel-8.4.0.tgz#18bb23d2815e12e5c9602f1f56658931eef68d82" + integrity sha512-sUzm4DGGsdZCom7LEO38Uu6C7oQoFfPorKDf/f7j2EeRCMhHSOt3CvF+pHCaI6N+x5Y8/tfLueJ0WZlgUREnew== emittery@^0.13.1: version "0.13.1" @@ -21515,7 +21515,7 @@ string-length@^5.0.1: char-regex "^2.0.0" strip-ansi "^7.0.1" -"string-width-cjs@npm:string-width@^4.2.0": +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -21550,15 +21550,6 @@ string-width@^3.0.0, string-width@^3.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -21659,7 +21650,7 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -21694,13 +21685,6 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1, strip-ansi@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -23945,7 +23929,7 @@ workspace-tools@^0.27.0: js-yaml "^4.1.0" micromatch "^4.0.0" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -23980,15 +23964,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From e90641381b0b9ec03f40dad93bdddf5be6d3270a Mon Sep 17 00:00:00 2001 From: Mitch-At-Work Date: Mon, 18 Nov 2024 14:58:14 -0800 Subject: [PATCH 02/10] Lint and gen api --- .../react-carousel/library/etc/react-carousel.api.md | 1 + .../CarouselAutoplayButton/useCarouselAutoplayButton.tsx | 8 +------- .../library/src/components/useEmblaCarousel.ts | 8 +------- 3 files changed, 3 insertions(+), 14 deletions(-) diff --git a/packages/react-components/react-carousel/library/etc/react-carousel.api.md b/packages/react-components/react-carousel/library/etc/react-carousel.api.md index 981198ec2ee4f9..1ad158a8fdb28e 100644 --- a/packages/react-components/react-carousel/library/etc/react-carousel.api.md +++ b/packages/react-components/react-carousel/library/etc/react-carousel.api.md @@ -209,6 +209,7 @@ export type CarouselProps = ComponentProps & { align?: 'center' | 'start' | 'end'; activeIndex?: number; onActiveIndexChange?: EventHandler; + onAutoplayIndexChange?: (ev: null, data: CarouselAutoplayIndexChangeData) => void; circular?: boolean; groupSize?: number | 'auto'; draggable?: boolean; diff --git a/packages/react-components/react-carousel/library/src/components/CarouselAutoplayButton/useCarouselAutoplayButton.tsx b/packages/react-components/react-carousel/library/src/components/CarouselAutoplayButton/useCarouselAutoplayButton.tsx index f6eed7b912a307..5d64b061dcfd6a 100644 --- a/packages/react-components/react-carousel/library/src/components/CarouselAutoplayButton/useCarouselAutoplayButton.tsx +++ b/packages/react-components/react-carousel/library/src/components/CarouselAutoplayButton/useCarouselAutoplayButton.tsx @@ -1,13 +1,7 @@ import type { ARIAButtonElement } from '@fluentui/react-aria'; import { useToggleButton_unstable } from '@fluentui/react-button'; import { PlayCircleRegular, PauseCircleRegular } from '@fluentui/react-icons'; -import { - mergeCallbacks, - slot, - useControllableState, - useEventCallback, - useIsomorphicLayoutEffect, -} from '@fluentui/react-utilities'; +import { mergeCallbacks, slot, useControllableState, useEventCallback } from '@fluentui/react-utilities'; import * as React from 'react'; import type { CarouselAutoplayButtonProps, CarouselAutoplayButtonState } from './CarouselAutoplayButton.types'; diff --git a/packages/react-components/react-carousel/library/src/components/useEmblaCarousel.ts b/packages/react-components/react-carousel/library/src/components/useEmblaCarousel.ts index 3c82489d1ad1b7..f47d4a14643b8e 100644 --- a/packages/react-components/react-carousel/library/src/components/useEmblaCarousel.ts +++ b/packages/react-components/react-carousel/library/src/components/useEmblaCarousel.ts @@ -1,10 +1,4 @@ -import { - type EventHandler, - useAnimationFrame, - useControllableState, - useEventCallback, - useTimeout, -} from '@fluentui/react-utilities'; +import { type EventHandler, useControllableState, useEventCallback } from '@fluentui/react-utilities'; import EmblaCarousel, { EmblaPluginType, type EmblaCarouselType, type EmblaOptionsType } from 'embla-carousel'; import * as React from 'react'; From 6dc4ba02f62b5e6a319ae256a0f8d1204f64c9dc Mon Sep 17 00:00:00 2001 From: Mitch-At-Work Date: Thu, 21 Nov 2024 16:06:43 -0800 Subject: [PATCH 03/10] Only use one autoplay var --- .../src/components/useEmblaCarousel.ts | 131 +++++++++--------- 1 file changed, 63 insertions(+), 68 deletions(-) diff --git a/packages/react-components/react-carousel/library/src/components/useEmblaCarousel.ts b/packages/react-components/react-carousel/library/src/components/useEmblaCarousel.ts index f47d4a14643b8e..2daaf74aad3584 100644 --- a/packages/react-components/react-carousel/library/src/components/useEmblaCarousel.ts +++ b/packages/react-components/react-carousel/library/src/components/useEmblaCarousel.ts @@ -78,22 +78,75 @@ export function useEmblaCarousel( }); const emblaApi = React.useRef(null); - /* We store the autoplay as both a ref and as state: - * State: Used to trigger a re-init on the carousel engine itself - * Ref: Used to prevent getPlugin dependencies from recreating embla carousel - */ - const [autoplay, setAutoplay] = React.useState(false); - const autoplayRef = React.useRef(false); + const autoplay = React.useRef(false); const resetAutoplay = React.useCallback(() => { emblaApi.current?.plugins().autoplay?.reset(); }, []); + const getPlugins = React.useCallback(() => { + const plugins: EmblaPluginType[] = []; + + if (autoplay.current) { + plugins.push( + Autoplay({ + playOnInit: true, + /* stopOnInteraction: false causes autoplay to restart on interaction end*/ + /* we must remove/re-add plugin on autoplay state change*/ + stopOnInteraction: false, + stopOnMouseEnter: true, + stopOnFocusIn: true, + }), + ); + } + + // Optionally add Fade plugin + if (motion === 'fade') { + plugins.push(Fade()); + } + + if (watchDrag) { + plugins.push( + pointerEventPlugin({ + onSelectViaDrag: onDragEvent, + }), + ); + } + + return plugins; + }, [motion, onDragEvent, watchDrag]); + + const reinitializeCarousel = React.useCallback(() => { + const plugins = getPlugins(); + + emblaOptions.current = { + startIndex: emblaOptions.current.startIndex, + align, + direction, + loop, + slidesToScroll, + watchDrag, + containScroll, + }; + + emblaApi.current?.reInit( + { + ...DEFAULT_EMBLA_OPTIONS, + ...emblaOptions.current, + }, + plugins, + ); + }, [align, containScroll, direction, getPlugins, loop, slidesToScroll, watchDrag]); + + React.useEffect(() => { + reinitializeCarousel(); + }, [reinitializeCarousel]); + /* Our autoplay button, which is required by standards for autoplay to be enabled, will handle controlled state */ const enableAutoplay = React.useCallback( (_autoplay: boolean) => { - autoplayRef.current = _autoplay; - setAutoplay(_autoplay); + autoplay.current = _autoplay; + reinitializeCarousel(); if (_autoplay) { emblaApi.current?.plugins().autoplay?.play(); @@ -103,42 +156,7 @@ export function useEmblaCarousel( emblaApi.current?.plugins().autoplay?.stop(); } }, - [resetAutoplay], - ); - - const getPlugins = React.useCallback( - (initAutoplay: boolean) => { - const plugins: EmblaPluginType[] = []; - - if (initAutoplay) { - plugins.push( - Autoplay({ - playOnInit: true, - /* stopOnInteraction: false causes autoplay to restart on interaction end*/ - /* we must remove/re-add plugin on autoplay state change*/ - stopOnInteraction: false, - stopOnMouseEnter: true, - stopOnFocusIn: true, - }), - ); - } - - // Optionally add Fade plugin - if (motion === 'fade') { - plugins.push(Fade()); - } - - if (watchDrag) { - plugins.push( - pointerEventPlugin({ - onSelectViaDrag: onDragEvent, - }), - ); - } - - return plugins; - }, - [motion, onDragEvent, watchDrag], + [reinitializeCarousel, resetAutoplay], ); // Listeners contains callbacks for UI elements that may require state update based on embla changes @@ -205,7 +223,7 @@ export function useEmblaCarousel( }; // Get plugins using autoplayRef to prevent state change recreating EmblaCarousel - const plugins = getPlugins(autoplayRef.current); + const plugins = getPlugins(); return { set current(newElement: HTMLDivElement | null) { @@ -280,29 +298,6 @@ export function useEmblaCarousel( } }, [activeIndex]); - React.useEffect(() => { - // Get plugins with autoplay state to trigger re-init when nessecary - const plugins = getPlugins(autoplay); - - emblaOptions.current = { - startIndex: emblaOptions.current.startIndex, - align, - direction, - loop, - slidesToScroll, - watchDrag, - containScroll, - }; - - emblaApi.current?.reInit( - { - ...DEFAULT_EMBLA_OPTIONS, - ...emblaOptions.current, - }, - plugins, - ); - }, [align, direction, loop, slidesToScroll, watchDrag, containScroll, getPlugins, autoplay]); - return { activeIndex, carouselApi, From 6a68477c6ad88b1d5cfafd8411cc04eaf4a25405 Mon Sep 17 00:00:00 2001 From: Mitch-At-Work Date: Fri, 22 Nov 2024 10:46:03 -0800 Subject: [PATCH 04/10] Fix autoplay to work with Tabster --- package.json | 6 +-- .../library/etc/react-carousel.api.md | 1 + .../src/components/Carousel/useCarousel.ts | 37 ++++++++++++------- .../Carousel/useCarouselContextValues.ts | 3 ++ .../useCarouselAutoplayButton.tsx | 12 +++--- .../library/src/components/CarouselContext.ts | 3 ++ .../src/components/CarouselContext.types.ts | 1 + .../CarouselViewport/useCarouselViewport.ts | 26 ++++++++++++- .../src/components/useEmblaCarousel.ts | 24 ++++++++---- .../Carousel/CarouselActionCards.stories.tsx | 1 + yarn.lock | 21 +++++++++-- 11 files changed, 101 insertions(+), 34 deletions(-) diff --git a/package.json b/package.json index 5448fa691d0b4d..416ab89efa43d2 100644 --- a/package.json +++ b/package.json @@ -218,9 +218,9 @@ "doctrine": "3.0.0", "dotparser": "1.1.1", "ejs": "3.1.10", - "embla-carousel": "8.4.0", - "embla-carousel-autoplay": "8.4.0", - "embla-carousel-fade": "8.4.0", + "embla-carousel": "8.5.1", + "embla-carousel-autoplay": "8.5.1", + "embla-carousel-fade": "8.5.1", "enquirer": "2.3.6", "enzyme": "3.10.0", "enzyme-to-json": "3.6.2", diff --git a/packages/react-components/react-carousel/library/etc/react-carousel.api.md b/packages/react-components/react-carousel/library/etc/react-carousel.api.md index 1ad158a8fdb28e..37811801e3ead6 100644 --- a/packages/react-components/react-carousel/library/etc/react-carousel.api.md +++ b/packages/react-components/react-carousel/library/etc/react-carousel.api.md @@ -104,6 +104,7 @@ export type CarouselContextValue = { selectPageByIndex: (event: React_2.MouseEvent, value: number, jump?: boolean) => void; subscribeForValues: (listener: (data: CarouselUpdateData) => void) => () => void; enableAutoplay: (autoplay: boolean) => void; + initAutoplay: (autoplay: boolean) => void; resetAutoplay: () => void; containerRef?: React_2.RefObject; viewportRef?: React_2.RefObject; diff --git a/packages/react-components/react-carousel/library/src/components/Carousel/useCarousel.ts b/packages/react-components/react-carousel/library/src/components/Carousel/useCarousel.ts index d71b5ff7d661d3..50a9c9a47ba5e9 100644 --- a/packages/react-components/react-carousel/library/src/components/Carousel/useCarousel.ts +++ b/packages/react-components/react-carousel/library/src/components/Carousel/useCarousel.ts @@ -38,20 +38,28 @@ export function useCarousel_unstable(props: CarouselProps, ref: React.Ref { const foundIndex = carouselApi.scrollToElement(element, jump); @@ -137,5 +145,6 @@ export function useCarousel_unstable(props: CarouselProps, ref: React.Ref ctx.enableAutoplay); + const initAutoplay = useCarouselContext(ctx => ctx.initAutoplay); React.useEffect(() => { - // Update carousel autoplay based on button state - enableAutoplay(autoplay); + // Initialize carousel autoplay based on button state + initAutoplay(autoplay); return () => { - // We disable autoplay if the button gets unmounted. - enableAutoplay(false); + // We uninitialize autoplay if the button gets unmounted. + initAutoplay(false); }; - }, [autoplay, enableAutoplay]); + }, [autoplay, initAutoplay]); const handleClick = (event: React.MouseEvent) => { if (event.isDefaultPrevented()) { diff --git a/packages/react-components/react-carousel/library/src/components/CarouselContext.ts b/packages/react-components/react-carousel/library/src/components/CarouselContext.ts index 1dd1a90bf1f0db..2120dd736c9cd6 100644 --- a/packages/react-components/react-carousel/library/src/components/CarouselContext.ts +++ b/packages/react-components/react-carousel/library/src/components/CarouselContext.ts @@ -18,6 +18,9 @@ export const carouselContextDefaultValue: CarouselContextValue = { enableAutoplay: () => { /** noop */ }, + initAutoplay: () => { + /** noop */ + }, resetAutoplay: () => { /** noop */ }, diff --git a/packages/react-components/react-carousel/library/src/components/CarouselContext.types.ts b/packages/react-components/react-carousel/library/src/components/CarouselContext.types.ts index 1cb4bff33b32dc..734f37a1f6a959 100644 --- a/packages/react-components/react-carousel/library/src/components/CarouselContext.types.ts +++ b/packages/react-components/react-carousel/library/src/components/CarouselContext.types.ts @@ -36,6 +36,7 @@ export type CarouselContextValue = { ) => void; subscribeForValues: (listener: (data: CarouselUpdateData) => void) => () => void; enableAutoplay: (autoplay: boolean) => void; + initAutoplay: (autoplay: boolean) => void; resetAutoplay: () => void; // Container with controls passed to carousel engine containerRef?: React.RefObject; diff --git a/packages/react-components/react-carousel/library/src/components/CarouselViewport/useCarouselViewport.ts b/packages/react-components/react-carousel/library/src/components/CarouselViewport/useCarouselViewport.ts index b04c25996074d0..d2f3eb30c2aaa3 100644 --- a/packages/react-components/react-carousel/library/src/components/CarouselViewport/useCarouselViewport.ts +++ b/packages/react-components/react-carousel/library/src/components/CarouselViewport/useCarouselViewport.ts @@ -1,5 +1,5 @@ import * as React from 'react'; -import { getIntrinsicElementProps, slot, useMergedRefs } from '@fluentui/react-utilities'; +import { getIntrinsicElementProps, mergeCallbacks, slot, useMergedRefs } from '@fluentui/react-utilities'; import type { CarouselViewportProps, CarouselViewportState } from './CarouselViewport.types'; import { useCarouselContext_unstable as useCarouselContext } from '../CarouselContext'; @@ -17,6 +17,28 @@ export const useCarouselViewport_unstable = ( ref: React.Ref, ): CarouselViewportState => { const viewportRef = useCarouselContext(ctx => ctx.viewportRef); + const enableAutoplay = useCarouselContext(ctx => ctx.enableAutoplay); + + const handleFocusCapture = React.useCallback( + (e: React.FocusEvent) => { + // Will pause autoplay when focus is captured within viewport (if autoplay is initialized) + enableAutoplay(false); + }, + [enableAutoplay], + ); + + const handleBlurCapture = React.useCallback( + (e: React.FocusEvent) => { + // Will enable autoplay (if initialized) when focus exits viewport + if (!e.currentTarget.contains(e.relatedTarget)) { + enableAutoplay(true); + } + }, + [enableAutoplay], + ); + + const onFocusCapture = mergeCallbacks(props.onFocusCapture, handleFocusCapture); + const onBlurCapture = mergeCallbacks(props.onBlurCapture, handleBlurCapture); return { components: { @@ -29,6 +51,8 @@ export const useCarouselViewport_unstable = ( // Draggable ensures dragging is supported (even if not enabled) draggable: true, ...props, + onFocusCapture, + onBlurCapture, }), { elementType: 'div' }, ), diff --git a/packages/react-components/react-carousel/library/src/components/useEmblaCarousel.ts b/packages/react-components/react-carousel/library/src/components/useEmblaCarousel.ts index 2daaf74aad3584..d15b9c0bdf3a33 100644 --- a/packages/react-components/react-carousel/library/src/components/useEmblaCarousel.ts +++ b/packages/react-components/react-carousel/library/src/components/useEmblaCarousel.ts @@ -90,12 +90,11 @@ export function useEmblaCarousel( if (autoplay.current) { plugins.push( Autoplay({ - playOnInit: true, /* stopOnInteraction: false causes autoplay to restart on interaction end*/ /* we must remove/re-add plugin on autoplay state change*/ stopOnInteraction: false, stopOnMouseEnter: true, - stopOnFocusIn: true, + stopOnFocusIn: false, // We'll handle this one manually to prevent conflicts with tabster }), ); } @@ -142,12 +141,11 @@ export function useEmblaCarousel( reinitializeCarousel(); }, [reinitializeCarousel]); - /* Our autoplay button, which is required by standards for autoplay to be enabled, will handle controlled state */ + /* This function enables autoplay to pause/play without affecting underlying state + * Useful for pausing on focus etc. without having to reinitialize or set autoplay to off + */ const enableAutoplay = React.useCallback( (_autoplay: boolean) => { - autoplay.current = _autoplay; - reinitializeCarousel(); - if (_autoplay) { emblaApi.current?.plugins().autoplay?.play(); // Reset after play to ensure timing and any focus/mouse pause state is reset. @@ -156,7 +154,18 @@ export function useEmblaCarousel( emblaApi.current?.plugins().autoplay?.stop(); } }, - [reinitializeCarousel, resetAutoplay], + [resetAutoplay], + ); + + /* Our autoplay button, which is required by standards for autoplay to be enabled, will handle controlled state */ + const initAutoplay = React.useCallback( + (_autoplay: boolean) => { + autoplay.current = _autoplay; + reinitializeCarousel(); + + enableAutoplay(_autoplay); + }, + [enableAutoplay, reinitializeCarousel], ); // Listeners contains callbacks for UI elements that may require state update based on embla changes @@ -306,5 +315,6 @@ export function useEmblaCarousel( subscribeForValues, enableAutoplay, resetAutoplay, + initAutoplay, }; } diff --git a/packages/react-components/react-carousel/stories/src/Carousel/CarouselActionCards.stories.tsx b/packages/react-components/react-carousel/stories/src/Carousel/CarouselActionCards.stories.tsx index 7c97d73aec0994..ce03868f6bb8dc 100644 --- a/packages/react-components/react-carousel/stories/src/Carousel/CarouselActionCards.stories.tsx +++ b/packages/react-components/react-carousel/stories/src/Carousel/CarouselActionCards.stories.tsx @@ -211,6 +211,7 @@ export const AlignmentAndWhitespace = () => { layout="inline" next={{ 'aria-label': 'go to next' }} prev={{ 'aria-label': 'go to prev' }} + autoplay={{}} > {index => } diff --git a/yarn.lock b/yarn.lock index 936c72fdee01c9..faafe4757ac20d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10267,17 +10267,32 @@ electron-to-chromium@^1.4.820: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.4.tgz#cd477c830dd6fca41fbd5465c1ff6ce08ac22343" integrity sha512-orzA81VqLyIGUEA77YkVA1D+N+nNfl2isJVjjmOyrlxuooZ19ynb+dOlaDTqd/idKRS9lDCSBmtzM+kyCsMnkA== -embla-carousel-autoplay@8.4.0, embla-carousel-autoplay@^8.4.0: +embla-carousel-autoplay@8.5.1: + version "8.5.1" + resolved "https://registry.yarnpkg.com/embla-carousel-autoplay/-/embla-carousel-autoplay-8.5.1.tgz#d0213ab6d7beeafcfcb8f7b1fa023a4d3882f0a2" + integrity sha512-FnZklFpePfp8wbj177UwVaGFehgs+ASVcJvYLWTtHuYKURynCc3IdDn2qrn0E5Qpa3g9yeGwCS4p8QkrZmO8xg== + +embla-carousel-autoplay@^8.4.0: version "8.4.0" resolved "https://registry.yarnpkg.com/embla-carousel-autoplay/-/embla-carousel-autoplay-8.4.0.tgz#7f92c113a67ee54eb616a90ef750fe20af4d865e" integrity sha512-AJHXrnaY+Tf4tb/+oItmJSpz4P0WvS62GrW5Z4iFY3zsH0mkKcijzd04LIkj0P4DkTazIBEuXple+nUVmuMsrQ== -embla-carousel-fade@8.4.0, embla-carousel-fade@^8.4.0: +embla-carousel-fade@8.5.1: + version "8.5.1" + resolved "https://registry.yarnpkg.com/embla-carousel-fade/-/embla-carousel-fade-8.5.1.tgz#216b27198ee7ed71b27c545d231da76697e34185" + integrity sha512-n7vRe2tsTW0vc0Xxtk3APoxhUSXIGh/lGRKYtBJS/SWDeXf9E3qVUst4MfHhwXaHlfu5PLqG3xIEDAr2gwbbNA== + +embla-carousel-fade@^8.4.0: version "8.4.0" resolved "https://registry.yarnpkg.com/embla-carousel-fade/-/embla-carousel-fade-8.4.0.tgz#089c8aee4ddb06d0b7d5730c0a4fa543354387f9" integrity sha512-d2/Pk/gHnlLCwE0MuwjLxLn22ngTf1rS17KT+TsYctVCApvDvxwgn5bDrwSpwg4BZhO4+xIrWw293rAHjCDewQ== -embla-carousel@8.4.0, embla-carousel@^8.4.0: +embla-carousel@8.5.1: + version "8.5.1" + resolved "https://registry.yarnpkg.com/embla-carousel/-/embla-carousel-8.5.1.tgz#8d83217e831666f6df573b0d3727ff0ae9208002" + integrity sha512-JUb5+FOHobSiWQ2EJNaueCNT/cQU9L6XWBbWmorWPQT9bkbk+fhsuLr8wWrzXKagO3oWszBO7MSx+GfaRk4E6A== + +embla-carousel@^8.4.0: version "8.4.0" resolved "https://registry.yarnpkg.com/embla-carousel/-/embla-carousel-8.4.0.tgz#18bb23d2815e12e5c9602f1f56658931eef68d82" integrity sha512-sUzm4DGGsdZCom7LEO38Uu6C7oQoFfPorKDf/f7j2EeRCMhHSOt3CvF+pHCaI6N+x5Y8/tfLueJ0WZlgUREnew== From 90842cbe21755d50ba82d9eafabd60b1e2065626 Mon Sep 17 00:00:00 2001 From: Mitch-At-Work Date: Fri, 22 Nov 2024 11:18:09 -0800 Subject: [PATCH 05/10] Update deps --- .../react-carousel/library/package.json | 6 +++--- yarn.lock | 21 +++---------------- 2 files changed, 6 insertions(+), 21 deletions(-) diff --git a/packages/react-components/react-carousel/library/package.json b/packages/react-components/react-carousel/library/package.json index 340142f22b7a8e..34abc44ff59dae 100644 --- a/packages/react-components/react-carousel/library/package.json +++ b/packages/react-components/react-carousel/library/package.json @@ -36,9 +36,9 @@ "@fluentui/react-utilities": "^9.18.17", "@griffel/react": "^1.5.22", "@swc/helpers": "^0.5.1", - "embla-carousel": "^8.4.0", - "embla-carousel-autoplay": "^8.4.0", - "embla-carousel-fade": "^8.4.0" + "embla-carousel": "^8.5.1", + "embla-carousel-autoplay": "^8.5.1", + "embla-carousel-fade": "^8.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <19.0.0", diff --git a/yarn.lock b/yarn.lock index faafe4757ac20d..2e29da842db8d5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10267,36 +10267,21 @@ electron-to-chromium@^1.4.820: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.4.tgz#cd477c830dd6fca41fbd5465c1ff6ce08ac22343" integrity sha512-orzA81VqLyIGUEA77YkVA1D+N+nNfl2isJVjjmOyrlxuooZ19ynb+dOlaDTqd/idKRS9lDCSBmtzM+kyCsMnkA== -embla-carousel-autoplay@8.5.1: +embla-carousel-autoplay@8.5.1, embla-carousel-autoplay@^8.5.1: version "8.5.1" resolved "https://registry.yarnpkg.com/embla-carousel-autoplay/-/embla-carousel-autoplay-8.5.1.tgz#d0213ab6d7beeafcfcb8f7b1fa023a4d3882f0a2" integrity sha512-FnZklFpePfp8wbj177UwVaGFehgs+ASVcJvYLWTtHuYKURynCc3IdDn2qrn0E5Qpa3g9yeGwCS4p8QkrZmO8xg== -embla-carousel-autoplay@^8.4.0: - version "8.4.0" - resolved "https://registry.yarnpkg.com/embla-carousel-autoplay/-/embla-carousel-autoplay-8.4.0.tgz#7f92c113a67ee54eb616a90ef750fe20af4d865e" - integrity sha512-AJHXrnaY+Tf4tb/+oItmJSpz4P0WvS62GrW5Z4iFY3zsH0mkKcijzd04LIkj0P4DkTazIBEuXple+nUVmuMsrQ== - -embla-carousel-fade@8.5.1: +embla-carousel-fade@8.5.1, embla-carousel-fade@^8.5.1: version "8.5.1" resolved "https://registry.yarnpkg.com/embla-carousel-fade/-/embla-carousel-fade-8.5.1.tgz#216b27198ee7ed71b27c545d231da76697e34185" integrity sha512-n7vRe2tsTW0vc0Xxtk3APoxhUSXIGh/lGRKYtBJS/SWDeXf9E3qVUst4MfHhwXaHlfu5PLqG3xIEDAr2gwbbNA== -embla-carousel-fade@^8.4.0: - version "8.4.0" - resolved "https://registry.yarnpkg.com/embla-carousel-fade/-/embla-carousel-fade-8.4.0.tgz#089c8aee4ddb06d0b7d5730c0a4fa543354387f9" - integrity sha512-d2/Pk/gHnlLCwE0MuwjLxLn22ngTf1rS17KT+TsYctVCApvDvxwgn5bDrwSpwg4BZhO4+xIrWw293rAHjCDewQ== - -embla-carousel@8.5.1: +embla-carousel@8.5.1, embla-carousel@^8.5.1: version "8.5.1" resolved "https://registry.yarnpkg.com/embla-carousel/-/embla-carousel-8.5.1.tgz#8d83217e831666f6df573b0d3727ff0ae9208002" integrity sha512-JUb5+FOHobSiWQ2EJNaueCNT/cQU9L6XWBbWmorWPQT9bkbk+fhsuLr8wWrzXKagO3oWszBO7MSx+GfaRk4E6A== -embla-carousel@^8.4.0: - version "8.4.0" - resolved "https://registry.yarnpkg.com/embla-carousel/-/embla-carousel-8.4.0.tgz#18bb23d2815e12e5c9602f1f56658931eef68d82" - integrity sha512-sUzm4DGGsdZCom7LEO38Uu6C7oQoFfPorKDf/f7j2EeRCMhHSOt3CvF+pHCaI6N+x5Y8/tfLueJ0WZlgUREnew== - emittery@^0.13.1: version "0.13.1" resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad" From 9315866355e627b7733f0520fb38ea537bf163c6 Mon Sep 17 00:00:00 2001 From: Mitch-At-Work Date: Sat, 23 Nov 2024 08:56:02 -0800 Subject: [PATCH 06/10] Update to only use one function and be compatible with previous implementations --- .../library/etc/react-carousel.api.md | 3 +- .../src/components/Carousel/useCarousel.ts | 37 ++++++--------- .../Carousel/useCarouselContextValues.ts | 3 -- .../useCarouselAutoplayButton.tsx | 8 ++-- .../library/src/components/CarouselContext.ts | 3 -- .../src/components/CarouselContext.types.ts | 3 +- .../CarouselViewport/useCarouselViewport.ts | 31 ++++++++++++- .../src/components/useEmblaCarousel.ts | 46 ++++++++----------- 8 files changed, 69 insertions(+), 65 deletions(-) diff --git a/packages/react-components/react-carousel/library/etc/react-carousel.api.md b/packages/react-components/react-carousel/library/etc/react-carousel.api.md index 37811801e3ead6..27d7ec103b9072 100644 --- a/packages/react-components/react-carousel/library/etc/react-carousel.api.md +++ b/packages/react-components/react-carousel/library/etc/react-carousel.api.md @@ -103,8 +103,7 @@ export type CarouselContextValue = { selectPageByDirection: (event: React_2.MouseEvent, direction: 'next' | 'prev') => number; selectPageByIndex: (event: React_2.MouseEvent, value: number, jump?: boolean) => void; subscribeForValues: (listener: (data: CarouselUpdateData) => void) => () => void; - enableAutoplay: (autoplay: boolean) => void; - initAutoplay: (autoplay: boolean) => void; + enableAutoplay: (autoplay: boolean, temporary?: boolean) => void; resetAutoplay: () => void; containerRef?: React_2.RefObject; viewportRef?: React_2.RefObject; diff --git a/packages/react-components/react-carousel/library/src/components/Carousel/useCarousel.ts b/packages/react-components/react-carousel/library/src/components/Carousel/useCarousel.ts index 50a9c9a47ba5e9..d71b5ff7d661d3 100644 --- a/packages/react-components/react-carousel/library/src/components/Carousel/useCarousel.ts +++ b/packages/react-components/react-carousel/library/src/components/Carousel/useCarousel.ts @@ -38,28 +38,20 @@ export function useCarousel_unstable(props: CarouselProps, ref: React.Ref { const foundIndex = carouselApi.scrollToElement(element, jump); @@ -145,6 +137,5 @@ export function useCarousel_unstable(props: CarouselProps, ref: React.Ref ctx.initAutoplay); + const enableAutoplay = useCarouselContext(ctx => ctx.enableAutoplay); React.useEffect(() => { // Initialize carousel autoplay based on button state - initAutoplay(autoplay); + enableAutoplay(autoplay); return () => { // We uninitialize autoplay if the button gets unmounted. - initAutoplay(false); + enableAutoplay(false); }; - }, [autoplay, initAutoplay]); + }, [autoplay, enableAutoplay]); const handleClick = (event: React.MouseEvent) => { if (event.isDefaultPrevented()) { diff --git a/packages/react-components/react-carousel/library/src/components/CarouselContext.ts b/packages/react-components/react-carousel/library/src/components/CarouselContext.ts index 2120dd736c9cd6..1dd1a90bf1f0db 100644 --- a/packages/react-components/react-carousel/library/src/components/CarouselContext.ts +++ b/packages/react-components/react-carousel/library/src/components/CarouselContext.ts @@ -18,9 +18,6 @@ export const carouselContextDefaultValue: CarouselContextValue = { enableAutoplay: () => { /** noop */ }, - initAutoplay: () => { - /** noop */ - }, resetAutoplay: () => { /** noop */ }, diff --git a/packages/react-components/react-carousel/library/src/components/CarouselContext.types.ts b/packages/react-components/react-carousel/library/src/components/CarouselContext.types.ts index 734f37a1f6a959..e1b21885b179a2 100644 --- a/packages/react-components/react-carousel/library/src/components/CarouselContext.types.ts +++ b/packages/react-components/react-carousel/library/src/components/CarouselContext.types.ts @@ -35,8 +35,7 @@ export type CarouselContextValue = { jump?: boolean, ) => void; subscribeForValues: (listener: (data: CarouselUpdateData) => void) => () => void; - enableAutoplay: (autoplay: boolean) => void; - initAutoplay: (autoplay: boolean) => void; + enableAutoplay: (autoplay: boolean, temporary?: boolean) => void; resetAutoplay: () => void; // Container with controls passed to carousel engine containerRef?: React.RefObject; diff --git a/packages/react-components/react-carousel/library/src/components/CarouselViewport/useCarouselViewport.ts b/packages/react-components/react-carousel/library/src/components/CarouselViewport/useCarouselViewport.ts index d2f3eb30c2aaa3..674983b1779b9d 100644 --- a/packages/react-components/react-carousel/library/src/components/CarouselViewport/useCarouselViewport.ts +++ b/packages/react-components/react-carousel/library/src/components/CarouselViewport/useCarouselViewport.ts @@ -16,13 +16,16 @@ export const useCarouselViewport_unstable = ( props: CarouselViewportProps, ref: React.Ref, ): CarouselViewportState => { + const hasFocus = React.useRef(false); + const hasMouse = React.useRef(false); const viewportRef = useCarouselContext(ctx => ctx.viewportRef); const enableAutoplay = useCarouselContext(ctx => ctx.enableAutoplay); const handleFocusCapture = React.useCallback( (e: React.FocusEvent) => { + hasFocus.current = true; // Will pause autoplay when focus is captured within viewport (if autoplay is initialized) - enableAutoplay(false); + enableAutoplay(false, true); }, [enableAutoplay], ); @@ -31,7 +34,27 @@ export const useCarouselViewport_unstable = ( (e: React.FocusEvent) => { // Will enable autoplay (if initialized) when focus exits viewport if (!e.currentTarget.contains(e.relatedTarget)) { - enableAutoplay(true); + hasFocus.current = false; + if (!hasMouse.current) { + enableAutoplay(true, true); + } + } + }, + [enableAutoplay], + ); + + const handleMouseEnter = React.useCallback( + (event: React.MouseEvent) => { + hasMouse.current = true; + enableAutoplay(false, true); + }, + [enableAutoplay], + ); + const handleMouseLeave = React.useCallback( + (event: React.MouseEvent) => { + hasMouse.current = false; + if (!hasFocus.current) { + enableAutoplay(true, true); } }, [enableAutoplay], @@ -39,6 +62,8 @@ export const useCarouselViewport_unstable = ( const onFocusCapture = mergeCallbacks(props.onFocusCapture, handleFocusCapture); const onBlurCapture = mergeCallbacks(props.onBlurCapture, handleBlurCapture); + const onMouseEnter = mergeCallbacks(props.onMouseEnter, handleMouseEnter); + const onMouseLeave = mergeCallbacks(props.onMouseLeave, handleMouseLeave); return { components: { @@ -53,6 +78,8 @@ export const useCarouselViewport_unstable = ( ...props, onFocusCapture, onBlurCapture, + onMouseEnter, + onMouseLeave, }), { elementType: 'div' }, ), diff --git a/packages/react-components/react-carousel/library/src/components/useEmblaCarousel.ts b/packages/react-components/react-carousel/library/src/components/useEmblaCarousel.ts index d15b9c0bdf3a33..7c6e9c4433661c 100644 --- a/packages/react-components/react-carousel/library/src/components/useEmblaCarousel.ts +++ b/packages/react-components/react-carousel/library/src/components/useEmblaCarousel.ts @@ -87,17 +87,16 @@ export function useEmblaCarousel( const getPlugins = React.useCallback(() => { const plugins: EmblaPluginType[] = []; - if (autoplay.current) { - plugins.push( - Autoplay({ - /* stopOnInteraction: false causes autoplay to restart on interaction end*/ - /* we must remove/re-add plugin on autoplay state change*/ - stopOnInteraction: false, - stopOnMouseEnter: true, - stopOnFocusIn: false, // We'll handle this one manually to prevent conflicts with tabster - }), - ); - } + plugins.push( + Autoplay({ + playOnInit: autoplay.current, + /* stopOnInteraction: false causes autoplay to restart on interaction end*/ + /* we'll handle this logic to ensure autoplay state is respected */ + stopOnInteraction: true, + stopOnMouseEnter: false, + stopOnFocusIn: false, // We'll handle this one manually to prevent conflicts with tabster + }), + ); // Optionally add Fade plugin if (motion === 'fade') { @@ -145,29 +144,23 @@ export function useEmblaCarousel( * Useful for pausing on focus etc. without having to reinitialize or set autoplay to off */ const enableAutoplay = React.useCallback( - (_autoplay: boolean) => { - if (_autoplay) { + (_autoplay: boolean, temporary?: boolean) => { + if (!temporary) { + autoplay.current = _autoplay; + } + + if (_autoplay && autoplay.current) { + // Autoplay should only enable in the case where underlying state is true, temporary should not override emblaApi.current?.plugins().autoplay?.play(); // Reset after play to ensure timing and any focus/mouse pause state is reset. resetAutoplay(); - } else { + } else if (!_autoplay) { emblaApi.current?.plugins().autoplay?.stop(); } }, [resetAutoplay], ); - /* Our autoplay button, which is required by standards for autoplay to be enabled, will handle controlled state */ - const initAutoplay = React.useCallback( - (_autoplay: boolean) => { - autoplay.current = _autoplay; - reinitializeCarousel(); - - enableAutoplay(_autoplay); - }, - [enableAutoplay, reinitializeCarousel], - ); - // Listeners contains callbacks for UI elements that may require state update based on embla changes const listeners = React.useRef(new Set<(data: CarouselUpdateData) => void>()); const subscribeForValues = React.useCallback((listener: (data: CarouselUpdateData) => void) => { @@ -261,6 +254,8 @@ export function useEmblaCarousel( emblaApi.current?.on('slidesInView', handleVisibilityChange); emblaApi.current?.on('select', handleIndexChange); emblaApi.current?.on('autoplay:select', handleAutoplayIndexChange); + emblaApi.current?.on('autoplay:play', () => console.log('Autoplay: start')); + emblaApi.current?.on('autoplay:stop', () => console.log('Autoplay: stop')); } }, }; @@ -315,6 +310,5 @@ export function useEmblaCarousel( subscribeForValues, enableAutoplay, resetAutoplay, - initAutoplay, }; } From 37d1efa019a7dffc73eef2712a99be7f2b28f46b Mon Sep 17 00:00:00 2001 From: Mitch-At-Work Date: Sat, 23 Nov 2024 08:57:07 -0800 Subject: [PATCH 07/10] Remove console log --- .../react-carousel/library/src/components/useEmblaCarousel.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/react-components/react-carousel/library/src/components/useEmblaCarousel.ts b/packages/react-components/react-carousel/library/src/components/useEmblaCarousel.ts index 7c6e9c4433661c..bda44403a56b74 100644 --- a/packages/react-components/react-carousel/library/src/components/useEmblaCarousel.ts +++ b/packages/react-components/react-carousel/library/src/components/useEmblaCarousel.ts @@ -254,8 +254,6 @@ export function useEmblaCarousel( emblaApi.current?.on('slidesInView', handleVisibilityChange); emblaApi.current?.on('select', handleIndexChange); emblaApi.current?.on('autoplay:select', handleAutoplayIndexChange); - emblaApi.current?.on('autoplay:play', () => console.log('Autoplay: start')); - emblaApi.current?.on('autoplay:stop', () => console.log('Autoplay: stop')); } }, }; From 1a1795476e871e57f71f6fb4448a97a36615cf52 Mon Sep 17 00:00:00 2001 From: Mitch-At-Work Date: Tue, 26 Nov 2024 14:24:22 -0800 Subject: [PATCH 08/10] PR comments --- .../library/etc/react-carousel.api.md | 3 +- .../src/components/Carousel/Carousel.types.ts | 12 +-- .../src/components/Carousel/useCarousel.ts | 3 +- .../src/components/CarouselContext.types.ts | 8 +- .../src/components/useEmblaCarousel.ts | 79 +++++++++---------- .../Carousel/CarouselActionCards.stories.tsx | 1 - .../Carousel/CarouselControlled.stories.tsx | 1 - .../src/Carousel/CarouselEventing.stories.tsx | 4 - 8 files changed, 42 insertions(+), 69 deletions(-) diff --git a/packages/react-components/react-carousel/library/etc/react-carousel.api.md b/packages/react-components/react-carousel/library/etc/react-carousel.api.md index 27d7ec103b9072..5ca840719cc5b7 100644 --- a/packages/react-components/react-carousel/library/etc/react-carousel.api.md +++ b/packages/react-components/react-carousel/library/etc/react-carousel.api.md @@ -115,7 +115,7 @@ export type CarouselContextValues = { }; // @public (undocumented) -export type CarouselIndexChangeData = (EventData<'click', React_2.MouseEvent> | EventData<'focus', React_2.FocusEvent> | EventData<'drag', PointerEvent | MouseEvent>) & { +export type CarouselIndexChangeData = (EventData<'click', React_2.MouseEvent> | EventData<'focus', React_2.FocusEvent> | EventData<'drag', PointerEvent | MouseEvent> | EventData<'autoplay', Event>) & { index: number; }; @@ -209,7 +209,6 @@ export type CarouselProps = ComponentProps & { align?: 'center' | 'start' | 'end'; activeIndex?: number; onActiveIndexChange?: EventHandler; - onAutoplayIndexChange?: (ev: null, data: CarouselAutoplayIndexChangeData) => void; circular?: boolean; groupSize?: number | 'auto'; draggable?: boolean; diff --git a/packages/react-components/react-carousel/library/src/components/Carousel/Carousel.types.ts b/packages/react-components/react-carousel/library/src/components/Carousel/Carousel.types.ts index 4cb2fd0fdc8e78..13503bb2f9b95b 100644 --- a/packages/react-components/react-carousel/library/src/components/Carousel/Carousel.types.ts +++ b/packages/react-components/react-carousel/library/src/components/Carousel/Carousel.types.ts @@ -1,9 +1,5 @@ import type { ComponentProps, ComponentState, EventHandler, Slot } from '@fluentui/react-utilities'; -import type { - CarouselAutoplayIndexChangeData, - CarouselContextValue, - CarouselIndexChangeData, -} from '../CarouselContext.types'; +import type { CarouselContextValue, CarouselIndexChangeData } from '../CarouselContext.types'; export type CarouselSlots = { root: Slot<'div'>; @@ -43,12 +39,6 @@ export type CarouselProps = ComponentProps & { */ onActiveIndexChange?: EventHandler; - /** - * Callback to notify a page change. - */ - // eslint-disable-next-line @nx/workspace-consistent-callback-type -- EventHandler does not support "null" - onAutoplayIndexChange?: (ev: null, data: CarouselAutoplayIndexChangeData) => void; - /** * Circular enables the carousel to loop back around on navigation past trailing index. */ diff --git a/packages/react-components/react-carousel/library/src/components/Carousel/useCarousel.ts b/packages/react-components/react-carousel/library/src/components/Carousel/useCarousel.ts index d71b5ff7d661d3..1f61ea71e13a9e 100644 --- a/packages/react-components/react-carousel/library/src/components/Carousel/useCarousel.ts +++ b/packages/react-components/react-carousel/library/src/components/Carousel/useCarousel.ts @@ -34,7 +34,6 @@ export function useCarousel_unstable(props: CarouselProps, ref: React.Ref { diff --git a/packages/react-components/react-carousel/library/src/components/CarouselContext.types.ts b/packages/react-components/react-carousel/library/src/components/CarouselContext.types.ts index e1b21885b179a2..51fae7eeb29c15 100644 --- a/packages/react-components/react-carousel/library/src/components/CarouselContext.types.ts +++ b/packages/react-components/react-carousel/library/src/components/CarouselContext.types.ts @@ -7,6 +7,7 @@ export type CarouselIndexChangeData = ( | EventData<'click', React.MouseEvent> | EventData<'focus', React.FocusEvent> | EventData<'drag', PointerEvent | MouseEvent> + | EventData<'autoplay', Event> ) & { /** * The index to be set after event has occurred. @@ -14,13 +15,6 @@ export type CarouselIndexChangeData = ( index: number; }; -export type CarouselAutoplayIndexChangeData = EventData<'autoplay', null> & { - /** - * The index to be set after event has occurred. - */ - index: number; -}; - export type CarouselContextValue = { activeIndex: number; circular: boolean; diff --git a/packages/react-components/react-carousel/library/src/components/useEmblaCarousel.ts b/packages/react-components/react-carousel/library/src/components/useEmblaCarousel.ts index bda44403a56b74..bde47aae3fa446 100644 --- a/packages/react-components/react-carousel/library/src/components/useEmblaCarousel.ts +++ b/packages/react-components/react-carousel/library/src/components/useEmblaCarousel.ts @@ -4,7 +4,7 @@ import * as React from 'react'; import { carouselCardClassNames } from './CarouselCard/useCarouselCardStyles.styles'; import { carouselSliderClassNames } from './CarouselSlider/useCarouselSliderStyles.styles'; -import { CarouselMotion, CarouselProps, CarouselUpdateData, CarouselVisibilityEventDetail } from '../Carousel'; +import { CarouselMotion, CarouselUpdateData, CarouselVisibilityEventDetail } from '../Carousel'; import Autoplay from 'embla-carousel-autoplay'; import Fade from 'embla-carousel-fade'; import { pointerEventPlugin } from './pointerEvents'; @@ -43,7 +43,7 @@ export function useEmblaCarousel( activeIndex: number | undefined; motion?: CarouselMotion; onDragIndexChange?: EventHandler; - onAutoplayIndexChange?: CarouselProps['onAutoplayIndexChange']; + onAutoplayIndexChange?: EventHandler; }, ) { const { @@ -78,7 +78,7 @@ export function useEmblaCarousel( }); const emblaApi = React.useRef(null); - const autoplay = React.useRef(false); + const autoplayRef = React.useRef(false); const resetAutoplay = React.useCallback(() => { emblaApi.current?.plugins().autoplay?.reset(); @@ -89,12 +89,12 @@ export function useEmblaCarousel( plugins.push( Autoplay({ - playOnInit: autoplay.current, + playOnInit: autoplayRef.current, /* stopOnInteraction: false causes autoplay to restart on interaction end*/ /* we'll handle this logic to ensure autoplay state is respected */ stopOnInteraction: true, - stopOnMouseEnter: false, stopOnFocusIn: false, // We'll handle this one manually to prevent conflicts with tabster + stopOnMouseEnter: false, // We will handle this manually to align functionality }), ); @@ -114,42 +114,16 @@ export function useEmblaCarousel( return plugins; }, [motion, onDragEvent, watchDrag]); - const reinitializeCarousel = React.useCallback(() => { - const plugins = getPlugins(); - - emblaOptions.current = { - startIndex: emblaOptions.current.startIndex, - align, - direction, - loop, - slidesToScroll, - watchDrag, - containScroll, - }; - - emblaApi.current?.reInit( - { - ...DEFAULT_EMBLA_OPTIONS, - ...emblaOptions.current, - }, - plugins, - ); - }, [align, containScroll, direction, getPlugins, loop, slidesToScroll, watchDrag]); - - React.useEffect(() => { - reinitializeCarousel(); - }, [reinitializeCarousel]); - /* This function enables autoplay to pause/play without affecting underlying state * Useful for pausing on focus etc. without having to reinitialize or set autoplay to off */ const enableAutoplay = React.useCallback( (_autoplay: boolean, temporary?: boolean) => { if (!temporary) { - autoplay.current = _autoplay; + autoplayRef.current = _autoplay; } - if (_autoplay && autoplay.current) { + if (_autoplay && autoplayRef.current) { // Autoplay should only enable in the case where underlying state is true, temporary should not override emblaApi.current?.plugins().autoplay?.play(); // Reset after play to ensure timing and any focus/mouse pause state is reset. @@ -202,14 +176,16 @@ export function useEmblaCarousel( }, [setActiveIndex]); const handleAutoplayIndexChange = useEventCallback(() => { - handleIndexChange(); + // Autoplay does not have an event trigger, we generate one to keep type consistency + const _event = new Event('autoplay'); const newIndex = emblaApi.current?.selectedScrollSnap() ?? 0; - onAutoplayIndexChange?.(null, { event: null, type: 'autoplay', index: newIndex }); + onAutoplayIndexChange?.(_event, { event: _event, type: 'autoplay', index: newIndex }); }); const viewportRef: React.RefObject = React.useRef(null); - const currentElementRef = React.useRef(); const containerRef: React.RefObject = React.useMemo(() => { + let currentElement: HTMLDivElement | null = null; + const handleVisibilityChange = () => { const cardElements = emblaApi.current?.slideNodes(); const visibleIndexes = emblaApi.current?.slidesInView() ?? []; @@ -229,7 +205,7 @@ export function useEmblaCarousel( return { set current(newElement: HTMLDivElement | null) { - if (currentElementRef.current) { + if (currentElement) { emblaApi.current?.off('slidesInView', handleVisibilityChange); emblaApi.current?.off('select', handleIndexChange); emblaApi.current?.off('reInit', handleReinit); @@ -238,11 +214,10 @@ export function useEmblaCarousel( } // Use direct viewport if available, else fallback to container (includes Carousel controls). - const wrapperElement = viewportRef.current ?? newElement; - currentElementRef.current = wrapperElement; - if (wrapperElement) { + currentElement = viewportRef.current ?? newElement; + if (currentElement) { emblaApi.current = EmblaCarousel( - wrapperElement, + currentElement, { ...DEFAULT_EMBLA_OPTIONS, ...emblaOptions.current, @@ -289,6 +264,28 @@ export function useEmblaCarousel( [], ); + React.useEffect(() => { + const plugins = getPlugins(); + + emblaOptions.current = { + startIndex: emblaOptions.current.startIndex, + align, + direction, + loop, + slidesToScroll, + watchDrag, + containScroll, + }; + + emblaApi.current?.reInit( + { + ...DEFAULT_EMBLA_OPTIONS, + ...emblaOptions.current, + }, + plugins, + ); + }, [align, containScroll, direction, getPlugins, loop, slidesToScroll, watchDrag]); + React.useEffect(() => { // Scroll to controlled values on update // If active index is out of bounds, re-init will handle instead diff --git a/packages/react-components/react-carousel/stories/src/Carousel/CarouselActionCards.stories.tsx b/packages/react-components/react-carousel/stories/src/Carousel/CarouselActionCards.stories.tsx index ce03868f6bb8dc..7c97d73aec0994 100644 --- a/packages/react-components/react-carousel/stories/src/Carousel/CarouselActionCards.stories.tsx +++ b/packages/react-components/react-carousel/stories/src/Carousel/CarouselActionCards.stories.tsx @@ -211,7 +211,6 @@ export const AlignmentAndWhitespace = () => { layout="inline" next={{ 'aria-label': 'go to next' }} prev={{ 'aria-label': 'go to prev' }} - autoplay={{}} > {index => } diff --git a/packages/react-components/react-carousel/stories/src/Carousel/CarouselControlled.stories.tsx b/packages/react-components/react-carousel/stories/src/Carousel/CarouselControlled.stories.tsx index 642db687426b4a..26ca5acceb4803 100644 --- a/packages/react-components/react-carousel/stories/src/Carousel/CarouselControlled.stories.tsx +++ b/packages/react-components/react-carousel/stories/src/Carousel/CarouselControlled.stories.tsx @@ -129,7 +129,6 @@ export const Controlled = () => { activeIndex={activeIndex} groupSize={1} onActiveIndexChange={(e, data) => setActiveIndex(data.index)} - onAutoplayIndexChange={(e, data) => setActiveIndex(data.index)} announcement={getAnnouncement} >
diff --git a/packages/react-components/react-carousel/stories/src/Carousel/CarouselEventing.stories.tsx b/packages/react-components/react-carousel/stories/src/Carousel/CarouselEventing.stories.tsx index c08bd7b221ebc2..94ab3e4e4f89d4 100644 --- a/packages/react-components/react-carousel/stories/src/Carousel/CarouselEventing.stories.tsx +++ b/packages/react-components/react-carousel/stories/src/Carousel/CarouselEventing.stories.tsx @@ -155,10 +155,6 @@ export const Eventing = () => { setActiveIndex(data.index); setStatusLog(prev => [[Date.now(), { type: data.type, index: data.index }], ...prev]); }} - onAutoplayIndexChange={(ev, data) => { - setActiveIndex(data.index); - setStatusLog(prev => [[Date.now(), { type: data.type, index: data.index }], ...prev]); - }} > From de72d549851203199f6445677dd626f0e7445887 Mon Sep 17 00:00:00 2001 From: Mitch-At-Work Date: Tue, 26 Nov 2024 15:56:14 -0800 Subject: [PATCH 09/10] Update index event to handle autoplay callback --- .../src/components/useEmblaCarousel.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/react-components/react-carousel/library/src/components/useEmblaCarousel.ts b/packages/react-components/react-carousel/library/src/components/useEmblaCarousel.ts index bde47aae3fa446..c6b846497ab346 100644 --- a/packages/react-components/react-carousel/library/src/components/useEmblaCarousel.ts +++ b/packages/react-components/react-carousel/library/src/components/useEmblaCarousel.ts @@ -10,6 +10,8 @@ import Fade from 'embla-carousel-fade'; import { pointerEventPlugin } from './pointerEvents'; import type { CarouselIndexChangeData } from './CarouselContext.types'; +type EmblaEventHandler = Parameters[1]; + const sliderClassname = `.${carouselSliderClassNames.root}`; const DEFAULT_EMBLA_OPTIONS: EmblaOptionsType = { @@ -163,7 +165,7 @@ export function useEmblaCarousel( } }); - const handleIndexChange = React.useCallback(() => { + const handleIndexChange: EmblaEventHandler = useEventCallback((_, eventType) => { const newIndex = emblaApi.current?.selectedScrollSnap() ?? 0; const slides = emblaApi.current?.slideNodes(); const actualIndex = emblaApi.current?.internalEngine().slideRegistry[newIndex][0] ?? 0; @@ -173,13 +175,11 @@ export function useEmblaCarousel( setTabsterDefault(slide, slideIndex === actualIndex); }); setActiveIndex(newIndex); - }, [setActiveIndex]); - const handleAutoplayIndexChange = useEventCallback(() => { - // Autoplay does not have an event trigger, we generate one to keep type consistency - const _event = new Event('autoplay'); - const newIndex = emblaApi.current?.selectedScrollSnap() ?? 0; - onAutoplayIndexChange?.(_event, { event: _event, type: 'autoplay', index: newIndex }); + if (eventType === 'autoplay:select') { + const _event = new Event('autoplay'); + onAutoplayIndexChange?.(_event, { event: _event, type: 'autoplay', index: newIndex }); + } }); const viewportRef: React.RefObject = React.useRef(null); @@ -209,7 +209,7 @@ export function useEmblaCarousel( emblaApi.current?.off('slidesInView', handleVisibilityChange); emblaApi.current?.off('select', handleIndexChange); emblaApi.current?.off('reInit', handleReinit); - emblaApi.current?.off('autoplay:select', handleAutoplayIndexChange); + emblaApi.current?.off('autoplay:select', handleIndexChange); emblaApi.current?.destroy(); } @@ -228,11 +228,11 @@ export function useEmblaCarousel( emblaApi.current?.on('reInit', handleReinit); emblaApi.current?.on('slidesInView', handleVisibilityChange); emblaApi.current?.on('select', handleIndexChange); - emblaApi.current?.on('autoplay:select', handleAutoplayIndexChange); + emblaApi.current?.on('autoplay:select', handleIndexChange); } }, }; - }, [getPlugins, handleAutoplayIndexChange, handleIndexChange, handleReinit]); + }, [getPlugins, handleIndexChange, handleReinit]); const carouselApi = React.useMemo( () => ({ From 2aba52c047ff1eee4557aee0210935c37b3ab885 Mon Sep 17 00:00:00 2001 From: Mitch-At-Work Date: Wed, 27 Nov 2024 09:29:32 -0800 Subject: [PATCH 10/10] Final PR comments --- .../library/src/components/useEmblaCarousel.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/react-components/react-carousel/library/src/components/useEmblaCarousel.ts b/packages/react-components/react-carousel/library/src/components/useEmblaCarousel.ts index c6b846497ab346..c23796005a4185 100644 --- a/packages/react-components/react-carousel/library/src/components/useEmblaCarousel.ts +++ b/packages/react-components/react-carousel/library/src/components/useEmblaCarousel.ts @@ -120,17 +120,17 @@ export function useEmblaCarousel( * Useful for pausing on focus etc. without having to reinitialize or set autoplay to off */ const enableAutoplay = React.useCallback( - (_autoplay: boolean, temporary?: boolean) => { + (autoplay: boolean, temporary?: boolean) => { if (!temporary) { - autoplayRef.current = _autoplay; + autoplayRef.current = autoplay; } - if (_autoplay && autoplayRef.current) { + if (autoplay && autoplayRef.current) { // Autoplay should only enable in the case where underlying state is true, temporary should not override emblaApi.current?.plugins().autoplay?.play(); // Reset after play to ensure timing and any focus/mouse pause state is reset. resetAutoplay(); - } else if (!_autoplay) { + } else if (!autoplay) { emblaApi.current?.plugins().autoplay?.stop(); } }, @@ -177,8 +177,8 @@ export function useEmblaCarousel( setActiveIndex(newIndex); if (eventType === 'autoplay:select') { - const _event = new Event('autoplay'); - onAutoplayIndexChange?.(_event, { event: _event, type: 'autoplay', index: newIndex }); + const noopEvent = new Event('autoplay'); + onAutoplayIndexChange?.(noopEvent, { event: noopEvent, type: 'autoplay', index: newIndex }); } });