Skip to content

WIP: feat(dnd) customize pointer drag source#9745

Open
reidbarber wants to merge 3 commits intomainfrom
dnd-pointerDragSource
Open

WIP: feat(dnd) customize pointer drag source#9745
reidbarber wants to merge 3 commits intomainfrom
dnd-pointerDragSource

Conversation

@reidbarber
Copy link
Member

@reidbarber reidbarber commented Mar 5, 2026

Closes #9739

Note: This is a PoC at this point, and needs more discussion before committing.

Adds a pointerDragSource?: 'item' | 'dragButton' option that allows the user to indicate that pointer drags must originate from the drag button (if it exists).

Open questions:

  • This doesn't support ListBox at the moment since it doesn't support drag buttons. Is this something we'd want to try to support somehow?

Remaining work:

  • Better documentation/stories
  • Dragging an item from the table doesn't seem to work until you've clicked somewhere on the table first?
  • Bug: where when you lift your pointer fast enough, there is no release event being registered so the drag preview stays open indefinitely until the next render

✅ Pull Request Checklist:

  • Included link to corresponding React Spectrum GitHub Issue.
  • Added/updated unit tests and storybook for this change (for new code or code which already has tests).
  • Filled out test instructions.
  • Updated documentation (if it already exists for this component).
  • Looked at the Accessibility Practices for this feature - Aria Practices

📝 Test Instructions:

Test it with useDrag.

Test it with Table.

🧢 Your Project:

@rspbot
Copy link

rspbot commented Mar 5, 2026

@rspbot
Copy link

rspbot commented Mar 5, 2026

## API Changes

react-aria-components

/react-aria-components:GridListRenderProps

 GridListRenderProps {
   isDropTarget: boolean
   isEmpty: boolean
   isFocusVisible: boolean
   isFocused: boolean
   layout: 'stack' | 'grid'
+  pointerDragSource?: 'item' | 'dragButton'
   state: ListState<unknown>
 }

/react-aria-components:TableRenderProps

 TableRenderProps {
   isDropTarget: boolean
   isFocusVisible: boolean
   isFocused: boolean
+  pointerDragSource?: 'item' | 'dragButton'
   state: TableState<unknown>
 }

/react-aria-components:TreeRenderProps

 TreeRenderProps {
   allowsDragging: boolean
   isEmpty: boolean
   isFocusVisible: boolean
   isFocused: boolean
+  pointerDragSource?: 'item' | 'dragButton'
   selectionMode: SelectionMode
   state: TreeState<unknown>
 }

/react-aria-components:DragOptions

 DragOptions {
   getAllowedDropOperations?: () => Array<DropOperation>
   getItems: () => Array<DragItem>
   hasDragButton?: boolean
   isDisabled?: boolean
   onDragEnd?: (DragEndEvent) => void
   onDragMove?: (DragMoveEvent) => void
   onDragStart?: (DragStartEvent) => void
+  pointerDragSource?: 'item' | 'dragButton'
   preview?: RefObject<DragPreviewRenderer | null>
 }

/react-aria-components:DragAndDropOptions

 DragAndDropOptions <T = {}> {
   acceptedDragTypes?: 'all' | Array<string | symbol> = 'all'
   dropTargetDelegate?: DropTargetDelegate
   getAllowedDropOperations?: () => Array<DropOperation>
   getDropOperation?: (DropTarget, DragTypes, Array<DropOperation>) => DropOperation
   getItems?: (Set<Key>, Array<T>) => Array<DragItem> = () => []
   isDisabled?: boolean
   onDragEnd?: (DraggableCollectionEndEvent) => void
   onDragMove?: (DraggableCollectionMoveEvent) => void
   onDragStart?: (DraggableCollectionStartEvent) => void
   onDrop?: (DroppableCollectionDropEvent) => void
   onDropActivate?: (DroppableCollectionActivateEvent) => void
   onDropEnter?: (DroppableCollectionEnterEvent) => void
   onDropExit?: (DroppableCollectionExitEvent) => void
   onInsert?: (DroppableCollectionInsertDropEvent) => void
   onItemDrop?: (DroppableCollectionOnItemDropEvent) => void
   onMove?: (DroppableCollectionReorderEvent) => void
   onReorder?: (DroppableCollectionReorderEvent) => void
   onRootDrop?: (DroppableCollectionRootDropEvent) => void
+  pointerDragSource?: 'item' | 'dragButton' = "item"
   renderDragPreview?: (Array<DragItem>) => JSX.Element | {
     element: JSX.Element
   x: number
   y: number
   renderDropIndicator?: (DropTarget) => JSX.Element
   shouldAcceptItemDrop?: (ItemDropTarget, DragTypes) => boolean
 }

@react-aria/dnd

/@react-aria/dnd:DraggableItemProps

 DraggableItemProps {
   hasAction?: boolean
   hasDragButton?: boolean
   key: Key
+  pointerDragSource?: 'item' | 'dragButton'
 }

/@react-aria/dnd:DragOptions

 DragOptions {
   getAllowedDropOperations?: () => Array<DropOperation>
   getItems: () => Array<DragItem>
   hasDragButton?: boolean
   isDisabled?: boolean
   onDragEnd?: (DragEndEvent) => void
   onDragMove?: (DragMoveEvent) => void
   onDragStart?: (DragStartEvent) => void
+  pointerDragSource?: 'item' | 'dragButton'
   preview?: RefObject<DragPreviewRenderer | null>
 }

@nwidynski
Copy link
Contributor

@reidbarber Since this is about dnd, there is a bug i just stumbled upon, where when you lift your pointer fast enough, there is no release event being registered so the drag preview stays open indefinitely until the next render. Its reproducible with a trackpad on OSX in the deployed storybook on main (tree dnd story).

Just wanted to share in case you want to squash that here too 👍

@reidbarber
Copy link
Member Author

@nwidynski Thanks! I hadn't seen that yet.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Can't restrict Table drag to a drag handle

3 participants