import { Active, DndContext, DragEndEvent, DragOverlay, DragStartEvent, DraggableSyntheticListeners, KeyboardSensor, PointerSensor, defaultDropAnimationSideEffects, useSensor, useSensors, type DropAnimation, type UniqueIdentifier, } from "@dnd-kit/core" import { SortableContext, arrayMove, sortableKeyboardCoordinates, useSortable, } from "@dnd-kit/sortable" import { CSS } from "@dnd-kit/utilities" import { DotsSix } from "@medusajs/icons" import { IconButton, clx } from "@medusajs/ui" import { CSSProperties, Fragment, PropsWithChildren, ReactNode, createContext, useContext, useMemo, useState, } from "react" type SortableBaseItem = { id: UniqueIdentifier } interface SortableListProps { items: TItem[] onChange: (items: TItem[]) => void renderItem: (item: TItem, index: number) => ReactNode } const List = ({ items, onChange, renderItem, }: SortableListProps) => { const [active, setActive] = useState(null) const [activeItem, activeIndex] = useMemo(() => { if (active === null) { return [null, null] } const index = items.findIndex(({ id }) => id === active.id) return [items[index], index] }, [active, items]) const sensors = useSensors( useSensor(PointerSensor), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }) ) const handleDragStart = ({ active }: DragStartEvent) => { setActive(active) } const handleDragEnd = ({ active, over }: DragEndEvent) => { if (over && active.id !== over.id) { const activeIndex = items.findIndex(({ id }) => id === active.id) const overIndex = items.findIndex(({ id }) => id === over.id) onChange(arrayMove(items, activeIndex, overIndex)) } setActive(null) } const handleDragCancel = () => { setActive(null) } return ( {activeItem && activeIndex !== null ? renderItem(activeItem, activeIndex) : null}
    {items.map((item, index) => ( {renderItem(item, index)} ))}
) } const dropAnimationConfig: DropAnimation = { sideEffects: defaultDropAnimationSideEffects({ styles: { active: { opacity: "0.4", }, }, }), } type SortableOverlayProps = PropsWithChildren const Overlay = ({ children }: SortableOverlayProps) => { return ( {children} ) } type SortableItemProps = PropsWithChildren<{ id: TItem["id"] className?: string }> type SortableItemContextValue = { attributes: Record listeners: DraggableSyntheticListeners ref: (node: HTMLElement | null) => void isDragging: boolean } const SortableItemContext = createContext(null) const useSortableItemContext = () => { const context = useContext(SortableItemContext) if (!context) { throw new Error( "useSortableItemContext must be used within a SortableItemContext" ) } return context } const Item = ({ id, className, children, }: SortableItemProps) => { const { attributes, isDragging, listeners, setNodeRef, setActivatorNodeRef, transform, transition, } = useSortable({ id }) const context = useMemo( () => ({ attributes, listeners, ref: setActivatorNodeRef, isDragging, }), [attributes, listeners, setActivatorNodeRef, isDragging] ) const style: CSSProperties = { opacity: isDragging ? 0.4 : undefined, transform: CSS.Translate.toString(transform), transition, } return (
  • {children}
  • ) } const DragHandle = () => { const { attributes, listeners, ref } = useSortableItemContext() return ( ) } export const SortableList = Object.assign(List, { Item, DragHandle, })