UNPKG

@atlaskit/motion

Version:

A set of utilities to apply motion in your application.

200 lines (191 loc) 6.83 kB
import _slicedToArray from "@babel/runtime/helpers/slicedToArray"; import React, { Children, createContext, memo, useContext, useEffect, useState } from 'react'; import { fg } from '@atlaskit/platform-feature-flags'; /** * 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-motion) */ 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 = fg('platform-dst-motion-uplift') ? previous : 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;