@atlaskit/motion
Version:
A set of utilities to apply motion in your application.
94 lines (91 loc) • 3.69 kB
JavaScript
import React, { Fragment, useRef } from 'react';
import { isReducedMotion } from '../utils/accessibility';
import { easeInOut } from '../utils/curves';
import { durations } from '../utils/durations';
import { useRequestAnimationFrame, useSetTimeout } from '../utils/timer-hooks';
import { useElementRef } from '../utils/use-element-ref';
import { useLayoutEffect } from '../utils/use-layout-effect';
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).
*/
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
};
};
/**
* __ResizingHeight__
*
* Component which consumes the useResizingHook() under-the-hood. Its props are the same as the hooks opts.
*
* See [examples](https://atlaskit.atlassian.com/packages/design-system/motion/docs/resizing-motions).
*/
export const ResizingHeight = ({
children,
...props
}) => {
const resizing = useResizingHeight(props);
return /*#__PURE__*/React.createElement(Fragment, null, children(resizing));
};