Mirror: React hooks for accessible, common web interactions. UI super powers without the UI.

Synchronise animating boolean flag in useTransition with state change

Changed files
+32 -33
src
+32 -33
src/useTransition.ts
··· 2 2 import { useState, useCallback } from 'react'; 3 3 import { useLayoutEffect } from './utils/react'; 4 4 5 - interface AnimationState { 6 - animation: Animation; 7 - to: Keyframe; 8 - } 9 - 10 - const animations = new WeakMap<HTMLElement, AnimationState>(); 5 + const animations = new WeakMap<HTMLElement, Animation>(); 11 6 12 7 export interface TransitionOptions { 13 - style: Style; 8 + style?: Style | null; 14 9 duration?: number | string; 15 10 easing?: string | [number, number, number, number]; 16 11 } 17 12 18 13 const animate = (element: HTMLElement, options: TransitionOptions) => { 19 - const prevState = animations.get(element); 20 - const prevTo = prevState ? prevState.to : {}; 14 + const style = options.style || {}; 21 15 const computed = getComputedStyle(element); 22 16 const from: Keyframe = {}; 23 17 const to: Keyframe = {}; 24 18 25 - let changed = !prevState; 26 - for (const propName in options.style) { 27 - let value: string = options.style[propName]; 19 + for (const propName in style) { 20 + let value: string = style[propName]; 28 21 if (typeof value === 'number') (value as string) += 'px'; 29 22 30 23 let key: string; 31 24 if (/^--/.test(propName)) { 32 25 key = propName; 33 26 from[key] = element.style.getPropertyValue(propName); 34 - element.style.setProperty(key, (to[key] = options.style[propName])); 27 + element.style.setProperty(key, (to[key] = value)); 35 28 } else { 36 - if (propName === 'float') { 37 - key = 'cssFloat'; 38 - } else if (propName === 'offset') { 39 - key = 'cssOffset'; 40 - } else if (propName === 'transform') { 29 + if (propName === 'transform') { 41 30 key = propName; 42 31 value = 43 32 ('' + value || '').replace(/\w+\((?:0\w*\s*)+\)\s*/g, '') || 'none'; ··· 48 37 from[key] = computed[key]; 49 38 element.style[key] = to[key] = value; 50 39 } 51 - 52 - changed = changed || prevState!.to[key] !== to[key]; 53 40 } 54 - 55 - if (!changed && Object.keys(to).length === Object.keys(prevTo).length) return; 56 41 57 42 const effect: KeyframeEffectOptions = { 58 43 duration: 59 44 typeof options.duration === 'number' 60 45 ? options.duration * 1000 61 - : options.duration, 46 + : options.duration || 1000, 62 47 easing: Array.isArray(options.easing) 63 48 ? `cubic-bezier(${options.easing.join(', ')})` 64 - : options.easing, 49 + : options.easing || 'ease', 65 50 }; 66 51 67 - if (prevState) prevState.animation.cancel(); 52 + const prevAnimation = animations.get(element); 53 + if (prevAnimation) prevAnimation.cancel(); 68 54 69 55 const animation = element.animate([from, to], effect); 70 56 animation.playbackRate = 1.000001; ··· 88 74 } 89 75 90 76 return new Promise<unknown>((resolve, reject) => { 91 - animations.set(element, { animation, to }); 77 + animations.set(element, animation); 92 78 animation.addEventListener('cancel', reject); 93 79 animation.addEventListener('finish', resolve); 94 80 }); ··· 96 82 97 83 export function useTransition<T extends HTMLElement>( 98 84 ref: Ref<T>, 99 - options: TransitionOptions 85 + options?: TransitionOptions 100 86 ): [boolean, (options: TransitionOptions) => Promise<void>] { 101 - const [animating, setAnimating] = useState(false); 87 + if (!options) options = {}; 88 + 89 + const style = options.style || {}; 90 + const [state, setState] = useState<[boolean, Style]>([false, style]); 91 + if (JSON.stringify(style) !== JSON.stringify(state[1])) { 92 + setState([true, style]); 93 + } 102 94 103 95 const animateTo = useCallback( 104 96 (options: TransitionOptions) => { 97 + const updateAnimating = (animating: boolean) => { 98 + setState(state => 99 + state[0] !== animating ? [animating, state[1]] : state 100 + ); 101 + }; 102 + 105 103 const animation = animate(ref.current!, options); 106 104 if (animation) { 107 - setAnimating(true); 105 + updateAnimating(true); 108 106 return animation 109 107 .then(() => { 110 - setAnimating(false); 108 + updateAnimating(false); 111 109 }) 112 110 .catch(() => {}); 113 111 } else { 112 + updateAnimating(false); 114 113 return Promise.resolve(); 115 114 } 116 115 }, ··· 118 117 ); 119 118 120 119 useLayoutEffect(() => { 121 - animateTo(options); 122 - }, [animateTo, options.style]); 120 + animateTo(options!); 121 + }, [animateTo, state[1]]); 123 122 124 - return [animating, animateTo]; 123 + return [state[0], animateTo]; 125 124 }