framer-motion
Version:
A simple and powerful JavaScript animation library
223 lines (220 loc) • 9.81 kB
JavaScript
"use client";
import { jsx } from 'react/jsx-runtime';
import { invariant, moveItem } from 'motion-utils';
import { forwardRef, useContext, useRef, useState, useEffect } from 'react';
import { ReorderBoardContext } from '../../context/ReorderBoardContext.mjs';
import { ReorderContext } from '../../context/ReorderContext.mjs';
import { useConstant } from '../../utils/use-constant.mjs';
import { useIsomorphicLayoutEffect } from '../../utils/use-isomorphic-effect.mjs';
import { checkReorder, keyTarget } from './utils/check-reorder.mjs';
import { detectAxis } from './utils/detect-axis.mjs';
import { mergeRefs } from './utils/merge-refs.mjs';
function ReorderGroupComponent({ children, as = "ul", axis: axisOverride, onReorder, values, onPointerDownCapture, onClickCapture, ...props }, externalRef, proxy) {
const Component = useConstant(() => proxy[as]);
const board = useContext(ReorderBoardContext);
const items = useRef(new Map());
const elements = useRef(new Map());
const [active, setActive] = useState();
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);
for (const map of [items.current, elements.current]) {
map.forEach((_, value) => valuesSet.has(value) || map.delete(value));
}
const tabStop = valuesSet.has(active) ? active : values[0];
const isRendered = (value) => elements.current.get(value)?.current?.isConnected;
/**
* Only values in `values` can be the tab stop.
*/
const activate = (value) => valuesSet.has(value) && setActive(value);
/**
* If the tab stop isn't rendered (e.g. it's virtualized), the first
* rendered item takes its place. Items check again when they unmount.
*/
const checkTabStop = () => {
isRendered(tabStop) || activate(values.find(isRendered));
};
useEffect(checkTabStop);
const getOrder = () => values.flatMap((value) => items.current.get(value) || []);
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) => items.current.has(value) ? newOrder[i++].value : value);
newValues.some((value, j) => value !== values[j]) &&
onReorder(newValues, moved);
};
const context = {
axis,
proxy,
groupRef,
hasReordered,
values,
onReorder,
getOrder,
isTabStop: (value) => value === tabStop || !valuesSet.has(value),
setActive: activate,
checkTabStop,
elements: elements.current,
/**
* Space grabs and drops the item, arrows along `axis` move
* the grabbed item one place, or a row in a grid (see keyTarget),
* or move focus that way if it isn't grabbed. Home and End go to
* either end. Escape restores the order in `from`, the values when
* it was grabbed. Keys from children or with modifiers (Alt+Left
* etc.) are left alone.
*
* In a Reorder.Board, arrows across `axis`, or along it from either
* end, move a grabbed item to the nearest group that way, and
* Escape returns it to the group it was grabbed in.
*/
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",
"Home",
"End",
].indexOf(key);
const isX = keyIndex % 2;
if (key === " ") {
event.repeat || grab(currentTarget, from ? undefined : values);
}
else if (keyIndex < 0 && !(key === "Escape" && from)) {
return;
}
else if (board?.isMoving(currentTarget)) ;
else if (key === "Escape" && from) {
/**
* Drop it first: a move back to the group it was grabbed in
* hands it on to the item that mounts there.
*/
grab(currentTarget);
if (!board?.restore(context, item, currentTarget, from)) {
/**
* 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);
}
}
else {
const isAlongAxis = keyIndex > 3 || axis === "xy" || axis === (isX ? "x" : "y");
/**
* Focus only moves between rendered items.
*/
const list = from
? order
: order.filter(({ value }) => isRendered(value));
const index = list.findIndex(({ value }) => value === item);
const target = isAlongAxis
? keyTarget(list, index, keyIndex, axis, !!isX && isRTL())
: -1;
if (target > -1) {
from
? commit(moveItem(order, index, target), item)
: elements.current
.get(list[target].value)
?.current?.focus();
}
else if (!(from &&
keyIndex < 4 &&
board?.key(context, item, keyIndex, currentTarget, from)) &&
!isAlongAxis) {
return;
}
}
event.preventDefault();
},
registerItem: (value, layout, ref) => {
items.current.set(value, { value, layout, ref });
if (!axisOverride) {
const nextAxis = detectAxis(getOrder().map((data) => data.layout));
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]);
/**
* Keep a Reorder.Board up to date with the committed values.
*/
useIsomorphicLayoutEffect(() => board?.add(context));
/**
* 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: mergeRefs(groupRef, externalRef),
/**
* 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