Provide a general summary of the issue here
During a keyboard drag in a RAC Tree, collapsing an ancestor of the dragged item unmounts the drag source. The DragManager session survives that unmount with its document-level capture listeners still installed. Every keystroke on the page is then swallowed, no drop indicator is visible, and focus sits on <body>. Escape is the only way out, and nothing on screen says so.
The trigger is not collapsing as such. It is unmounting the drag source mid-drag, so any app that removes the dragged row during a keyboard drag (filtering, live data, virtualization) should hit the same state.
🤔 Expected Behavior?
A drag session that can no longer be driven should end itself: tear down its listeners, restore focus to a surviving element, and fire onDragEnd once. A keyboard-only user should never be left on a page that eats every key while showing no drag.
Separately, there is a design call about what collapsing an ancestor of the dragged item should mean. Two options:
- Keep the drag alive. The item's data still exists and only its row is hidden. RAC already tracks
draggingKeys independently of the DOM. The drop target would re-derive to the nearest surviving position (on Photos, or after Photos), and the hidden descendants' indicators disappear. This matches what already happens when collapsing a node that does not contain the dragged item.
- Refuse the collapse while the drag source sits inside that subtree.
Option 1 seems more useful. Collapsing a folder to bring a distant drop target into view is the flow keyboard reordering needs.
😯 Current Behavior
Trace from the automated repro below (swallowed? reports event.defaultPrevented after dispatching a keydown on the page):
onDragStart keys: ["image-2"]
after drag start target=Insert after Image 2
ArrowUp x4 target=Drop on Photos rows=[documents, budget, photos, image-1, image-2]
ArrowLeft (collapse)
>> onDragEnd op: cancel
target=(none) rows=[documents, budget, photos]
focus=<body>
ArrowUp swallowed? true
Tab swallowed? true
"a" swallowed? true
Escape swallowed? true -> >> onDragEnd op: cancel (second time)
post-Escape ArrowUp swallowed? false
final focus=<body>
Four distinct problems:
- The drag ends with no drop, and nothing on screen marks the transition.
- The session keeps eating every keydown on the page via
preventDefault() and stopImmediatePropagation().
onDragEnd fires twice.
- Focus lands on
<body>, so a keyboard user restarts tabbing from the top of the document.
Control case. Drag Budget (a child of Documents), then collapse Photos. The drag survives, the target stays on Photos, and arrow keys keep working. This is what isolates the trigger to the drag source unmounting.
💁 Possible Solution
Three pieces are involved, in order. Paths and line numbers are as published in the 1.21.1 sourcemaps.
1. react-aria-components/src/Tree.tsx:545 passes an onKeyDown to useDroppableCollection that calls state.toggleKey(target.key) when the collapse key fires on an on target:
onKeyDown: e => {
let target = dropState?.target;
if (target && target.type === 'item' && target.dropPosition === 'on') {
let item = state.collection.getItem(target.key);
if (e.key === EXPANSION_KEYS['expand'][direction] && item?.hasChildNodes && !state.expandedKeys.has(target.key)) {
state.toggleKey(target.key);
} else if (e.key === EXPANSION_KEYS['collapse'][direction] && item?.hasChildNodes && state.expandedKeys.has(target.key)) {
state.toggleKey(target.key);
}
}
}
Collapsing Photos unmounts the Image 2 row, which DragManager holds as dragTarget.element.
2. react-aria/src/dnd/useDrag.ts:283-309 cleans up on unmount by firing onDragEnd and clearing global DnD state, but never tears down the DragManager session. That cleanup exists for a Firefox native-drag bug and predates keyboard DnD. The session's document.addEventListener('keydown', ..., true) and cancelEvent() stay installed, which is what kills the keyboard. This looks like the main fix: the cleanup should end the DragManager session rather than only the global state.
3. react-aria/src/dnd/DragManager.ts:634 cancel() calls this.dragTarget.element.focus(), which does nothing on a detached node, so even Escape leaves focus on <body>. A fallback target is needed.
Two related open issues that may share fix surface:
- #6649: Escape during a keyboard drag does not restore focus in lists. Same weak
cancel() focus restore, triggered by aria-hidden rather than a detached node.
- #5336: cannot drag when the virtualizer removes the item from the DOM. The pointer-DnD counterpart of "drag source unmounts".
🔦 Context
We use RAC Tree with useDragAndDrop for a file-tree style reorder UI where users move items between nested folders. Collapsing a folder to bring a distant drop target into view is a natural part of that flow with a keyboard, and it is the one path that breaks.
The severity comes from the blast radius. The failure is not confined to the tree: the whole page stops responding to the keyboard, with no visible drag and no hint that Escape is the exit. For a keyboard-only or screen-reader user the page looks frozen. Escape does recover input, but focus is still lost to <body>.
🖥️ Steps to Reproduce
Given a Tree with dragAndDropHooks and a Photos folder containing Image 1 and Image 2:
- Keyboard-navigate to the
Image 2 row.
- Move focus to its drag handle and press Enter to start the drag.
- Press ArrowUp until on
Photos.
- Press ArrowLeft to collapse
Photos.
- Press any key. Nothing on the page responds until Escape.
Version
react-aria-components 1.21.1 (with react-aria 3.52.1, react-stately 3.50.0, React 19.2.8).
What browsers are you seeing the problem on?
Firefox, Chrome
If other, please specify.
No response
What operating system are you using?
Fedora 44
🧢 Your Company/Team
No response
🕷 Tracking Issue
No response
Provide a general summary of the issue here
During a keyboard drag in a RAC
Tree, collapsing an ancestor of the dragged item unmounts the drag source. TheDragManagersession survives that unmount with its document-level capture listeners still installed. Every keystroke on the page is then swallowed, no drop indicator is visible, and focus sits on<body>. Escape is the only way out, and nothing on screen says so.The trigger is not collapsing as such. It is unmounting the drag source mid-drag, so any app that removes the dragged row during a keyboard drag (filtering, live data, virtualization) should hit the same state.
🤔 Expected Behavior?
A drag session that can no longer be driven should end itself: tear down its listeners, restore focus to a surviving element, and fire
onDragEndonce. A keyboard-only user should never be left on a page that eats every key while showing no drag.Separately, there is a design call about what collapsing an ancestor of the dragged item should mean. Two options:
draggingKeysindependently of the DOM. The drop target would re-derive to the nearest surviving position (on Photos, orafter Photos), and the hidden descendants' indicators disappear. This matches what already happens when collapsing a node that does not contain the dragged item.Option 1 seems more useful. Collapsing a folder to bring a distant drop target into view is the flow keyboard reordering needs.
😯 Current Behavior
Trace from the automated repro below (
swallowed?reportsevent.defaultPreventedafter dispatching akeydownon the page):Four distinct problems:
preventDefault()andstopImmediatePropagation().onDragEndfires twice.<body>, so a keyboard user restarts tabbing from the top of the document.Control case. Drag
Budget(a child ofDocuments), then collapsePhotos. The drag survives, the target stays onPhotos, and arrow keys keep working. This is what isolates the trigger to the drag source unmounting.💁 Possible Solution
Three pieces are involved, in order. Paths and line numbers are as published in the 1.21.1 sourcemaps.
1.
react-aria-components/src/Tree.tsx:545passes anonKeyDowntouseDroppableCollectionthat callsstate.toggleKey(target.key)when the collapse key fires on anontarget:Collapsing
Photosunmounts theImage 2row, whichDragManagerholds asdragTarget.element.2.
react-aria/src/dnd/useDrag.ts:283-309cleans up on unmount by firingonDragEndand clearing global DnD state, but never tears down theDragManagersession. That cleanup exists for a Firefox native-drag bug and predates keyboard DnD. The session'sdocument.addEventListener('keydown', ..., true)andcancelEvent()stay installed, which is what kills the keyboard. This looks like the main fix: the cleanup should end theDragManagersession rather than only the global state.3.
react-aria/src/dnd/DragManager.ts:634cancel()callsthis.dragTarget.element.focus(), which does nothing on a detached node, so even Escape leaves focus on<body>. A fallback target is needed.Two related open issues that may share fix surface:
cancel()focus restore, triggered byaria-hiddenrather than a detached node.🔦 Context
We use RAC
TreewithuseDragAndDropfor a file-tree style reorder UI where users move items between nested folders. Collapsing a folder to bring a distant drop target into view is a natural part of that flow with a keyboard, and it is the one path that breaks.The severity comes from the blast radius. The failure is not confined to the tree: the whole page stops responding to the keyboard, with no visible drag and no hint that Escape is the exit. For a keyboard-only or screen-reader user the page looks frozen. Escape does recover input, but focus is still lost to
<body>.🖥️ Steps to Reproduce
Given a
TreewithdragAndDropHooksand aPhotosfolder containingImage 1andImage 2:Image 2row.Photos.Photos.Version
react-aria-components1.21.1 (withreact-aria3.52.1,react-stately3.50.0, React 19.2.8).What browsers are you seeing the problem on?
Firefox, Chrome
If other, please specify.
No response
What operating system are you using?
Fedora 44
🧢 Your Company/Team
No response
🕷 Tracking Issue
No response