UNPKG

@atlaskit/motion

Version:

A set of utilities to apply motion in your application.

149 lines (142 loc) 4.98 kB
import { useRef } from 'react'; import { isReducedMotion } from '../utils/is-reduced-motion'; import { useElementRef } from '../utils/use-element-ref'; import { useLayoutEffect } from '../utils/use-layout-effect'; import { useRequestAnimationFrame } from '../utils/use-request-animation-frame'; import { useSetTimeout } from '../utils/use-set-timeout'; import { useSnapshotBeforeUpdate } from '../utils/use-snapshot-before-update'; /** * Which dimension(s) of the element to animate. * * - `'width'` animates only width changes. * - `'height'` animates only height changes. * - `'both'` animates width and height changes simultaneously. */ /** * Parses a CSS time value (e.g. `"0.2s"` or `"200ms"`) and returns milliseconds. * Falls back to 0 if the value cannot be parsed. */ function parseCSSTimeToMs(value) { const parsed = parseFloat(value); if (Number.isNaN(parsed)) { return 0; } if (value.endsWith('ms')) { return parsed; } return parsed * 1000; } /** * Returns `true` if any of the dimension(s) being animated have changed * between the previous and next measurements. */ function hasDimensionChanged(dimension, prev, next) { if (dimension === 'width') { return prev.width !== next.width; } if (dimension === 'height') { return prev.height !== next.height; } return prev.width !== next.width || prev.height !== next.height; } /** * Returns the CSS `transition-property` / `will-change` value for the * given dimension(s). */ function getDimensionPropertyList(dimension) { if (dimension === 'both') { return 'width, height'; } return dimension; } /** * `useResizing` animates dimension changes (width, height, or both) over state changes. * If the relevant dimension(s) haven't changed nothing will happen. * * Pass `dimension: 'width' | 'height' | 'both'` to choose which axis (or axes) to animate. * * __WARNING__: Potentially janky. This hook animates layout-affecting properties which are * [notoriously unperformant](https://firefox-source-docs.mozilla.org/performance/bestpractices.html#Get_familiar_with_the_pipeline_that_gets_pixels_to_the_screen). * Test your app over low powered devices, you may want to avoid this if you can see it impacting FPS. */ export const useResizing = ({ dimension, duration, easing, onFinishMotion }) => { const prevDimensions = useRef(); const [element, setElementRef] = useElementRef(); const setTimeout = useSetTimeout({ cleanup: 'next-effect' }); const requestAnimationFrame = useRequestAnimationFrame(); useSnapshotBeforeUpdate(() => { if (isReducedMotion() || !element) { return; } const rect = element.getBoundingClientRect(); prevDimensions.current = { width: rect.width, height: rect.height }; }); useLayoutEffect(() => { if (isReducedMotion() || !element || !prevDimensions.current) { return; } // We might already be animating. // Because of that we need to expand to the destination dimensions first. element.setAttribute('style', ''); const rect = element.getBoundingClientRect(); const nextDimensions = { width: rect.width, height: rect.height }; if (!hasDimensionChanged(dimension, prevDimensions.current, nextDimensions)) { onFinishMotion === null || onFinishMotion === void 0 ? void 0 : onFinishMotion(); return; } const propertyList = getDimensionPropertyList(dimension); const newStyles = { willChange: propertyList, transitionProperty: propertyList, transitionDuration: duration, boxSizing: 'border-box', transitionTimingFunction: easing }; if (dimension === 'width' || dimension === 'both') { newStyles.width = `${prevDimensions.current.width}px`; } if (dimension === 'height' || dimension === 'both') { newStyles.height = `${prevDimensions.current.height}px`; } Object.assign(element.style, newStyles); const resolvedDuration = parseCSSTimeToMs(getComputedStyle(element).transitionDuration); // We split this over two animation frames so the DOM has enough time to flush the changes. // We are deliberately not skipping this frame if another render happens - if we do the motion doesn't finish properly. requestAnimationFrame(() => { requestAnimationFrame(() => { if (!element) { return; } if (dimension === 'width' || dimension === 'both') { element.style.width = `${nextDimensions.width}px`; } if (dimension === 'height' || dimension === 'both') { element.style.height = `${nextDimensions.height}px`; } setTimeout(() => { if (!element) { return; } element.setAttribute('style', ''); onFinishMotion === null || onFinishMotion === void 0 ? void 0 : onFinishMotion(); }, resolvedDuration); }); }); }); return { ref: setElementRef }; };