diff --git a/src/components/TileDock/TileDock.tsx b/src/components/TileDock/TileDock.tsx index 6375ea0a2..885501148 100644 --- a/src/components/TileDock/TileDock.tsx +++ b/src/components/TileDock/TileDock.tsx @@ -1,5 +1,5 @@ import classNames from 'classnames'; -import React, { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import React, { type ReactNode, useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'; import styles from './TileDock.module.scss'; @@ -16,13 +16,13 @@ export type TileDockProps = { minimalTouchMovement?: number; showControls?: boolean; showDots?: boolean; - animated?: boolean; + animationModeOverride?: boolean; wrapWithEmptyTiles?: boolean; transitionTime?: string; - renderTile: (item: T, isInView: boolean) => JSX.Element; - renderLeftControl?: (handleClick: () => void) => JSX.Element; - renderRightControl?: (handleClick: () => void) => JSX.Element; - renderPaginationDots?: (index: number, pageIndex: number) => JSX.Element; + renderTile: (item: T, isInView: boolean) => ReactNode; + renderLeftControl?: (handleClick: () => void) => ReactNode; + renderRightControl?: (handleClick: () => void) => ReactNode; + renderPaginationDots?: (index: number, pageIndex: number) => ReactNode; }; type Tile = { @@ -66,7 +66,7 @@ function TileDock({ spacing = 12, minimalTouchMovement = 30, showControls = true, - animated = !window.matchMedia('(prefers-reduced-motion)').matches, + animationModeOverride, transitionTime = '0.6s', wrapWithEmptyTiles = false, showDots = false, @@ -75,11 +75,13 @@ function TileDock({ renderRightControl, renderPaginationDots, }: TileDockProps) { - const [index, setIndex] = useState(0); - const [slideToIndex, setSlideToIndex] = useState(0); - const [transform, setTransform] = useState(-100); - const [doAnimationReset, setDoAnimationReset] = useState(false); - const [hasTransition, setHasTransition] = useState(false); + const [index, setIndex] = useState(0); + const [slideToIndex, setSlideToIndex] = useState(0); + const [transform, setTransform] = useState(-100); + // Prevent animation mode from changing after first load + const [isAnimated] = useState(animationModeOverride ?? !window.matchMedia('(prefers-reduced-motion)').matches); + const [isAnimationDone, setIsAnimationDone] = useState(false); + const [isAnimationRunning, setIsAnimationRunning] = useState(false); const frameRef = useRef() as React.MutableRefObject; const tileWidth: number = 100 / tilesToShow; @@ -90,7 +92,7 @@ function TileDock({ return sliceItems(items, isMultiPage, index, tilesToShow, cycleMode); }, [items, isMultiPage, index, tilesToShow, cycleMode]); - const transitionBasis: string = isMultiPage && animated && hasTransition ? `transform ${transitionTime} ease` : ''; + const transitionBasis: string = isMultiPage && isAnimated && isAnimationRunning ? `transform ${transitionTime} ease` : ''; const needControls: boolean = showControls && isMultiPage; const showLeftControl: boolean = needControls && !(cycleMode === 'stop' && index === 0); @@ -98,7 +100,8 @@ function TileDock({ const slide = useCallback( (direction: Direction): void => { - if (hasTransition) { + // Debounce slide events based on if the animation is running + if (isAnimationRunning) { return; } @@ -120,11 +123,17 @@ function TileDock({ setSlideToIndex(nextIndex); setTransform(-100 + movement); - setHasTransition(true); - if (!animated) setDoAnimationReset(true); + // If this is an animated slider, start the animation 'slide' + if (isAnimated) { + setIsAnimationRunning(true); + } + // If not anmiated, trigger the post animation code right away + else { + setIsAnimationDone(true); + } }, - [animated, cycleMode, index, items.length, tileWidth, tilesToShow, hasTransition], + [isAnimated, cycleMode, index, items.length, tileWidth, tilesToShow, isAnimationRunning], ); const handleTouchStart = useCallback( @@ -134,7 +143,7 @@ function TileDock({ y: event.touches[0].clientY, }; - function handleTouchMove(this: HTMLDocument, event: TouchEvent): void { + function handleTouchMove(this: Document, event: TouchEvent): void { const newPosition: Position = { x: event.changedTouches[0].clientX, y: event.changedTouches[0].clientY, @@ -148,7 +157,7 @@ function TileDock({ } } - function handleTouchEnd(this: HTMLDocument, event: TouchEvent): void { + function handleTouchEnd(this: Document, event: TouchEvent): void { const newPosition = { x: event.changedTouches[0].clientX, y: event.changedTouches[0].clientY, @@ -182,8 +191,9 @@ function TileDock({ [minimalTouchMovement, slide], ); + // Run code after the slide animation to set the new index useLayoutEffect(() => { - const resetAnimation = (): void => { + const postAnimationCleanup = (): void => { let resetIndex: number = slideToIndex; resetIndex = resetIndex >= items.length ? slideToIndex - items.length : resetIndex; @@ -195,16 +205,18 @@ function TileDock({ setIndex(resetIndex); setTransform(-100); - setDoAnimationReset(false); + setIsAnimationRunning(false); + setIsAnimationDone(false); }; - if (doAnimationReset) resetAnimation(); - }, [doAnimationReset, index, items.length, slideToIndex, tileWidth, tilesToShow, transitionBasis]); + if (isAnimationDone) { + postAnimationCleanup(); + } + }, [isAnimationDone, index, items.length, slideToIndex, tileWidth, tilesToShow, transitionBasis]); const handleTransitionEnd = (event: React.TransitionEvent) => { if (event.target === frameRef.current) { - setDoAnimationReset(true); - setHasTransition(false); + setIsAnimationDone(true); } };