UNPKG

@atlaskit/motion

Version:

A set of utilities to apply motion in your application.

84 lines (82 loc) 3.57 kB
/* eslint-disable @repo/internal/deprecations/deprecation-ticket-required */ import { useRef } from 'react'; import { easeInOut } from '../utils/curves'; import { durations } from '../utils/durations'; 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'; /** * `useResizingHeight` animates height changes over state changes. If the height hasn't changed nothing will happen. * * __WARNING__: Potentially janky. This hook animates height which is * [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. * * See [examples](https://atlaskit.atlassian.com/packages/design-system/motion/docs/resizing-motions). * * @deprecated Use `useResizing` from `@atlaskit/motion/resizing` instead. Pass `dimension: 'height'` * to animate height changes. The new hook supports `'width'`, `'height'`, or `'both'`. */ export const useResizingHeight = ({ duration: calcDuration = () => durations.medium, timingFunction: calcTimingFunction = () => easeInOut } = {}) => { const prevDimensions = useRef(); const [element, setElementRef] = useElementRef(); // We cleanup on the next effect to prevent the previous timeout being called during // the next motion - as now the timeout has essentially been extended! const setTimeout = useSetTimeout({ cleanup: 'next-effect' }); const requestAnimationFrame = useRequestAnimationFrame(); useSnapshotBeforeUpdate(() => { if (isReducedMotion() || !element) { return; } prevDimensions.current = element.getBoundingClientRect(); }); useLayoutEffect(() => { if (isReducedMotion() || !element || !prevDimensions.current) { return; } // We might already be animating. // Because of that we need to expand to the destination height first. element.setAttribute('style', ''); const nextDimensions = element.getBoundingClientRect(); if (nextDimensions.height === prevDimensions.current.height) { return; } const duration = calcDuration(prevDimensions.current.height, nextDimensions.height); const newStyles = { height: `${prevDimensions.current.height}px`, willChange: 'height', transitionProperty: 'height', transitionDuration: `${duration}ms`, boxSizing: 'border-box', transitionTimingFunction: calcTimingFunction(prevDimensions.current.height, nextDimensions.height, duration) }; Object.assign(element.style, newStyles); // 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; } element.style.height = `${nextDimensions.height}px`; setTimeout(() => { if (!element) { return; } element.setAttribute('style', ''); }, duration); }); }); }); return { ref: setElementRef }; };