305 lines
18 KiB
JavaScript
305 lines
18 KiB
JavaScript
'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
|
|
};
|
|
}
|