@vimeo/iris
Version:
Vimeo Design System
427 lines (415 loc) • 15.8 kB
JavaScript
;
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;