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

Configure Feed

Select the types of activity you want to include in your feed.

Add useTransition hook

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