53 lines
1.9 KiB
TypeScript
53 lines
1.9 KiB
TypeScript
import React, { useCallback, useRef, useState } from "react";
|
|
|
|
// Native HTML5 drag-and-drop reordering — zero dependencies. Pointer users drag; keyboard users use
|
|
// the up/down buttons the lists also render (the accessible path). Returns per-item props plus the
|
|
// current drag/over indices so the caller can animate (dragged item dims, drop target shows a line).
|
|
export function useDragReorder(onReorder: (from: number, to: number) => void) {
|
|
const [dragIndex, setDragIndex] = useState<number | null>(null);
|
|
const [overIndex, setOverIndex] = useState<number | null>(null);
|
|
// Drop clears the rendered state before the source receives dragend.
|
|
const ownsGesture = useRef(false);
|
|
|
|
const reset = useCallback(() => {
|
|
setDragIndex(null);
|
|
setOverIndex(null);
|
|
}, []);
|
|
|
|
const getItemProps = useCallback(
|
|
(index: number) => ({
|
|
draggable: true,
|
|
onDragStart: (e: React.DragEvent) => {
|
|
e.stopPropagation();
|
|
ownsGesture.current = true;
|
|
setDragIndex(index);
|
|
e.dataTransfer.effectAllowed = "move";
|
|
// Firefox needs data set for a drag to start.
|
|
e.dataTransfer.setData("text/plain", String(index));
|
|
},
|
|
onDragOver: (e: React.DragEvent) => {
|
|
if (dragIndex === null) return;
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
e.dataTransfer.dropEffect = "move";
|
|
if (overIndex !== index) setOverIndex(index);
|
|
},
|
|
onDrop: (e: React.DragEvent) => {
|
|
e.preventDefault();
|
|
if (dragIndex === null) return;
|
|
e.stopPropagation();
|
|
if (dragIndex !== index) onReorder(dragIndex, index);
|
|
reset();
|
|
},
|
|
onDragEnd: (e: React.DragEvent) => {
|
|
if (ownsGesture.current) e.stopPropagation();
|
|
ownsGesture.current = false;
|
|
reset();
|
|
},
|
|
}),
|
|
[dragIndex, overIndex, onReorder, reset],
|
|
);
|
|
|
|
return { dragIndex, overIndex, getItemProps };
|
|
}
|