import * as React from 'react'; import { CSSProperties, useContext, useRef, useEffect, MouseEvent } from 'react'; import Arrow from '../../../Icons/Arrows/Arrow'; import { StoreContext } from '../../../../services/store'; import { IMedia } from '../../../../types'; import { uniformCarouselTransitionDurationSec } from '../../../consts'; import { useAccessibilityMessage } from '../../../../hooks/useAccessibilityMessage'; import { useUniformCenteredConfig, useCarouselLayout, useInfiniteNavigation, } from '../shared/hooks'; import { useUniformTranslateX } from './hooks'; import { getCarouselMovementMessage, getSrOnlyStyle, normalizeIndex, } from '../shared/accessibility'; interface Props { items: T[]; nbElems: number; style?: CSSProperties; onPostChange: (post: IMedia | null) => void; } const UniformCarousel = (props: Props) => { const context = useContext(StoreContext); const slideRef = useRef(null); const containerRef = useRef(null); const outerRef = useRef(null); const itemCount = props.items.length; const { accessibilityMessage, announce } = useAccessibilityMessage(); const visibleContentSize = Math.max(props.nbElems, 1); const contentSize = context.data.settings.content_size; const { contentSizeNotReached, cloneCount } = useUniformCenteredConfig( itemCount, visibleContentSize, context.setStoreState, ); const { compactStyle, carouselDimensions, arrowSize } = useCarouselLayout({ context: { widgetWidth: context.widgetWidth, contentSize, isMobile: context.isMobile, hideArrows: context.config.hideArrows || false, }, config: { contentSize: visibleContentSize, itemCount, partialItemWidth: 0 }, refs: { slideRef }, }); const { activeIndex, handleSlide, skipTransition } = useInfiniteNavigation({ config: { itemCount, contentSize: visibleContentSize, cloneCount, contentSizeNotReached }, refs: { slideRef, containerRef }, state: { itemWidth: carouselDimensions.itemWidth, currentPosition: context.currentPosition }, actions: { setStoreState: context.setStoreState }, transitionDurationSec: uniformCarouselTransitionDurationSec, }); const { getTranslateX } = useUniformTranslateX({ config: { cloneCount }, state: { itemWidth: carouselDimensions.itemWidth, activeIndex }, }); const onArrowClick = ( event: MouseEvent | undefined, direction: 'left' | 'right', ) => { const nextIndex = activeIndex + (direction === 'left' ? -1 : 1); const normalizedIndex = normalizeIndex(nextIndex, itemCount); // Announce the next position for screen reader users, then perform the slide. announce(getCarouselMovementMessage(normalizedIndex, itemCount)); handleSlide(direction); if (event) { context.triggerEvent( 'carouselArrowClicked', { position: activeIndex, originalEvent: event.nativeEvent, direction, }, context.data.id, ); } }; const renderSlides = () => { const { itemWidth } = carouselDimensions; if (cloneCount === 0) { return props.items.map((item, i) => renderSlide(item, i, itemWidth)); } // Prepend and append clones so the carousel can scroll seamlessly. const cloneStart = props.items .slice(props.items.length - cloneCount) .map((item, i) => renderSlide(item, i - cloneCount, itemWidth)); const realSlides = props.items.map((item, i) => renderSlide(item, i, itemWidth)); const cloneEnd = props.items .slice(0, cloneCount) .map((item, i) => renderSlide(item, props.items.length + i, itemWidth)); return [...cloneStart, ...realSlides, ...cloneEnd]; }; const renderSlide = (item: JSX.Element, index: number, width: number) => { const isClone = index < 0 || index >= itemCount; const cloneAccessibilityProps = isClone ? { 'aria-hidden': true, 'data-carousel-clone': 'true' } : {}; // Clone slides are only for visual wrapping and should not be exposed to assistive tech. return (
{item}
); }; const renderDesktopCarousel = () => { // Hide arrows when there is only one item, when arrows are disabled, or when the content size limit is not yet reached. const showArrows = itemCount > 1 && !context.config.hideArrows && !contentSizeNotReached; const hasContentSizeNotReachedWithoutClones = cloneCount === 0 && itemCount < visibleContentSize; // When content size is not reached, shift items so the empty space is split evenly left/right const contentSizeNotReachedOffset = hasContentSizeNotReachedWithoutClones ? ((visibleContentSize - itemCount) * carouselDimensions.itemWidth) / 2 : 0; const leftPosition = hasContentSizeNotReachedWithoutClones ? contentSizeNotReachedOffset : getTranslateX(); return ( <> onArrowClick(e, 'left')} show={showArrows} classPrefix={context.config.classPrefix} size={arrowSize} ariaLabel='Previous posts' customArrow={context.config.arrowLeft} />
{renderSlides()}
onArrowClick(e, 'right')} show={showArrows} classPrefix={context.config.classPrefix} size={arrowSize} ariaLabel='Next posts' customArrow={context.config.arrowRight} /> ); }; return (
{accessibilityMessage}
{renderDesktopCarousel()}
); }; const carouselStyle: CSSProperties = { width: '100%', display: 'flex', position: 'relative', minHeight: 'fit-content', alignItems: 'center', }; const desktopSlideStyle: CSSProperties = { flex: 1, height: 'auto', overflow: 'hidden', position: 'relative', display: 'flex', alignItems: 'center', paddingTop: '20px', paddingBottom: '20px', }; const desktopSlideContainerStyle: CSSProperties = { position: 'relative', height: 'auto', display: 'flex', alignItems: 'center', willChange: 'transform', transition: `transform ${uniformCarouselTransitionDurationSec}s ease`, }; const slideItemStyle: CSSProperties = { display: 'inline-block', height: 'auto', flexShrink: 0, boxSizing: 'border-box', isolation: 'auto', }; export default UniformCarousel;