feat(drag-and-drop): cross-list DnD, useDragAndDrop hook, and DataTable integration#1556
feat(drag-and-drop): cross-list DnD, useDragAndDrop hook, and DataTable integration#1556ByronDWall wants to merge 15 commits into
Conversation
🦋 Changeset detectedLatest commit: ac2ab7c The changes in this PR will be included in the next version bump. This PR includes changesets to release 5 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Localization reminder: This PR adds or modifies `.i18n.ts` files. Please create a Jira ticket for the localization manager to initiate translation of any new or updated strings in Transifex. See LOC-1766 as an example. |
Bundle Size ReportLast updated: 2026-05-29 14:40:48 UTC
Baseline source: comment-chain |
d0d9093 to
1c8dad3
Compare
…solation and external drop support Extract drag-and-drop logic from DraggableList into a shared useDragAndDrop hook that any React Aria collection component can adopt. Adds dragNamespace prop for cross-list isolation, external drop support (text, files, directories, JSON, images, CSV), outgoing format serialization, and state handler factories (createListDataHandlers, createArrayHandlers). Refactors DraggableList to use the shared hook.
Forward optional dragAndDropHooks to the underlying React Aria Table, enabling consumers to use useDragAndDrop for row reordering and cross-table drag without bundling DnD code by default.
… and changeset Add MDX documentation for the shared useDragAndDrop hook covering namespace isolation, external drops, outgoing formats, state handler factories, and DataTable usage. Update DraggableList dev docs with external drop and outgoing format sections. Add changeset for the minor release.
…dicator, and text selection - Add drag handle column to Header and drag button (IconButton slot="drag") to Row when dragAndDropHooks is provided - Use RaCell for drag cell to preserve React Aria slot context - Add drop indicator styles to recipe - Add grab cursor on draggable rows - Restore double-click word selection on draggable rows via caretPositionFromPoint in handleRowDoubleClick - Remove hasSelectedText guard from handleRowClick so clicks work after text was previously selected - Add DragAndDropRows story with selection and row click - Add ExternalTextDrop and OutgoingFormat stories to DraggableList
…nd useDragAndDrop hook
Move dragAndDropHooks from the internal DataTable.Table compound component to the top-level DataTable props so consumers use the simple API. Update story and docs to match.
…est coverage - Add missing aria-label to drag handle button with i18n key "dragRow" - Use dedicated i18n key "dragRowsColumn" for drag column header - Fix colSpan calculation for nested rows when drag is enabled - Add Safari fallback (caretRangeFromPoint) for double-click text selection - Add keyboard drag reorder test to DragAndDropRows story - Add source list count assertion to ExternalTextDrop story - Remove unused isTextDropItem import from useDragAndDrop hook - Remove internal processDropItems from public exports - Add runtime guard in createArrayHandlers for missing key/id - Add try/catch for malformed JSON in createItemsFromJsonDrop - Fix misleading MDX example that created hooks but discarded them - Align keyboard instruction docs (Enter/Space) across components - Update changeset to mention DataTable base component prop
…docs, and hook stories - Add cursor: grabbing style for data-dragging state on DataTable rows - Cast Selection.modify() calls for TypeScript type safety - Fix DataTable DnD story import to use public API path - Add type guard in createItemsFromTextDrop to skip items without requested type - Document CSV parser limitation in createItemsFromCsvDrop JSDoc - Add DnD + pinning interaction note in DataTable docs - Add useDragAndDrop hook-level stories (reorder, namespace isolation, cross-list transfer)
Cover createArrayHandlers, createListDataHandlers, processDropItems, and all external-drop-helpers (text, file, directory, JSON, CSV, image).
- Guard JSON.parse in processDropItems against corrupted drag data - Prefix internal drag column id to avoid consumer column collisions - Add dev-mode console.warn for invalid reorder target key - Add explicit drag handle assertion in DragAndDropRows story - Merge NamespaceIsolation steps into self-contained verification - Add DirectHookWithTable story demonstrating standalone hook usage - Fix FileDropItem mock missing getText method
…stories - getDropOperation now validates against allowedOperations to prevent silently cancelling valid external drops - Remove unnecessary type cast in createArrayHandlers usage - Remove redundant Record cast in processDropItems auto-keying - Add drag-column-header recipe style for consistency - Clarify ExternalTextDrop story text to reflect copy semantics - Remove unnecessary Escape keypress in NamespacedIsolation test
… and drag keyboard tests - Add sticky positioning rules for drag + selection + expand column combinations in recipe, using size tokens instead of raw px values - Make drag handle cells sticky in table body rows - Fix event listener lifecycle: replace broken callback-ref-attach + effect-cleanup pattern with stable-identity delegates through refs, fixing capture flag mismatch that silently prevented cleanup - Guard programmatic word selection on dblclick to only run when the row is actually draggable - Fix keyboard drag tests to use row focus + ArrowRight instead of direct button focus, matching React Aria Table navigation - Add selectionMode and onRowClick to hook DataTable story for realistic coverage - Extract shared dragItem/dragItemToList test helpers to use-drag-and-drop.test-utils.ts - Fix composed helper docs example to avoid double-processing images
…s, and consistency - Extract resolveDropOperation into testable function and validate external types against acceptExternalTypes before returning operation - Add unit tests for resolveDropOperation covering all branches - Add drag-column sticky offsets to pinned row selectors in recipe - Move test-utils to utils/ subdirectory matching codebase convention - Replace inline setTimeout delays with shared wait() helper in stories - Improve JSDoc for externalDropOperation cancel behavior - Rename discarded params for transparency (_keys, _target)
…test, and link React Aria docs Use == null instead of falsy check for auto-keying so items with key: 0 or key: "" are not incorrectly overwritten. Add explicit test for cross-namespace rejection in resolveDropOperation. Type the hook stories Meta generic to eliminate unnecessary as-casts. Document tab-order assumption in the namespaced isolation story. Link to React Aria DnD docs from the state handler factories section.
…rows feat(draggable-list): add selection support with stories and documentation Chrome 128+ added caretPositionFromPoint, which skips elements with user-select:none — causing wrong text to be selected and no visible highlight. Temporarily set user-select:text on the row before resolving the caret position, then reset on the next pointerdown. Also removed an unnecessary intermediate sel.extend() call from the selection algorithm. Added SingleSelection and MultipleSelection stories with play functions testing click, deselect, and keyboard interactions. Added selected-state styling (colorPalette.5 background) to the DraggableList recipe and documented both selection modes in the dev.mdx with live examples.
e472c29 to
ac2ab7c
Compare
| // Prevent row click when clicking on interactive elements to avoid conflicts | ||
| const isInteractiveElement = getIsTableRowChildElementInteractive(e); | ||
| // Prevent row click when text is selected | ||
| const hasSelectedText = |
There was a problem hiding this comment.
from claude:
hasSelectedText guard removed unconditionally — regression for non-draggable rows
In data-table.row.tsx, the old handleRowClick path skipped navigation if the user had selected text (!hasSelectedText). That guard is gone now. On any row with onRowClick — draggable or not — a mouse-up after text selection will trigger navigation. The dblclick programmatic-selection workaround only applies to [draggable='true'] rows, so non-draggable rows lose the protection.
But honestly if the row has an onRowClick, shouldn't the navigation or other onRowClick operations take precedence over selecting text?
tylermorrisford
left a comment
There was a problem hiding this comment.
one Q, otherwise 💰
Closes #1554
Motivation
DraggableList.Rootuses a single hardcoded drag type (nimbus-draggable-list-item) for all instances on a page. Every list accepts every other list's items, with no public API to opt out. Consumers rendering two independent lists — say, a "selected columns" list and an "available filters" list — have no way to prevent items from leaking between them. ThegetDropOperationcallback is hardcoded to"move"and the internally-constructeddragAndDropHookssilently overrides any consumer-supplied value.The only workaround today is reverting foreign items inside
onUpdateItems(which flashes the wrong state) or forking the component entirely.This PR solves the problem with a
dragNamespaceprop (option 1 from the issue) and, while doing so, extracts the drag-and-drop wiring into a shareduseDragAndDrophook that also works withDataTableand any other React Aria collection component.What changed
New
useDragAndDrophookRather than fixing
DraggableListin isolation, the DnD configuration is extracted into a reusable hook. It wraps React Aria'suseDragAndDropwith:dragNamespaceappends a suffix to the internal drag type, so only collections sharing the same namespace can exchange items. Omitting it preserves backward-compatible behavior where all lists interoperate.onExternalDrop+acceptExternalTypeslet a collection accept drops from outside the app (text, files, etc.).resolveDropOperationvalidates incoming types againstacceptExternalTypesbefore returning the configured operation.serializeDragItemadds custom MIME representations alongside the internal format when items are dragged out.onExternalDropwithoutkeyoridget acrypto.randomUUID()key so React Aria's collection can track them.State handler factories
Consumers need to wire up insert/append/reorder/remove callbacks. Two factories make this a one-liner:
createArrayHandlers(setItems, getKey?)— foruseStatearrayscreateListDataHandlers(list)— for React Aria'suseListDataBoth return an object meant to be spread into the hook options.
External drop helpers
Six composable utilities for converting raw
DropItem[]insideonExternalDrop:createItemsFromTextDroptext/plain(or custom){ key, label }createItemsFromFileDrop{ key, label, fileName, fileType, file }createItemsFromDirectoryDrop{ directory }createItemsFromJsonDropapplication/json{ key, label, data }createItemsFromCsvDroptext/csv{ key, label, headers, row }createItemsFromImageDrop{ key, label, fileType, objectUrl, file }DraggableList changes
dragNamespace,onExternalDrop,acceptExternalTypes,externalDropOperation,serializeDragItemDraggableList.Rootcontinues to manage its ownuseListDatastate; the hook handles the drag-and-drop configurationDataTable integration
The DataTable previously had no drag-and-drop support. This PR adds:
dragAndDropHooksprop on bothDataTableandDataTable.TableDragIndicatoricon) is automatically prependeddraggable="true"on rows suppresses native double-click text selection; a programmaticselection.modify()workaround restores it usingcaretPositionFromPoint(Chrome/Firefox) withcaretRangeFromPointfallback (Safari)Architecture
resolveDropOperationis extracted as a pure function so the drop-operation decision logic (internal vs. external type validation vs. cancel) is unit-testable independent of React.Testing
Unit tests (63 tests in
use-drag-and-drop.spec.ts):createArrayHandlers— insert, append, reorder, remove, default key extraction, missing key errorcreateListDataHandlers— all delegation pathsprocessDropItems— internal deserialization, external routing, auto-keying, corrupted JSONresolveDropOperation— internal move/cancel, external type accepted/rejected, operation allowed/deniedStorybook interaction tests:
Test plan
pnpm test:dev— 110 tests across 4 files)pnpm typecheck)useDragAndDrop,DraggableList, andDataTableDnD variantsdragAndDropHooksis provided