Skip to content

React Aria: collapsing a node during a keyboard drag traps the page keyboard #10599

Description

@af-cf

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:

  1. 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.
  2. 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:

  1. Keyboard-navigate to the Image 2 row.
  2. Move focus to its drag handle and press Enter to start the drag.
  3. Press ArrowUp until on Photos.
  4. Press ArrowLeft to collapse Photos.
  5. 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions