@atlaskit/motion
Version:
A set of utilities to apply motion in your application.
158 lines (151 loc) • 5.73 kB
JavaScript
;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.useResizing = void 0;
var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
var _react = require("react");
var _isReducedMotion = require("../utils/is-reduced-motion");
var _useElementRef3 = require("../utils/use-element-ref");
var _useLayoutEffect = require("../utils/use-layout-effect");
var _useRequestAnimationFrame = require("../utils/use-request-animation-frame");
var _useSetTimeout = require("../utils/use-set-timeout");
var _useSnapshotBeforeUpdate = require("../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.
*/
var useResizing = exports.useResizing = function useResizing(_ref) {
var dimension = _ref.dimension,
duration = _ref.duration,
easing = _ref.easing,
onFinishMotion = _ref.onFinishMotion;
var prevDimensions = (0, _react.useRef)();
var _useElementRef = (0, _useElementRef3.useElementRef)(),
_useElementRef2 = (0, _slicedToArray2.default)(_useElementRef, 2),
element = _useElementRef2[0],
setElementRef = _useElementRef2[1];
var setTimeout = (0, _useSetTimeout.useSetTimeout)({
cleanup: 'next-effect'
});
var requestAnimationFrame = (0, _useRequestAnimationFrame.useRequestAnimationFrame)();
(0, _useSnapshotBeforeUpdate.useSnapshotBeforeUpdate)(function () {
if ((0, _isReducedMotion.isReducedMotion)() || !element) {
return;
}
var rect = element.getBoundingClientRect();
prevDimensions.current = {
width: rect.width,
height: rect.height
};
});
(0, _useLayoutEffect.useLayoutEffect)(function () {
if ((0, _isReducedMotion.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
};
};