From e827affd97f4cd3dbff4296c8686b77fab4a06b4 Mon Sep 17 00:00:00 2001 From: Danny Budzinski Date: Fri, 19 Jan 2024 11:28:23 +0100 Subject: [PATCH 1/3] fix: prevent slider from getting stuck in non-anmiated mode --- src/components/TileDock/TileDock.tsx | 56 ++++++++++++++++------------ 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/src/components/TileDock/TileDock.tsx b/src/components/TileDock/TileDock.tsx index 6375ea0a2..ff44868fe 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'; @@ -19,10 +19,10 @@ export type TileDockProps = { animated?: 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 = { @@ -75,11 +75,11 @@ 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); + const [animationDone, setAnimationDone] = useState(false); + const [isAnimationRunning, setIsAnimationRunning] = useState(false); const frameRef = useRef() as React.MutableRefObject; const tileWidth: number = 100 / tilesToShow; @@ -90,7 +90,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 && animated && isAnimationRunning ? `transform ${transitionTime} ease` : ''; const needControls: boolean = showControls && isMultiPage; const showLeftControl: boolean = needControls && !(cycleMode === 'stop' && index === 0); @@ -98,7 +98,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 +121,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 (animated) { + setIsAnimationRunning(true); + } + // If not anmiated, trigger the post animation code right away + else { + setAnimationDone(true); + } }, - [animated, cycleMode, index, items.length, tileWidth, tilesToShow, hasTransition], + [animated, cycleMode, index, items.length, tileWidth, tilesToShow, isAnimationRunning], ); const handleTouchStart = useCallback( @@ -134,7 +141,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 +155,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 +189,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 +203,18 @@ function TileDock({ setIndex(resetIndex); setTransform(-100); - setDoAnimationReset(false); + setIsAnimationRunning(false); + setAnimationDone(false); }; - if (doAnimationReset) resetAnimation(); - }, [doAnimationReset, index, items.length, slideToIndex, tileWidth, tilesToShow, transitionBasis]); + if (animationDone) { + postAnimationCleanup(); + } + }, [animationDone, index, items.length, slideToIndex, tileWidth, tilesToShow, transitionBasis]); const handleTransitionEnd = (event: React.TransitionEvent) => { if (event.target === frameRef.current) { - setDoAnimationReset(true); - setHasTransition(false); + setAnimationDone(true); } }; From 1b36ae0e391d9ec977fdd6e8b0ef3befa43ddc5a Mon Sep 17 00:00:00 2001 From: Danny Budzinski Date: Fri, 19 Jan 2024 11:57:19 +0100 Subject: [PATCH 2/3] fix: lock animation mode on first load --- src/components/TileDock/TileDock.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/TileDock/TileDock.tsx b/src/components/TileDock/TileDock.tsx index ff44868fe..a95ea47df 100644 --- a/src/components/TileDock/TileDock.tsx +++ b/src/components/TileDock/TileDock.tsx @@ -16,7 +16,7 @@ export type TileDockProps = { minimalTouchMovement?: number; showControls?: boolean; showDots?: boolean; - animated?: boolean; + animatedOverride?: boolean; wrapWithEmptyTiles?: boolean; transitionTime?: string; renderTile: (item: T, isInView: boolean) => ReactNode; @@ -66,7 +66,7 @@ function TileDock({ spacing = 12, minimalTouchMovement = 30, showControls = true, - animated = !window.matchMedia('(prefers-reduced-motion)').matches, + animatedOverride, transitionTime = '0.6s', wrapWithEmptyTiles = false, showDots = false, @@ -78,6 +78,8 @@ function TileDock({ 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(animatedOverride ?? !window.matchMedia('(prefers-reduced-motion)').matches); const [animationDone, setAnimationDone] = useState(false); const [isAnimationRunning, setIsAnimationRunning] = useState(false); @@ -90,7 +92,7 @@ function TileDock({ return sliceItems(items, isMultiPage, index, tilesToShow, cycleMode); }, [items, isMultiPage, index, tilesToShow, cycleMode]); - const transitionBasis: string = isMultiPage && animated && isAnimationRunning ? `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); @@ -123,7 +125,7 @@ function TileDock({ setTransform(-100 + movement); // If this is an animated slider, start the animation 'slide' - if (animated) { + if (isAnimated) { setIsAnimationRunning(true); } // If not anmiated, trigger the post animation code right away @@ -131,7 +133,7 @@ function TileDock({ setAnimationDone(true); } }, - [animated, cycleMode, index, items.length, tileWidth, tilesToShow, isAnimationRunning], + [isAnimated, cycleMode, index, items.length, tileWidth, tilesToShow, isAnimationRunning], ); const handleTouchStart = useCallback( From 9bed444a3ce66daad52789243fa1acd3303996ce Mon Sep 17 00:00:00 2001 From: Danny Budzinski Date: Fri, 19 Jan 2024 12:02:36 +0100 Subject: [PATCH 3/3] chore: rename variables for consistency --- src/components/TileDock/TileDock.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/TileDock/TileDock.tsx b/src/components/TileDock/TileDock.tsx index a95ea47df..885501148 100644 --- a/src/components/TileDock/TileDock.tsx +++ b/src/components/TileDock/TileDock.tsx @@ -16,7 +16,7 @@ export type TileDockProps = { minimalTouchMovement?: number; showControls?: boolean; showDots?: boolean; - animatedOverride?: boolean; + animationModeOverride?: boolean; wrapWithEmptyTiles?: boolean; transitionTime?: string; renderTile: (item: T, isInView: boolean) => ReactNode; @@ -66,7 +66,7 @@ function TileDock({ spacing = 12, minimalTouchMovement = 30, showControls = true, - animatedOverride, + animationModeOverride, transitionTime = '0.6s', wrapWithEmptyTiles = false, showDots = false, @@ -79,8 +79,8 @@ function TileDock({ const [slideToIndex, setSlideToIndex] = useState(0); const [transform, setTransform] = useState(-100); // Prevent animation mode from changing after first load - const [isAnimated] = useState(animatedOverride ?? !window.matchMedia('(prefers-reduced-motion)').matches); - const [animationDone, setAnimationDone] = useState(false); + 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; @@ -130,7 +130,7 @@ function TileDock({ } // If not anmiated, trigger the post animation code right away else { - setAnimationDone(true); + setIsAnimationDone(true); } }, [isAnimated, cycleMode, index, items.length, tileWidth, tilesToShow, isAnimationRunning], @@ -206,17 +206,17 @@ function TileDock({ setIndex(resetIndex); setTransform(-100); setIsAnimationRunning(false); - setAnimationDone(false); + setIsAnimationDone(false); }; - if (animationDone) { + if (isAnimationDone) { postAnimationCleanup(); } - }, [animationDone, index, items.length, slideToIndex, tileWidth, tilesToShow, transitionBasis]); + }, [isAnimationDone, index, items.length, slideToIndex, tileWidth, tilesToShow, transitionBasis]); const handleTransitionEnd = (event: React.TransitionEvent) => { if (event.target === frameRef.current) { - setAnimationDone(true); + setIsAnimationDone(true); } };