UNPKG

framer-motion

Version:

A simple and powerful JavaScript animation library

1 lines • 14.8 kB
{"version":3,"file":"Group.mjs","sources":["../../../../src/components/Reorder/Group.tsx"],"sourcesContent":["\"use client\"\n\nimport { invariant, moveItem } from \"motion-utils\"\nimport * as React from \"react\"\nimport {\n forwardRef,\n FunctionComponent,\n JSX,\n useContext,\n useEffect,\n useRef,\n useState,\n} from \"react\"\nimport { ReorderBoardContext } from \"../../context/ReorderBoardContext\"\nimport { ReorderContext } from \"../../context/ReorderContext\"\nimport type { MotionProxy } from \"../../render/components/create-proxy\"\nimport { HTMLMotionProps } from \"../../render/html/types\"\nimport { useConstant } from \"../../utils/use-constant\"\nimport { useIsomorphicLayoutEffect } from \"../../utils/use-isomorphic-effect\"\nimport {\n DefaultGroupElement,\n ItemData,\n ReorderAxis,\n ReorderContextProps,\n ReorderElementTag,\n} from \"./types\"\nimport { checkReorder, keyTarget } from \"./utils/check-reorder\"\nimport { detectAxis } from \"./utils/detect-axis\"\nimport { mergeRefs } from \"./utils/merge-refs\"\n\nexport interface Props<\n V,\n TagName extends ReorderElementTag = DefaultGroupElement\n> {\n /**\n * A HTML element to render this component as. Defaults to `\"ul\"`.\n *\n * @public\n */\n as?: TagName\n\n /**\n * The axis to reorder along. By default, this is detected from the item\n * layout. Use `\"xy\"` to explicitly enable wrapped layout reordering.\n *\n * @public\n */\n axis?: ReorderAxis\n\n /**\n * A callback to fire with the new value order. For instance, if the values\n * are provided as a state from `useState`, this could be the set state function.\n *\n * The second argument is the value of the item that moved. Its old and\n * new positions are `values.indexOf(moved)` and `newOrder.indexOf(moved)`.\n * This fires for each step of a drag, not once when it ends, so these\n * describe that step's move.\n *\n * @public\n */\n onReorder: (newOrder: V[], moved: V) => void\n\n /**\n * The latest values state.\n *\n * ```jsx\n * function Component() {\n * const [items, setItems] = useState([0, 1, 2])\n *\n * return (\n * <Reorder.Group values={items} onReorder={setItems}>\n * {items.map((item) => <Reorder.Item key={item} value={item} />)}\n * </Reorder.Group>\n * )\n * }\n * ```\n *\n * @public\n */\n values: V[]\n}\n\ntype ReorderGroupProps<\n V,\n TagName extends ReorderElementTag = DefaultGroupElement\n> = Props<V, TagName> &\n Omit<HTMLMotionProps<TagName>, \"values\"> &\n React.PropsWithChildren<{}>\n\nexport function ReorderGroupComponent<\n V,\n TagName extends ReorderElementTag = DefaultGroupElement\n>(\n {\n children,\n as = \"ul\" as TagName,\n axis: axisOverride,\n onReorder,\n values,\n onPointerDownCapture,\n onClickCapture,\n ...props\n }: ReorderGroupProps<V, TagName>,\n externalRef: React.ForwardedRef<any>,\n proxy: MotionProxy\n): JSX.Element {\n const Component = useConstant(\n () => proxy[as as keyof MotionProxy]\n ) as FunctionComponent<\n React.PropsWithChildren<HTMLMotionProps<any> & { ref?: React.Ref<any> }>\n >\n\n const board = useContext(ReorderBoardContext)\n const items = useRef(new Map<V, ItemData<V>>())\n const elements = useRef(new Map<V, { current: HTMLElement | null }>())\n const [active, setActive] = useState<V>()\n const [detectedAxis, setDetectedAxis] = useState<ReorderAxis>(\"y\")\n const isReordering = useRef(false)\n const hasReordered = useRef(false)\n const groupRef = useRef<Element>(null)\n const axis = axisOverride || detectedAxis\n\n process.env.NODE_ENV !== \"production\" &&\n invariant(\n Boolean(values),\n \"Reorder.Group must be provided a values prop\",\n \"reorder-values\"\n )\n\n const valuesSet = new Set(values)\n for (const map of [items.current, elements.current]) {\n map.forEach((_, value) => valuesSet.has(value) || map.delete(value))\n }\n\n const tabStop = valuesSet.has(active!) ? active : values[0]\n const isRendered = (value?: V) =>\n elements.current.get(value!)?.current?.isConnected\n\n /**\n * Only values in `values` can be the tab stop.\n */\n const activate = (value: V) => valuesSet.has(value) && setActive(value)\n\n /**\n * If the tab stop isn't rendered (e.g. it's virtualized), the first\n * rendered item takes its place. Items check again when they unmount.\n */\n const checkTabStop = () => {\n isRendered(tabStop) || activate(values.find(isRendered)!)\n }\n useEffect(checkTabStop)\n\n const getOrder = () =>\n values.flatMap((value) => items.current.get(value) || [])\n\n const isRTL = () =>\n groupRef.current?.ownerDocument.defaultView?.getComputedStyle(\n groupRef.current\n ).direction === \"rtl\"\n\n /**\n * Write the new order of the measured items back into the slots they\n * hold in `values`, leaving unmeasured (e.g. virtualized) values in place.\n */\n const commit = (newOrder: ItemData<V>[], moved: V) => {\n let i = 0\n const newValues = values.map((value) =>\n items.current.has(value) ? newOrder[i++].value : value\n )\n newValues.some((value, j) => value !== values[j]) &&\n onReorder(newValues, moved)\n }\n\n const context: ReorderContextProps<V> = {\n axis,\n proxy,\n groupRef,\n hasReordered,\n values,\n onReorder,\n getOrder,\n isTabStop: (value) => value === tabStop || !valuesSet.has(value),\n setActive: activate,\n checkTabStop,\n elements: elements.current,\n /**\n * Space grabs and drops the item, arrows along `axis` move\n * the grabbed item one place, or a row in a grid (see keyTarget),\n * or move focus that way if it isn't grabbed. Home and End go to\n * either end. Escape restores the order in `from`, the values when\n * it was grabbed. Keys from children or with modifiers (Alt+Left\n * etc.) are left alone.\n *\n * In a Reorder.Board, arrows across `axis`, or along it from either\n * end, move a grabbed item to the nearest group that way, and\n * Escape returns it to the group it was grabbed in.\n */\n onKeyDown: (event, item, from, grab) => {\n const { key, currentTarget } = event\n if (\n event.defaultPrevented ||\n event.target !== currentTarget ||\n event.altKey ||\n event.ctrlKey ||\n event.metaKey\n ) {\n return\n }\n\n const order = getOrder()\n const keyIndex = [\n \"ArrowUp\",\n \"ArrowLeft\",\n \"ArrowDown\",\n \"ArrowRight\",\n \"Home\",\n \"End\",\n ].indexOf(key)\n const isX = keyIndex % 2\n if (key === \" \") {\n event.repeat || grab(currentTarget, from ? undefined : values)\n } else if (keyIndex < 0 && !(key === \"Escape\" && from)) {\n return\n } else if (board?.isMoving(currentTarget)) {\n /**\n * Until React renders its move to another group, this item\n * and its group's values are out of date: moving it again,\n * or back, would leave it in both groups.\n */\n } else if (key === \"Escape\" && from) {\n /**\n * Drop it first: a move back to the group it was grabbed in\n * hands it on to the item that mounts there.\n */\n grab(currentTarget)\n if (!board?.restore(context, item, currentTarget, from)) {\n /**\n * Values added since the grab keep their slots.\n */\n const kept = from.flatMap((value) =>\n order.filter((data) => data.value === value)\n )\n commit(\n order.map((data) =>\n from.includes(data.value) ? kept.shift()! : data\n ),\n item\n )\n }\n } else {\n const isAlongAxis =\n keyIndex > 3 || axis === \"xy\" || axis === (isX ? \"x\" : \"y\")\n /**\n * Focus only moves between rendered items.\n */\n const list = from\n ? order\n : order.filter(({ value }) => isRendered(value))\n const index = list.findIndex(({ value }) => value === item)\n const target = isAlongAxis\n ? keyTarget(list, index, keyIndex, axis, !!isX && isRTL())\n : -1\n if (target > -1) {\n from\n ? commit(moveItem(order, index, target), item)\n : elements.current\n .get(list[target].value)\n ?.current?.focus()\n } else if (\n !(\n from &&\n keyIndex < 4 &&\n board?.key(context, item, keyIndex, currentTarget, from)\n ) &&\n !isAlongAxis\n ) {\n return\n }\n }\n event.preventDefault()\n },\n registerItem: (value, layout, ref) => {\n items.current.set(value, { value, layout, ref })\n\n if (!axisOverride) {\n const nextAxis = detectAxis(\n getOrder().map((data) => data.layout)\n )\n\n if (nextAxis !== detectedAxis) setDetectedAxis(nextAxis)\n }\n },\n updateOrder: (item, offset, velocity) => {\n if (isReordering.current) return\n\n const order = getOrder()\n const newOrder = checkReorder(\n order,\n item,\n offset,\n velocity,\n axis,\n isRTL() ? \"rtl\" : \"ltr\"\n )\n\n if (order !== newOrder) {\n isReordering.current = true\n hasReordered.current = true\n commit(newOrder, item)\n }\n },\n }\n\n /**\n * Only clear the guard once the new order has been rendered. Unrelated\n * commits (useTransition's isPending, stale renders in front of\n * useDeferredValue) would otherwise re-enable onReorder while the\n * previous order is still pending.\n */\n useEffect(() => {\n isReordering.current = false\n }, [values])\n\n /**\n * Keep a Reorder.Board up to date with the committed values.\n */\n useIsomorphicLayoutEffect(() => board?.add(context))\n\n /**\n * Disable browser scroll anchoring on the group container.\n * When items reorder, scroll anchoring can cause the browser to adjust\n * the scroll position, which interferes with drag position calculations.\n */\n const groupStyle = {\n overflowAnchor: \"none\" as const,\n ...props.style,\n }\n\n return (\n <Component\n {...props}\n style={groupStyle}\n ref={mergeRefs(groupRef, externalRef)}\n /**\n * A release that ends a reordering drag isn't a click. Browsers\n * only click an element holding both the press and release\n * targets, so whether it was clicked depended on which nodes\n * React moved, and so on drag direction (#2682): Chrome drops\n * the click if the dragged item's node moved, others may click\n * an ancestor. Swallow it here, above every item, either way.\n * The flag is cleared when the drag ends, a frame after the\n * release's click, or on the next press. Clicks with no\n * pointer press behind them (detail 0, e.g. keyboard) are left\n * alone.\n */\n onPointerDownCapture={(event: React.PointerEvent<any>) => {\n hasReordered.current = false\n onPointerDownCapture?.(event)\n }}\n onClickCapture={(event: React.MouseEvent<any>) => {\n if (hasReordered.current && event.detail) {\n hasReordered.current = false\n event.stopPropagation()\n event.preventDefault()\n }\n onClickCapture?.(event)\n }}\n ignoreStrict\n >\n <ReorderContext.Provider value={context}>\n {children}\n </ReorderContext.Provider>\n </Component>\n )\n}\n\n/**\n * Creates `Reorder.Group` rendering the components of `proxy`: `motion`,\n * or `m` to get its features from `LazyMotion`. Its items render the same.\n */\nexport const createReorderGroup = (proxy: MotionProxy) =>\n forwardRef((props: ReorderGroupProps<any, any>, ref) =>\n ReorderGroupComponent(props, ref, proxy)\n ) as <\n Values extends any[],\n TagName extends ReorderElementTag = DefaultGroupElement\n >(\n props: Omit<\n ReorderGroupProps<Values[number], TagName>,\n \"values\" | \"onReorder\"\n > & {\n values: Values\n onReorder: (newOrder: Values, moved: Values[number]) => void\n } & { ref?: React.ForwardedRef<any> }\n ) => ReturnType<typeof ReorderGroupComponent>\n"],"names":[],"mappings":";;;;;;;;;;;;AAyFM;AAiBF;AAMA;;;;;AAKA;AACA;AACA;AACA;AAEA;;AAOA;AACA;;;AAIA;AACA;AAGA;;AAEG;AACH;AAEA;;;AAGG;;AAEC;AACJ;;;;AAWA;;;AAGG;AACH;;AAEI;AAGA;AACI;AACR;AAEA;;;;;;;;AAQI;AACA;;;AAGA;;;;;;;;;;;AAWG;;AAEC;;;AAII;AACA;;;;AAMJ;AACA;;;;;;;AAOC;AACD;AACA;AACI;;AACG;;;AAEA;AAMA;AACH;;;AAGG;;AAEH;AACI;;AAEG;;AAIH;;;;;AAUJ;;AAEG;;AAEC;AACA;AACJ;;AAEI;;AAEJ;;AAEQ;;AAEO;AACD;;;AAIN;AACA;;;;;;;;AAUZ;;AAGI;;;;;;;;AAUJ;;AAUA;AACI;AACA;AACA;;;;AAKZ;;;;;AAKG;;AAEC;AACJ;AAEA;;AAEG;;AAGH;;;;AAIG;AACH;AACI;;;AAIJ;AAKQ;;;;;;;;;;;AAWG;AACH;AACI;AACA;AACJ;;AAGQ;;;;AAIJ;AACJ;AAQZ;AAEA;;;AAGG;AACI;;"}