'use client'; import { useControllableState, useEventCallback } from '@fluentui/react-utilities'; import EmblaCarousel from 'embla-carousel'; import * as React from 'react'; import { carouselCardClassNames } from './CarouselCard/useCarouselCardStyles.styles'; import { carouselSliderClassNames } from './CarouselSlider/useCarouselSliderStyles.styles'; import Autoplay from 'embla-carousel-autoplay'; import Fade from 'embla-carousel-fade'; import { pointerEventPlugin } from './pointerEvents'; const sliderClassname = `.${carouselSliderClassNames.root}`; const DEFAULT_EMBLA_OPTIONS = { containScroll: 'trimSnaps', inViewThreshold: 0.99, watchDrag: false, skipSnaps: true, container: sliderClassname, slides: `.${carouselCardClassNames.root}` }; export const EMBLA_VISIBILITY_EVENT = 'embla:visibilitychange'; export function setTabsterDefault(element, isDefault) { const tabsterAttr = element.getAttribute('data-tabster'); if (tabsterAttr) { const tabsterAttributes = JSON.parse(tabsterAttr); if (tabsterAttributes.focusable) { // If tabster.focusable isn't present, we will ignore. tabsterAttributes.focusable.isDefault = isDefault; element.setAttribute('data-tabster', JSON.stringify(tabsterAttributes)); } } } export function useEmblaCarousel(options) { const { align, autoplayInterval, direction, loop, slidesToScroll, watchDrag, containScroll, motion, onDragIndexChange, onAutoplayIndexChange } = options; var _motion_kind; const motionType = typeof motion === 'string' ? motion : (_motion_kind = motion === null || motion === void 0 ? void 0 : motion.kind) !== null && _motion_kind !== void 0 ? _motion_kind : 'slide'; var _motion_duration; const motionDuration = typeof motion === 'string' ? 25 : (_motion_duration = motion === null || motion === void 0 ? void 0 : motion.duration) !== null && _motion_duration !== void 0 ? _motion_duration : 25; const [activeIndex, setActiveIndex] = useControllableState({ defaultState: options.defaultActiveIndex, state: options.activeIndex, initialState: 0 }); const onDragEvent = useEventCallback((event, index)=>{ onDragIndexChange === null || onDragIndexChange === void 0 ? void 0 : onDragIndexChange(event, { event, type: 'drag', index }); }); const emblaOptions = React.useRef({ align, direction, loop, slidesToScroll, startIndex: activeIndex, watchDrag, containScroll, duration: motionDuration }); const emblaApi = React.useRef(null); const autoplayRef = React.useRef(false); const resetAutoplay = React.useCallback(()=>{ var _emblaApi_current_plugins_autoplay, _emblaApi_current; (_emblaApi_current = emblaApi.current) === null || _emblaApi_current === void 0 ? void 0 : (_emblaApi_current_plugins_autoplay = _emblaApi_current.plugins().autoplay) === null || _emblaApi_current_plugins_autoplay === void 0 ? void 0 : _emblaApi_current_plugins_autoplay.reset(); }, []); const getPlugins = React.useCallback(()=>{ const plugins = []; plugins.push(Autoplay({ playOnInit: autoplayRef.current, delay: autoplayInterval, /* stopOnInteraction: false causes autoplay to restart on interaction end*/ /* we'll handle this logic to ensure autoplay state is respected */ stopOnInteraction: true, stopOnFocusIn: false, stopOnMouseEnter: false })); // Optionally add Fade plugin if (motionType === 'fade') { plugins.push(Fade()); } if (watchDrag) { plugins.push(pointerEventPlugin({ onSelectViaDrag: onDragEvent })); } return plugins; }, [ motionType, onDragEvent, watchDrag, autoplayInterval ]); /* 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, temporary)=>{ if (!temporary) { autoplayRef.current = autoplay; } if (autoplay && autoplayRef.current) { var // Autoplay should only enable in the case where underlying state is true, temporary should not override _emblaApi_current_plugins_autoplay, _emblaApi_current; (_emblaApi_current = emblaApi.current) === null || _emblaApi_current === void 0 ? void 0 : (_emblaApi_current_plugins_autoplay = _emblaApi_current.plugins().autoplay) === null || _emblaApi_current_plugins_autoplay === void 0 ? void 0 : _emblaApi_current_plugins_autoplay.play(); // Reset after play to ensure timing and any focus/mouse pause state is reset. resetAutoplay(); } else if (!autoplay) { var _emblaApi_current_plugins_autoplay1, _emblaApi_current1; (_emblaApi_current1 = emblaApi.current) === null || _emblaApi_current1 === void 0 ? void 0 : (_emblaApi_current_plugins_autoplay1 = _emblaApi_current1.plugins().autoplay) === null || _emblaApi_current_plugins_autoplay1 === void 0 ? void 0 : _emblaApi_current_plugins_autoplay1.stop(); } }, [ resetAutoplay ]); // Listeners contains callbacks for UI elements that may require state update based on embla changes const listeners = React.useRef(new Set()); const subscribeForValues = React.useCallback((listener)=>{ listeners.current.add(listener); return ()=>{ listeners.current.delete(listener); }; }, []); const updateIndex = ()=>{ var _emblaApi_current, _emblaApi_current1, _emblaApi_current2, _slideRegistry_newIndex; var _emblaApi_current_selectedScrollSnap; const newIndex = (_emblaApi_current_selectedScrollSnap = (_emblaApi_current = emblaApi.current) === null || _emblaApi_current === void 0 ? void 0 : _emblaApi_current.selectedScrollSnap()) !== null && _emblaApi_current_selectedScrollSnap !== void 0 ? _emblaApi_current_selectedScrollSnap : 0; const slides = (_emblaApi_current1 = emblaApi.current) === null || _emblaApi_current1 === void 0 ? void 0 : _emblaApi_current1.slideNodes(); const slideRegistry = (_emblaApi_current2 = emblaApi.current) === null || _emblaApi_current2 === void 0 ? void 0 : _emblaApi_current2.internalEngine().slideRegistry; var _slideRegistry_newIndex_; const actualIndex = (_slideRegistry_newIndex_ = slideRegistry === null || slideRegistry === void 0 ? void 0 : (_slideRegistry_newIndex = slideRegistry[newIndex]) === null || _slideRegistry_newIndex === void 0 ? void 0 : _slideRegistry_newIndex[0]) !== null && _slideRegistry_newIndex_ !== void 0 ? _slideRegistry_newIndex_ : 0; // We set the first card in the current group as the default tabster index for focus capture slides === null || slides === void 0 ? void 0 : slides.forEach((slide, slideIndex)=>{ setTabsterDefault(slide, slideIndex === actualIndex); }); setActiveIndex(newIndex); }; const handleReinit = useEventCallback(()=>{ var _emblaApi_current, _emblaApi_current1, _emblaApi_current2, _emblaApi_current3, _emblaApi_current4; var _emblaApi_current_slideNodes; const nodes = (_emblaApi_current_slideNodes = (_emblaApi_current = emblaApi.current) === null || _emblaApi_current === void 0 ? void 0 : _emblaApi_current.slideNodes()) !== null && _emblaApi_current_slideNodes !== void 0 ? _emblaApi_current_slideNodes : []; var _emblaApi_current_internalEngine_slideRegistry; const groupIndexList = (_emblaApi_current_internalEngine_slideRegistry = (_emblaApi_current1 = emblaApi.current) === null || _emblaApi_current1 === void 0 ? void 0 : _emblaApi_current1.internalEngine().slideRegistry) !== null && _emblaApi_current_internalEngine_slideRegistry !== void 0 ? _emblaApi_current_internalEngine_slideRegistry : []; const navItemsCount = groupIndexList.length > 0 ? groupIndexList.length : nodes.length; const canLoop = (_emblaApi_current2 = emblaApi.current) === null || _emblaApi_current2 === void 0 ? void 0 : _emblaApi_current2.internalEngine().slideLooper.canLoop(); var _emblaApi_current_selectedScrollSnap; const data = { navItemsCount, activeIndex: (_emblaApi_current_selectedScrollSnap = (_emblaApi_current3 = emblaApi.current) === null || _emblaApi_current3 === void 0 ? void 0 : _emblaApi_current3.selectedScrollSnap()) !== null && _emblaApi_current_selectedScrollSnap !== void 0 ? _emblaApi_current_selectedScrollSnap : 0, groupIndexList, slideNodes: nodes, canLoop }; updateIndex(); (_emblaApi_current4 = emblaApi.current) === null || _emblaApi_current4 === void 0 ? void 0 : _emblaApi_current4.scrollTo(activeIndex, false); for (const listener of listeners.current){ listener(data); } }); const handleIndexChange = useEventCallback((_, eventType)=>{ var _emblaApi_current; var _emblaApi_current_selectedScrollSnap; const newIndex = (_emblaApi_current_selectedScrollSnap = (_emblaApi_current = emblaApi.current) === null || _emblaApi_current === void 0 ? void 0 : _emblaApi_current.selectedScrollSnap()) !== null && _emblaApi_current_selectedScrollSnap !== void 0 ? _emblaApi_current_selectedScrollSnap : 0; updateIndex(); if (eventType === 'autoplay:select') { const noopEvent = new Event('autoplay'); onAutoplayIndexChange === null || onAutoplayIndexChange === void 0 ? void 0 : onAutoplayIndexChange(noopEvent, { event: noopEvent, type: 'autoplay', index: newIndex }); } }); const viewportRef = React.useRef(null); const containerRef = React.useMemo(()=>{ const handleVisibilityChange = ()=>{ var _emblaApi_current, _emblaApi_current1; const cardElements = (_emblaApi_current = emblaApi.current) === null || _emblaApi_current === void 0 ? void 0 : _emblaApi_current.slideNodes(); var _emblaApi_current_slidesInView; const visibleIndexes = (_emblaApi_current_slidesInView = (_emblaApi_current1 = emblaApi.current) === null || _emblaApi_current1 === void 0 ? void 0 : _emblaApi_current1.slidesInView()) !== null && _emblaApi_current_slidesInView !== void 0 ? _emblaApi_current_slidesInView : []; cardElements === null || cardElements === void 0 ? void 0 : cardElements.forEach((cardElement, index)=>{ cardElement.dispatchEvent(new CustomEvent(EMBLA_VISIBILITY_EVENT, { bubbles: false, detail: { isVisible: visibleIndexes.includes(index) } })); }); }; // Get plugins using autoplayRef to prevent state change recreating EmblaCarousel const plugins = getPlugins(); return { set current (newElement){ if (emblaApi.current) { var // Stop autoplay before reinitializing. _emblaApi_current_plugins_autoplay, _emblaApi_current_plugins, _emblaApi_current; (_emblaApi_current_plugins = (_emblaApi_current = emblaApi.current).plugins) === null || _emblaApi_current_plugins === void 0 ? void 0 : (_emblaApi_current_plugins_autoplay = _emblaApi_current_plugins.call(_emblaApi_current).autoplay) === null || _emblaApi_current_plugins_autoplay === void 0 ? void 0 : _emblaApi_current_plugins_autoplay.stop(); emblaApi.current.off('slidesInView', handleVisibilityChange); emblaApi.current.off('select', handleIndexChange); emblaApi.current.off('reInit', handleReinit); emblaApi.current.off('autoplay:select', handleIndexChange); emblaApi.current.destroy(); emblaApi.current = null; } if (newElement) { var // Use direct viewport if available, else fallback to container (includes Carousel controls). _viewportRef_current; const newEmblaApi = EmblaCarousel((_viewportRef_current = viewportRef.current) !== null && _viewportRef_current !== void 0 ? _viewportRef_current : newElement, { ...DEFAULT_EMBLA_OPTIONS, ...emblaOptions.current }, plugins); newEmblaApi.on('reInit', handleReinit); newEmblaApi.on('slidesInView', handleVisibilityChange); newEmblaApi.on('select', handleIndexChange); newEmblaApi.on('autoplay:select', handleIndexChange); emblaApi.current = newEmblaApi; } } }; }, [ getPlugins, handleIndexChange, handleReinit ]); const carouselApi = React.useMemo(()=>({ scrollToElement: (element, jump)=>{ var _emblaApi_current, _emblaApi_current1, _emblaApi_current2; const cardElements = (_emblaApi_current = emblaApi.current) === null || _emblaApi_current === void 0 ? void 0 : _emblaApi_current.slideNodes(); var _emblaApi_current_internalEngine_slideRegistry; const groupIndexList = (_emblaApi_current_internalEngine_slideRegistry = (_emblaApi_current1 = emblaApi.current) === null || _emblaApi_current1 === void 0 ? void 0 : _emblaApi_current1.internalEngine().slideRegistry) !== null && _emblaApi_current_internalEngine_slideRegistry !== void 0 ? _emblaApi_current_internalEngine_slideRegistry : []; var _cardElements_indexOf; const cardIndex = (_cardElements_indexOf = cardElements === null || cardElements === void 0 ? void 0 : cardElements.indexOf(element)) !== null && _cardElements_indexOf !== void 0 ? _cardElements_indexOf : 0; const groupIndex = groupIndexList.findIndex((group)=>{ return group.includes(cardIndex); }); const indexFocus = groupIndex !== null && groupIndex !== void 0 ? groupIndex : cardIndex; (_emblaApi_current2 = emblaApi.current) === null || _emblaApi_current2 === void 0 ? void 0 : _emblaApi_current2.scrollTo(indexFocus, jump); return indexFocus; }, scrollToIndex: (index, jump)=>{ var _emblaApi_current; (_emblaApi_current = emblaApi.current) === null || _emblaApi_current === void 0 ? void 0 : _emblaApi_current.scrollTo(index, jump); }, scrollInDirection: (dir)=>{ var _emblaApi_current; if (dir === 'prev') { var _emblaApi_current1; (_emblaApi_current1 = emblaApi.current) === null || _emblaApi_current1 === void 0 ? void 0 : _emblaApi_current1.scrollPrev(); } else { var _emblaApi_current2; (_emblaApi_current2 = emblaApi.current) === null || _emblaApi_current2 === void 0 ? void 0 : _emblaApi_current2.scrollNext(); } var _emblaApi_current_selectedScrollSnap; return (_emblaApi_current_selectedScrollSnap = (_emblaApi_current = emblaApi.current) === null || _emblaApi_current === void 0 ? void 0 : _emblaApi_current.selectedScrollSnap()) !== null && _emblaApi_current_selectedScrollSnap !== void 0 ? _emblaApi_current_selectedScrollSnap : 0; } }), []); React.useEffect(()=>{ var // Stop autoplay before reinitializing. _emblaApi_current_plugins_autoplay, _emblaApi_current_plugins, _emblaApi_current, _emblaApi_current1; const plugins = getPlugins(); emblaOptions.current = { startIndex: emblaOptions.current.startIndex, align, direction, loop, slidesToScroll, watchDrag, containScroll, duration: motionDuration }; (_emblaApi_current = emblaApi.current) === null || _emblaApi_current === void 0 ? void 0 : (_emblaApi_current_plugins = _emblaApi_current.plugins) === null || _emblaApi_current_plugins === void 0 ? void 0 : (_emblaApi_current_plugins_autoplay = _emblaApi_current_plugins.call(_emblaApi_current).autoplay) === null || _emblaApi_current_plugins_autoplay === void 0 ? void 0 : _emblaApi_current_plugins_autoplay.stop(); (_emblaApi_current1 = emblaApi.current) === null || _emblaApi_current1 === void 0 ? void 0 : _emblaApi_current1.reInit({ ...DEFAULT_EMBLA_OPTIONS, ...emblaOptions.current }, plugins); }, [ align, containScroll, direction, getPlugins, loop, slidesToScroll, watchDrag, motionDuration ]); React.useEffect(()=>{ var _emblaApi_current, _emblaApi_current_slideNodes, _emblaApi_current1; var _emblaApi_current_selectedScrollSnap; // Scroll to controlled values on update // If active index is out of bounds, re-init will handle instead const currentActiveIndex = (_emblaApi_current_selectedScrollSnap = (_emblaApi_current = emblaApi.current) === null || _emblaApi_current === void 0 ? void 0 : _emblaApi_current.selectedScrollSnap()) !== null && _emblaApi_current_selectedScrollSnap !== void 0 ? _emblaApi_current_selectedScrollSnap : 0; var _emblaApi_current_slideNodes_length; const slideLength = (_emblaApi_current_slideNodes_length = (_emblaApi_current1 = emblaApi.current) === null || _emblaApi_current1 === void 0 ? void 0 : (_emblaApi_current_slideNodes = _emblaApi_current1.slideNodes()) === null || _emblaApi_current_slideNodes === void 0 ? void 0 : _emblaApi_current_slideNodes.length) !== null && _emblaApi_current_slideNodes_length !== void 0 ? _emblaApi_current_slideNodes_length : 0; emblaOptions.current.startIndex = activeIndex; if (activeIndex < slideLength && activeIndex !== currentActiveIndex) { var _emblaApi_current2; (_emblaApi_current2 = emblaApi.current) === null || _emblaApi_current2 === void 0 ? void 0 : _emblaApi_current2.scrollTo(activeIndex); } }, [ activeIndex ]); return { activeIndex, carouselApi, viewportRef, containerRef, subscribeForValues, enableAutoplay, resetAutoplay }; }