UNPKG

framer-motion

Version:

A simple and powerful JavaScript animation library

180 lines (177 loc) • 7.61 kB
"use client"; import { jsx } from 'react/jsx-runtime'; import { invariant, moveItem } from 'motion-utils'; import { forwardRef, useRef, useState, useEffect } from 'react'; import { ReorderContext } from '../../context/ReorderContext.mjs'; import { useConstant } from '../../utils/use-constant.mjs'; import { checkReorder } from './utils/check-reorder.mjs'; import { detectAxis } from './utils/detect-axis.mjs'; function ReorderGroupComponent({ children, as = "ul", axis: axisOverride, onReorder, values, onPointerDownCapture, onClickCapture, ...props }, externalRef, proxy) { const Component = useConstant(() => proxy[as]); const itemLayouts = useRef(new Map()); const [detectedAxis, setDetectedAxis] = useState("y"); const isReordering = useRef(false); const hasReordered = useRef(false); const groupRef = useRef(null); const axis = axisOverride || detectedAxis; process.env.NODE_ENV !== "production" && invariant(Boolean(values), "Reorder.Group must be provided a values prop", "reorder-values"); const valuesSet = new Set(values); itemLayouts.current.forEach((_, value) => { if (!valuesSet.has(value)) itemLayouts.current.delete(value); }); const getOrder = () => values.flatMap((value) => { const layout = itemLayouts.current.get(value); return layout ? [{ value, layout }] : []; }); const isRTL = () => groupRef.current?.ownerDocument.defaultView?.getComputedStyle(groupRef.current).direction === "rtl"; /** * Write the new order of the measured items back into the slots they * hold in `values`, leaving unmeasured (e.g. virtualized) values in place. */ const commit = (newOrder, moved) => { let i = 0; const newValues = values.map((value) => itemLayouts.current.has(value) ? newOrder[i++].value : value); newValues.some((value, j) => value !== values[j]) && onReorder(newValues, moved); }; const context = { axis, proxy, groupRef, hasReordered, /** * Space grabs and drops the item, arrows along `axis` move * the grabbed item one place (Left and Right swap in RTL) and Escape * restores the order in `from`, the values when it was grabbed. Keys * from children or with modifiers (Alt+Left etc.) are left alone. */ onKeyDown: (event, item, from, grab) => { const { key, currentTarget } = event; if (event.defaultPrevented || event.target !== currentTarget || event.altKey || event.ctrlKey || event.metaKey) { return; } const order = getOrder(); const keyIndex = [ "ArrowUp", "ArrowLeft", "ArrowDown", "ArrowRight", ].indexOf(key); const isX = keyIndex % 2; if (key === " ") { event.repeat || grab(currentTarget, from ? undefined : values); } else if (!from) { return; } else if (key === "Escape") { /** * Values added since the grab keep their slots. */ const kept = from.flatMap((value) => order.filter((data) => data.value === value)); commit(order.map((data) => from.includes(data.value) ? kept.shift() : data), item); grab(currentTarget); } else if (keyIndex < 0 || (axis !== "xy" && axis !== (isX ? "x" : "y"))) { return; } else { const index = order.findIndex(({ value }) => value === item); const target = index + (keyIndex > 1 !== !!(isX && isRTL()) ? 1 : -1); index > -1 && order[target] && commit(moveItem(order, index, target), item); } event.preventDefault(); }, registerItem: (value, layout) => { itemLayouts.current.set(value, layout); if (!axisOverride) { const nextAxis = detectAxis(values.flatMap((itemValue) => { const itemLayout = itemLayouts.current.get(itemValue); return itemLayout ? [itemLayout] : []; })); if (nextAxis !== detectedAxis) setDetectedAxis(nextAxis); } }, updateOrder: (item, offset, velocity) => { if (isReordering.current) return; const order = getOrder(); const newOrder = checkReorder(order, item, offset, velocity, axis, isRTL() ? "rtl" : "ltr"); if (order !== newOrder) { isReordering.current = true; hasReordered.current = true; commit(newOrder, item); } }, }; /** * Only clear the guard once the new order has been rendered. Unrelated * commits (useTransition's isPending, stale renders in front of * useDeferredValue) would otherwise re-enable onReorder while the * previous order is still pending. */ useEffect(() => { isReordering.current = false; }, [values]); // Combine refs if external ref is provided const setRef = (element) => { groupRef.current = element; if (typeof externalRef === "function") { externalRef(element); } else if (externalRef) { externalRef.current = element; } }; /** * Disable browser scroll anchoring on the group container. * When items reorder, scroll anchoring can cause the browser to adjust * the scroll position, which interferes with drag position calculations. */ const groupStyle = { overflowAnchor: "none", ...props.style, }; return (jsx(Component, { ...props, style: groupStyle, ref: setRef, /** * A release that ends a reordering drag isn't a click. Browsers * only click an element holding both the press and release * targets, so whether it was clicked depended on which nodes * React moved, and so on drag direction (#2682): Chrome drops * the click if the dragged item's node moved, others may click * an ancestor. Swallow it here, above every item, either way. * The flag is cleared when the drag ends, a frame after the * release's click, or on the next press. Clicks with no * pointer press behind them (detail 0, e.g. keyboard) are left * alone. */ onPointerDownCapture: (event) => { hasReordered.current = false; onPointerDownCapture?.(event); }, onClickCapture: (event) => { if (hasReordered.current && event.detail) { hasReordered.current = false; event.stopPropagation(); event.preventDefault(); } onClickCapture?.(event); }, ignoreStrict: true, children: jsx(ReorderContext.Provider, { value: context, children: children }) })); } /** * Creates `Reorder.Group` rendering the components of `proxy`: `motion`, * or `m` to get its features from `LazyMotion`. Its items render the same. */ const createReorderGroup = (proxy) => forwardRef((props, ref) => ReorderGroupComponent(props, ref, proxy)); export { ReorderGroupComponent, createReorderGroup }; //# sourceMappingURL=Group.mjs.map