22
33import { useEffect , useRef } from 'react'
44
5- /**
6- * How long `dragover` must go silent before the drag is presumed over.
7- *
8- * `dragover` fires continuously (roughly per animation frame) for as long as a drag is live and
9- * inside the window, so a gap this long means the drag ended somewhere no event of ours can
10- * observe. Generous on purpose: the cost of firing late is a few frames of stale drag styling,
11- * while firing early would tear down a drag the user is still holding.
12- */
13- const DRAG_IDLE_TEARDOWN_MS = 400
14-
155/**
166 * Runs a drag's teardown wherever the drag actually ends.
177 *
188 * Three signals, because no single one is reliable here:
199 *
2010 * 1. `drop` on `window` — a release over any valid target, wherever it bubbles from.
2111 * 2. `dragend` on `window` — the normal end of a drag whose source row still exists.
22- * 3. A `dragover` idle watchdog — the case the first two miss. `dragend` is dispatched *at the
12+ * 3. `pointermove` on `window` — the case the first two miss. `dragend` is dispatched *at the
2313 * source node*, so once spring-loading navigates the list and unmounts that row, the event
2414 * has no path to `window` and neither listener above ever runs. Cancelling with Escape or
2515 * releasing over nothing then leaves the ghost on the page, every row frozen at drag
2616 * opacity, and the spring-open set uncleared so those folders refuse to open again.
2717 *
28- * The watchdog also covers a drag that leaves the window entirely. If it re-enters, the visual
29- * state has been reset but the drop still resolves — the row ids live in `dataTransfer`, not in
30- * the state this tears down.
18+ * The third signal works because browsers suppress mouse and pointer events for the duration of
19+ * a native drag: the first `pointermove` after one starts can only mean it is over. That makes
20+ * it exact, where a timer is not — the drag model fires `dragover` on roughly a 350ms cadence
21+ * while the pointer is stationary, so an idle-timeout version of this tore down mid-drag
22+ * whenever the user rested on a folder waiting for it to spring open.
3123 *
3224 * `teardown` is read through a ref and the listeners bind once, deliberately. Depending on the
3325 * callback would re-run this effect on every render, and the teardown wired into it would then
@@ -38,32 +30,32 @@ export function useDragTeardown(teardown: () => void): void {
3830 teardownRef . current = teardown
3931
4032 useEffect ( ( ) => {
41- let idleTimer : ReturnType < typeof setTimeout > | null = null
42-
43- const clearIdleTimer = ( ) => {
44- if ( idleTimer !== null ) clearTimeout ( idleTimer )
45- idleTimer = null
33+ /**
34+ * Set from `dragover` rather than `dragstart` so the flag only turns on once a drag is
35+ * genuinely under way, and so a stray `pointermove` before the drag engages cannot tear
36+ * down a drag that never started.
37+ */
38+ let isDragging = false
39+
40+ const markDragging = ( ) => {
41+ isDragging = true
4642 }
4743
48- const runTeardown = ( ) => {
49- clearIdleTimer ( )
44+ const endDrag = ( ) => {
45+ if ( ! isDragging ) return
46+ isDragging = false
5047 teardownRef . current ( )
5148 }
5249
53- /** Restarted on every `dragover`; only elapses once the drag stops reporting. */
54- const handleDragOver = ( ) => {
55- clearIdleTimer ( )
56- idleTimer = setTimeout ( runTeardown , DRAG_IDLE_TEARDOWN_MS )
57- }
58-
59- window . addEventListener ( 'dragend' , runTeardown )
60- window . addEventListener ( 'drop' , runTeardown )
61- window . addEventListener ( 'dragover' , handleDragOver )
50+ window . addEventListener ( 'dragover' , markDragging )
51+ window . addEventListener ( 'dragend' , endDrag )
52+ window . addEventListener ( 'drop' , endDrag )
53+ window . addEventListener ( 'pointermove' , endDrag )
6254 return ( ) => {
63- clearIdleTimer ( )
64- window . removeEventListener ( 'dragend' , runTeardown )
65- window . removeEventListener ( 'drop' , runTeardown )
66- window . removeEventListener ( 'dragover ' , handleDragOver )
55+ window . removeEventListener ( 'dragover' , markDragging )
56+ window . removeEventListener ( 'dragend' , endDrag )
57+ window . removeEventListener ( 'drop' , endDrag )
58+ window . removeEventListener ( 'pointermove ' , endDrag )
6759 }
6860 } , [ ] )
6961}
0 commit comments