@atlaskit/motion
Version:
A set of utilities to apply motion in your application.
199 lines (190 loc) • 6.73 kB
JavaScript
import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
import React, { Children, createContext, memo, useContext, useEffect, useState } from 'react';
/**
* Internally we will be playing with an element that will always have a key defined.
*/
/**
* Internal data passed to child motions.
*/
// We define empty context here so the object doesn't change.
var emptyContext = {
// Motions will always appear if not inside a exiting persistence component.
appear: true,
isExiting: false
};
/**
* __Exiting context__
*
* An exiting context.
*/
var ExitingContext = /*#__PURE__*/createContext(emptyContext);
/**
* This method will wrap any React element with a context provider. We're using context (instead of
* cloneElement) so we can communicate between multiple elements without the need of prop drilling
* (results in a better API for consumers).
*/
var wrapChildWithContextProvider = function wrapChildWithContextProvider(child) {
var value = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : emptyContext;
return /*#__PURE__*/React.createElement(ExitingContext.Provider, {
key: "".concat(child.key, "-provider"),
value: value
}, child);
};
/**
* This function will convert all children types to an array while also filtering out non-valid React elements.
*/
var childrenToArray = function childrenToArray(children) {
var childrenAsArray = [];
// We convert children to an array using this helper method as it will add keys to children that do not
// have them, such as when we have hardcoded children that are conditionally rendered.
Children.toArray(children).forEach(function (child) {
// We ignore any boolean children to make our code a little more simple later on,
// and also filter out any falsies (empty strings, nulls, and undefined).
if (typeof child !== 'boolean' && Boolean(child)) {
// Children WILL have a key after being forced into an array using the React.Children helper.
childrenAsArray.push(child);
}
});
return childrenAsArray;
};
var spliceNewElementsIntoPrevious = function spliceNewElementsIntoPrevious(current, previous) {
var splicedChildren = previous.concat([]);
var previousMap = childrenToObj(previous);
for (var i = 0; i < current.length; i++) {
var child = current[i];
var childIsNew = !previousMap[child.key];
if (childIsNew) {
// This will insert the new element after the previous element.
splicedChildren.splice(i + 1, 0, child);
}
}
return splicedChildren;
};
var childrenToObj = function childrenToObj(children) {
return children.reduce(function (acc, child) {
acc[child.key] = child;
return acc;
}, {});
};
var getMissingKeys = function getMissingKeys(current, previous) {
var currentMapKeys = new Set(current.map(function (child) {
return child.key;
}));
var missing = new Set();
for (var i = 0; i < previous.length; i++) {
var element = previous[i];
var _key = element.key;
if (!currentMapKeys.has(_key)) {
missing.add(_key);
}
}
return missing;
};
/**
* How does this component work?
*
* It looks at changes in its children to see what is removed.
*
* If a child is removed it clones it and wraps it with context providing an `onFinish` callback.
*
* The cloned child will call the `onFinish` when it finishes its exit animation,
* which lets `ExitingPersistence` know to stop rendering it.
*/
/**
* __ExitingPersistence__
*
* Useful for enabling elements to persist and animate away when they are removed from the DOM.
*
* - [Examples](https://atlaskit.atlassian.com/packages/design-system/motion/docs/entering-motions)
*/
var ExitingPersistence = /*#__PURE__*/memo(function (_ref) {
var _ref$appear = _ref.appear,
appear = _ref$appear === void 0 ? false : _ref$appear,
children = _ref.children,
exitThenEnter = _ref.exitThenEnter;
var _useState = useState([null, children]),
_useState2 = _slicedToArray(_useState, 2),
stateChildren = _useState2[0],
setChildren = _useState2[1];
var _useState3 = useState([]),
_useState4 = _slicedToArray(_useState3, 2),
exitingChildren = _useState4[0],
setExitingChildren = _useState4[1];
var _useState5 = useState(function () {
return {
appear: appear,
isExiting: false
};
}),
_useState6 = _slicedToArray(_useState5, 2),
defaultContext = _useState6[0],
setDefaultContext = _useState6[1];
useEffect(function () {
if (!defaultContext.appear) {
setDefaultContext({
appear: true,
isExiting: false
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
/**
* NOTE: This is a workaround for the test case written in Jira where the stateChildren is a boolean value because
* useState is mocked to return a boolean value.
*/
if (typeof stateChildren === 'boolean') {
return children;
}
var _stateChildren = _slicedToArray(stateChildren, 2),
previousChildren = _stateChildren[0],
currentChildren = _stateChildren[1];
var previous = childrenToArray(previousChildren);
var current = childrenToArray(currentChildren);
if (currentChildren !== children) {
setChildren([currentChildren, children]);
}
var missingKeys = getMissingKeys(current, previous);
var isSomeChildRemoved = !!missingKeys.size;
var visibleChildren = current;
if (isSomeChildRemoved) {
visibleChildren = spliceNewElementsIntoPrevious(current, previous);
}
if (exitThenEnter) {
if (exitingChildren.length) {
visibleChildren = exitingChildren;
} else {
var nextExitingChildren = visibleChildren.filter(function (child) {
return missingKeys.has(child.key);
});
if (nextExitingChildren.length) {
setExitingChildren(nextExitingChildren);
}
}
}
if (missingKeys.size) {
visibleChildren = visibleChildren.map(function (child) {
var isExiting = missingKeys.has(child.key);
return wrapChildWithContextProvider(child, {
appear: true,
isExiting: isExiting,
onFinish: isExiting ? function () {
missingKeys.delete(child.key);
if (missingKeys.size === 0) {
setChildren([null, children]);
setExitingChildren([]);
}
} : undefined
});
});
} else {
visibleChildren = visibleChildren.map(function (child) {
return wrapChildWithContextProvider(child, defaultContext);
});
}
return visibleChildren;
});
export var useExitingPersistence = function useExitingPersistence() {
return useContext(ExitingContext);
};
ExitingPersistence.displayName = 'ExitingPersistence';
export default ExitingPersistence;