Drag and Drop in Element Web#
Element Web's drag-and-drop system is built on @dnd-kit (@dnd-kit/abstract, @dnd-kit/dom, @dnd-kit/react), at version ^0.5.0 . It is used exclusively in the grouped (treegrid) room list view and supports two drag interactions:
- Moving rooms between sections β drag a room item and drop it on a section header
- Reordering sections β drag a section header to a new position
Flat list mode has no drag-and-drop support; only the grouped (treegrid) layout wraps in a DragDropProvider .
All DnD code lives in the shared-components package under packages/shared-components/src/room-list/VirtualizedRoomListView/.
Key Source Files#
| File | Role |
|---|---|
VirtualizedRoomListView.tsx | Top-level DragDropProvider, sensor config, DragOverlay, event handlers |
dragAndDrop.ts | Discriminated union types (SectionDragData, RoomDragData) and type guard isSectionDragData() |
RoomListItemWrapper.tsx | Makes room items draggable via useDraggable |
RoomListSectionHeaderView.tsx | Makes section headers both draggable (useDraggable) and droppable (useDroppable) |
RoomListItemDragOverlayView.tsx | Visual clone of a room item rendered in the drag overlay |
RoomListSectionHeaderDragOverlayView.tsx | Visual clone of a section header in the drag overlay |
RoomListAccessibilityPlugin.ts | Custom dnd-kit plugin for screen reader announcements |
Sensor Configuration (PointerSensor & KeyboardSensor)#
Both sensors are configured on the DragDropProvider in VirtualizedRoomListView.tsx.
PointerSensor β uses the unified Pointer Events API, which handles mouse, touch, and stylus input without separate event listeners. It is configured with a 5 px activation distance so that a short tap or click does not accidentally start a drag .
KeyboardSensor β remapped to avoid conflicts with existing room-list shortcuts :
Spaceβ start and end drag (notEnter, which opens the room)Escapeβ cancel drag- Arrow keys β move the dragged item
- Offset: 17 px per keypress (
KEYBOARD_DRAG_OFFSET) instead of the dnd-kit default of 10 px, to avoid sluggish movement
During an active keyboard drag, useDragOperation is used to disable the room list's own keyboard navigation so arrow keys don't simultaneously scroll the list .
Drag Payload Types & Event Handling#
dragAndDrop.ts defines the discriminated union carried in every drag operation:
RoomDragDataβ{ type: "room" }β attached to each room list itemSectionDragDataβ{ type: "section"; index: number }β attached to section headers, carrying the section's current index for ordering logicisSectionDragData()β runtime type guard used inonDragEndand overlay rendering to dispatch the correct handler
onDragEnd in VirtualizedRoomListView routes drops to either vm.changeSectionOrder(sourceId, targetId) or vm.changeRoomSection(sourceId, targetId) based on this guard .
Draggable and Droppable Components#
Room items β RoomListItemWrapper.tsx wraps each room in a DraggableWrapper (treegrid mode only) that calls useDraggable<RoomDragData> with two plugins:
Feedback.configure({ feedback: "clone" })β renders a floating clone rather than lifting the DOM element, preventing a hole in the virtualized listRestrictToVerticalAxisβ limits movement to the vertical axis
Section headers β RoomListSectionHeaderView.tsx uses both useDraggable and useDroppable on each header:
- Dragging is gated on the
canBeReorderedflag β Favourites and Low Priority sections have this set tofalseand are pinned in place - Dropping is enabled for all sections (rooms can be dropped on non-reorderable sections); it is only disabled for section-on-section drags targeting non-reorderable headers
- Visual indicators (
.dropTargetTop,.dropTargetBottom) show whether the dragged section will land above or below the target - On section drag start, all sections collapse so the user can see the full list; original states are restored on drop
Drag Overlay Strategy#
Both draggable types use Feedback.configure({ feedback: "clone" }) instead of lifting the actual DOM node. This is critical for virtualized lists β lifting the real element would leave a gap in the list. The floating clone is rendered in the <DragOverlay dropAnimation={null}> , which is positioned outside the virtual scroll container.
DragOverlayContent calls useDragOperation to detect the active source and render:
RoomListItemDragOverlayViewβ reusesRoomListItemContentwith matching styles; hidden from the accessibility tree viaaria-hiddensince the real item stays in the DOMRoomListSectionHeaderDragOverlayViewβ reusesRoomListSectionHeaderContent; alsoaria-hidden
Accessibility#
A custom RoomListAccessibilityPlugin replaces dnd-kit's built-in Accessibility plugin. The reason: dnd-kit's default plugin sets aria-pressed/aria-grabbed on drag sources, which VoiceOver reads as "selected" β incorrect semantics .
The custom plugin maintains two aria-live regions :
aria-live="polite"(role="status") β announcesdragstartanddragoverprogress without interrupting speecharia-live="assertive"(role="alert") β announces the terminaldragend/cancel result, interrupting if necessary
Keyboard drag instructions are injected via a hidden element wired to each draggable's aria-describedby. The effect re-runs as draggables mount/unmount, supporting virtualized list scrolling .
All announcement strings are localized, including positional descriptions like "Section A will be moved before Section B" .
Version History#
The older matrix-react-sdk package used react-beautiful-dnd for the Space panel drag-and-drop . The new room list (element-web v2 architecture in shared-components) was built directly on @dnd-kit β there was no migration; @dnd-kit is the greenfield choice for the rewritten room list.
Key milestones:
| PR | Change |
|---|---|
| #33366 | Initial DnD: move rooms between sections |
| #33606 | Section reordering via drag |
| #33885 | Bump to @dnd-kit ^0.5.0; removed custom TypeScript patch; added independent disabled.dragging/disabled.dropping on useSortable |
The ^0.4.0 β ^0.5.0 upgrade also fixed a sensor bug where inline array sensor configs caused unnecessary entity mutations on every render .