UNPKG

@vimeo/iris

Version:
427 lines (415 loc) 15.8 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var tslib_es6 = require('../../../tslib.es6-3ec409b7.js'); var React = require('react'); var useId = require('../../../use-id-29bc1b38.js'); var utils_general_throttle = require('../../general/throttle.js'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } function _interopNamespace(e) { if (e && e.__esModule) return e; var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n["default"] = e; return Object.freeze(n); } var React__namespace = /*#__PURE__*/_interopNamespace(React); var React__default = /*#__PURE__*/_interopDefaultLegacy(React); function useIsMounted() { var isMounted = React.useRef(false); useId.useIsomorphicLayoutEffect(function () { isMounted.current = true; return function () { isMounted.current = false; }; }, []); return isMounted; } function useForceUpdate() { var isMounted = useIsMounted(); var _a = tslib_es6.__read(React.useState(0), 2), forcedRenderCount = _a[0], setForcedRenderCount = _a[1]; var forceRender = React.useCallback(function () { isMounted.current && setForcedRenderCount(forcedRenderCount + 1); }, [forcedRenderCount]); /** * Defer this to the end of the next animation frame in case there are multiple * synchronous calls. */ var deferredForceRender = React.useCallback(function () { return useId.sync.postRender(forceRender); }, [forceRender]); return [deferredForceRender, forcedRenderCount]; } var PresenceChild = function (_a) { var children = _a.children, initial = _a.initial, isPresent = _a.isPresent, onExitComplete = _a.onExitComplete, custom = _a.custom, presenceAffectsLayout = _a.presenceAffectsLayout; var presenceChildren = useId.useConstant(newChildrenMap); var id = useId.useId(); var context = React.useMemo(function () { return { id: id, initial: initial, isPresent: isPresent, custom: custom, onExitComplete: function (childId) { var e_1, _a; presenceChildren.set(childId, true); try { for (var _b = tslib_es6.__values(presenceChildren.values()), _c = _b.next(); !_c.done; _c = _b.next()) { var isComplete = _c.value; if (!isComplete) return; // can stop searching when any is incomplete } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (_c && !_c.done && (_a = _b.return)) _a.call(_b); } finally { if (e_1) throw e_1.error; } } onExitComplete === null || onExitComplete === void 0 ? void 0 : onExitComplete(); }, register: function (childId) { presenceChildren.set(childId, false); return function () { return presenceChildren.delete(childId); }; } }; }, /** * If the presence of a child affects the layout of the components around it, * we want to make a new context value to ensure they get re-rendered * so they can detect that layout change. */ presenceAffectsLayout ? undefined : [isPresent]); React.useMemo(function () { presenceChildren.forEach(function (_, key) { return presenceChildren.set(key, false); }); }, [isPresent]); /** * If there's no `motion` components to fire exit animations, we want to remove this * component immediately. */ React__namespace.useEffect(function () { !isPresent && !presenceChildren.size && (onExitComplete === null || onExitComplete === void 0 ? void 0 : onExitComplete()); }, [isPresent]); return /*#__PURE__*/React__namespace.createElement(useId.PresenceContext.Provider, { value: context }, children); }; function newChildrenMap() { return new Map(); } var getChildKey = function (child) { return child.key || ""; }; function updateChildLookup(children, allChildren) { children.forEach(function (child) { var key = getChildKey(child); allChildren.set(key, child); }); } function onlyElements(children) { var filtered = []; // We use forEach here instead of map as map mutates the component key by preprending `.$` React.Children.forEach(children, function (child) { if ( /*#__PURE__*/React.isValidElement(child)) filtered.push(child); }); return filtered; } /** * `AnimatePresence` enables the animation of components that have been removed from the tree. * * When adding/removing more than a single child, every child **must** be given a unique `key` prop. * * Any `motion` components that have an `exit` property defined will animate out when removed from * the tree. * * ```jsx * import { motion, AnimatePresence } from 'framer-motion' * * export const Items = ({ items }) => ( * <AnimatePresence> * {items.map(item => ( * <motion.div * key={item.id} * initial={{ opacity: 0 }} * animate={{ opacity: 1 }} * exit={{ opacity: 0 }} * /> * ))} * </AnimatePresence> * ) * ``` * * You can sequence exit animations throughout a tree using variants. * * If a child contains multiple `motion` components with `exit` props, it will only unmount the child * once all `motion` components have finished animating out. Likewise, any components using * `usePresence` all need to call `safeToRemove`. * * @public */ var AnimatePresence = function (_a) { var children = _a.children, custom = _a.custom, _b = _a.initial, initial = _b === void 0 ? true : _b, onExitComplete = _a.onExitComplete, exitBeforeEnter = _a.exitBeforeEnter, _c = _a.presenceAffectsLayout, presenceAffectsLayout = _c === void 0 ? true : _c; // We want to force a re-render once all exiting animations have finished. We // either use a local forceRender function, or one from a parent context if it exists. var _d = tslib_es6.__read(useForceUpdate(), 1), forceRender = _d[0]; var forceRenderLayoutGroup = React.useContext(useId.LayoutGroupContext).forceRender; if (forceRenderLayoutGroup) forceRender = forceRenderLayoutGroup; var isMounted = useIsMounted(); // Filter out any children that aren't ReactElements. We can only track ReactElements with a props.key var filteredChildren = onlyElements(children); var childrenToRender = filteredChildren; var exiting = new Set(); // Keep a living record of the children we're actually rendering so we // can diff to figure out which are entering and exiting var presentChildren = React.useRef(childrenToRender); // A lookup table to quickly reference components by key var allChildren = React.useRef(new Map()).current; // If this is the initial component render, just deal with logic surrounding whether // we play onMount animations or not. var isInitialRender = React.useRef(true); useId.useIsomorphicLayoutEffect(function () { isInitialRender.current = false; updateChildLookup(filteredChildren, allChildren); presentChildren.current = childrenToRender; }); useId.useUnmountEffect(function () { isInitialRender.current = true; allChildren.clear(); exiting.clear(); }); if (isInitialRender.current) { return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, childrenToRender.map(function (child) { return /*#__PURE__*/React__namespace.createElement(PresenceChild, { key: getChildKey(child), isPresent: true, initial: initial ? undefined : false, presenceAffectsLayout: presenceAffectsLayout }, child); })); } // If this is a subsequent render, deal with entering and exiting children childrenToRender = tslib_es6.__spreadArray([], tslib_es6.__read(childrenToRender), false); // Diff the keys of the currently-present and target children to update our // exiting list. var presentKeys = presentChildren.current.map(getChildKey); var targetKeys = filteredChildren.map(getChildKey); // Diff the present children with our target children and mark those that are exiting var numPresent = presentKeys.length; for (var i = 0; i < numPresent; i++) { var key = presentKeys[i]; if (targetKeys.indexOf(key) === -1) { exiting.add(key); } } // If we currently have exiting children, and we're deferring rendering incoming children // until after all current children have exiting, empty the childrenToRender array if (exitBeforeEnter && exiting.size) { childrenToRender = []; } // Loop through all currently exiting components and clone them to overwrite `animate` // with any `exit` prop they might have defined. exiting.forEach(function (key) { // If this component is actually entering again, early return if (targetKeys.indexOf(key) !== -1) return; var child = allChildren.get(key); if (!child) return; var insertionIndex = presentKeys.indexOf(key); var onExit = function () { allChildren.delete(key); exiting.delete(key); // Remove this child from the present children var removeIndex = presentChildren.current.findIndex(function (presentChild) { return presentChild.key === key; }); presentChildren.current.splice(removeIndex, 1); // Defer re-rendering until all exiting children have indeed left if (!exiting.size) { presentChildren.current = filteredChildren; if (isMounted.current === false) return; forceRender(); onExitComplete && onExitComplete(); } }; childrenToRender.splice(insertionIndex, 0, /*#__PURE__*/React__namespace.createElement(PresenceChild, { key: getChildKey(child), isPresent: false, onExitComplete: onExit, custom: custom, presenceAffectsLayout: presenceAffectsLayout }, child)); }); // Add `MotionContext` even to children that don't need it to ensure we're rendering // the same tree between renders childrenToRender = childrenToRender.map(function (child) { var key = child.key; return exiting.has(key) ? child : /*#__PURE__*/React__namespace.createElement(PresenceChild, { key: getChildKey(child), isPresent: true, presenceAffectsLayout: presenceAffectsLayout }, child); }); if (useId.env !== "production" && exitBeforeEnter && childrenToRender.length > 1) { console.warn("You're attempting to animate multiple children within AnimatePresence, but its exitBeforeEnter prop is set to true. This will lead to odd visual behaviour."); } return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, exiting.size ? childrenToRender : childrenToRender.map(function (child) { return /*#__PURE__*/React.cloneElement(child); })); }; var useIsomorphicLayoutEffect = typeof window === 'undefined' || typeof document === 'undefined' ? React.useEffect : React.useLayoutEffect; function Anchor(_a) { var active = _a.active, children = _a.children, styleAnchor = _a.styleAnchor, styleChild = _a.styleChild, zIndex = _a.zIndex; return (React__default["default"].createElement(AnimatePresence, null, active && (React__default["default"].createElement("div", { style: tslib_es6.__assign(tslib_es6.__assign({ position: 'absolute' }, styleAnchor), { zIndex: zIndex }) }, React__default["default"].createElement("div", { style: tslib_es6.__assign({ position: 'absolute' }, styleChild) }, children))))); } function useAnchor(ref, //refAnchor, attach, active) { if (active === void 0) { active = false; } var _a = tslib_es6.__read(React.useState(new DOMRect()), 2), rect = _a[0], rectSet = _a[1]; var _b = tslib_es6.__read(attach.split('-'), 2), side = _b[0], placement = _b[1]; useIsomorphicLayoutEffect(function () { if (ref.current) { var updateRect_1 = function () { var rectRef = ref.current.getBoundingClientRect(); var width = rectRef.width, height = rectRef.height, top = rectRef.top, left = rectRef.left; if (rect.top !== top || rect.left !== left) { rectSet({ width: width, height: height, top: top, left: left }); } }; var resizeEventListener_1 = utils_general_throttle.throttle(function () { updateRect_1(); }, 10); window.addEventListener('resize', resizeEventListener_1); updateRect_1(); return function () { window.removeEventListener('resize', resizeEventListener_1); }; } }, [ref, rect]); var styleAnchor = rect; var translate = 'translate' + axis(side); var amount = 100 * orient(side); styleAnchor.transform = translate + '(' + amount + '%)'; var styleChild = {}; styleChild[side] = 0; styleChild[placement] = 0; if (!placement) { var translate_1 = 'translate' + flip(axis(side)); var amount_1 = 50 * orient(side) * -1; styleChild[placement] = null; styleChild[intersect(side)] = '50%'; styleChild.transform = translate_1 + '(' + amount_1 + '%)'; } return { active: active, styleAnchor: styleAnchor, styleChild: styleChild }; } function orient(side) { if (side === 'top' || side === 'left') return 1; if (side === 'right' || side === 'bottom') return -1; } function axis(position) { if (position === 'top' || position === 'bottom') return 'Y'; if (position === 'right' || position === 'left') return 'X'; } function intersect(position) { if (position === 'top') return 'left'; if (position === 'right') return 'bottom'; if (position === 'bottom') return 'right'; if (position === 'left') return 'top'; } function flip(position) { if (position === 'top') return 'bottom'; if (position === 'right') return 'left'; if (position === 'bottom') return 'top'; if (position === 'left') return 'right'; if (position === 'X') return 'Y'; if (position === 'Y') return 'X'; } // TODO // external scroll observer // // useIsomorphicLayoutEffect(() => { // if (!active) return; // function scrollObserve(event) { // console.log('scroll'); // if (ref.current) { // const rectRef: DOMRect = ref.current.getBoundingClientRect(); // const rectNew = rectRef; // console.log(rectRef); // // if (rect.top !== rectNew.top) rectSet(rectNew); // } // } // window.addEventListener('scroll', scrollObserve); // return () => window.removeEventListener('scroll', scrollObserve); // }, []); // TODO // child node observer // // When children change, the new height and width need to be // calculated and animated to. If this triggers an attachment // shift, that must also be factored into the animation. // useIsomorphicLayoutEffect(() => { // const config = { // attributes: true, // childList: true, // subtree: true, // }; // function onMutate(mutationsList, observer) { // if (mutationsList.length > 0) { // console.log({ mutationsList }); // } // } // const observer = new MutationObserver(onMutate); // if (refAnchor.current) { // observer.observe(refAnchor.current, config); // } // return () => observer.disconnect(); // }, []); exports.Anchor = Anchor; exports.useAnchor = useAnchor;