import { useCallback, useRef, useState, type ReactElement } from "react"; import { ScrollView, View } from "react-native"; import { DndContext, closestCenter, KeyboardSensor, PointerSensor, type Modifier, useSensor, useSensors, type DragEndEvent, type DragStartEvent, } from "@dnd-kit/core"; import { SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy, useSortable, arrayMove, } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import type { DraggableListProps, DraggableRenderItemInfo, } from "./draggable-list.types"; import { WebDesktopScrollbarOverlay, useWebDesktopScrollbarMetrics, } from "./web-desktop-scrollbar"; export type { DraggableListProps, DraggableRenderItemInfo }; const restrictToVerticalAxis: Modifier = ({ transform }) => ({ ...transform, x: 0, }); interface SortableItemProps { id: string; item: T; index: number; renderItem: (info: DraggableRenderItemInfo) => ReactElement; activeId: string | null; } function SortableItem({ id, item, index, renderItem, activeId, }: SortableItemProps) { const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id }); const dragRef = useRef<(() => void) | null>(null); const drag = useCallback(() => { // dnd-kit handles drag initiation via listeners // This is a no-op but matches the mobile API }, []); // Store listeners in ref so drag handle can access them dragRef.current = () => { // Trigger drag - handled by dnd-kit's listeners }; const baseTransform = CSS.Transform.toString(transform); const scaleTransform = isDragging ? "scale(1.02)" : ""; const combinedTransform = [baseTransform, scaleTransform].filter(Boolean).join(" "); const style = { transform: combinedTransform || undefined, transition, opacity: isDragging ? 0.9 : 1, zIndex: isDragging ? 1000 : 1, }; const info: DraggableRenderItemInfo = { item, index, drag, isActive: activeId === id, }; return (
{renderItem(info)}
); } export function DraggableList({ data, keyExtractor, renderItem, onDragEnd, style, contentContainerStyle, testID, ListFooterComponent, ListHeaderComponent, ListEmptyComponent, showsVerticalScrollIndicator = true, enableDesktopWebScrollbar = false, // simultaneousGestureRef is native-only, ignored on web }: DraggableListProps) { const [activeId, setActiveId] = useState(null); const [items, setItems] = useState(data); const scrollViewRef = useRef(null); const scrollbarMetrics = useWebDesktopScrollbarMetrics(); // Sync items with data prop if (data !== items && !activeId) { setItems(data); } const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8, }, }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }) ); const handleDragStart = useCallback((event: DragStartEvent) => { setActiveId(String(event.active.id)); }, []); const handleDragEnd = useCallback( (event: DragEndEvent) => { const { active, over } = event; setActiveId(null); if (over && active.id !== over.id) { const oldIndex = items.findIndex( (item, i) => keyExtractor(item, i) === active.id ); const newIndex = items.findIndex( (item, i) => keyExtractor(item, i) === over.id ); const newItems = arrayMove(items, oldIndex, newIndex); setItems(newItems); onDragEnd(newItems); } }, [items, keyExtractor, onDragEnd] ); const ids = items.map((item, index) => keyExtractor(item, index)); const showCustomScrollbar = enableDesktopWebScrollbar; return ( {ListHeaderComponent} {items.length === 0 && ListEmptyComponent} {items.map((item, index) => { const id = keyExtractor(item, index); return ( ); })} {ListFooterComponent} { scrollViewRef.current?.scrollTo({ y: nextOffset, animated: false }); scrollbarMetrics.setOffset(nextOffset); }} /> ); }