UNPKG

@atlaskit/motion

Version:

A set of utilities to apply motion in your application.

152 lines (145 loc) 5.29 kB
import _slicedToArray from "@babel/runtime/helpers/slicedToArray"; 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) { var 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 var useResizing = function useResizing(_ref) { var dimension = _ref.dimension, duration = _ref.duration, easing = _ref.easing, onFinishMotion = _ref.onFinishMotion; var prevDimensions = useRef(); var _useElementRef = useElementRef(), _useElementRef2 = _slicedToArray(_useElementRef, 2), element = _useElementRef2[0], setElementRef = _useElementRef2[1]; var setTimeout = useSetTimeout({ cleanup: 'next-effect' }); var requestAnimationFrame = useRequestAnimationFrame(); useSnapshotBeforeUpdate(function () { if (isReducedMotion() || !element) { return; } var rect = element.getBoundingClientRect(); prevDimensions.current = { width: rect.width, height: rect.height }; }); useLayoutEffect(function () { 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', ''); var rect = element.getBoundingClientRect(); var nextDimensions = { width: rect.width, height: rect.height }; if (!hasDimensionChanged(dimension, prevDimensions.current, nextDimensions)) { onFinishMotion === null || onFinishMotion === void 0 || onFinishMotion(); return; } var propertyList = getDimensionPropertyList(dimension); var newStyles = { willChange: propertyList, transitionProperty: propertyList, transitionDuration: duration, boxSizing: 'border-box', transitionTimingFunction: easing }; if (dimension === 'width' || dimension === 'both') { newStyles.width = "".concat(prevDimensions.current.width, "px"); } if (dimension === 'height' || dimension === 'both') { newStyles.height = "".concat(prevDimensions.current.height, "px"); } Object.assign(element.style, newStyles); var 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(function () { requestAnimationFrame(function () { if (!element) { return; } if (dimension === 'width' || dimension === 'both') { element.style.width = "".concat(nextDimensions.width, "px"); } if (dimension === 'height' || dimension === 'both') { element.style.height = "".concat(nextDimensions.height, "px"); } setTimeout(function () { if (!element) { return; } element.setAttribute('style', ''); onFinishMotion === null || onFinishMotion === void 0 || onFinishMotion(); }, resolvedDuration); }); }); }); return { ref: setElementRef }; };