Skip to content

feat: (WIP) Support DnD in S2 collection components#9791

Draft
LFDanLu wants to merge 32 commits intomainfrom
s2_dnd
Draft

feat: (WIP) Support DnD in S2 collection components#9791
LFDanLu wants to merge 32 commits intomainfrom
s2_dnd

Conversation

@LFDanLu
Copy link
Copy Markdown
Member

@LFDanLu LFDanLu commented Mar 13, 2026

Closes

✅ 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:

🧢 Your Project:

RSP

@github-actions github-actions bot added the S2 label Mar 13, 2026
@rspbot
Copy link
Copy Markdown

rspbot commented Mar 13, 2026

@rspbot
Copy link
Copy Markdown

rspbot commented Mar 14, 2026

@rspbot
Copy link
Copy Markdown

rspbot commented Mar 16, 2026

@github-actions github-actions bot added the RAC label Mar 24, 2026
@rspbot
Copy link
Copy Markdown

rspbot commented Mar 24, 2026

@rspbot
Copy link
Copy Markdown

rspbot commented Apr 1, 2026

@rspbot
Copy link
Copy Markdown

rspbot commented Apr 4, 2026

@rspbot
Copy link
Copy Markdown

rspbot commented Apr 6, 2026

@rspbot
Copy link
Copy Markdown

rspbot commented Apr 6, 2026

## API Changes

react-aria-components

/react-aria-components:TreeRenderProps

 TreeRenderProps {
   allowsDragging: boolean
+  isDropTarget: boolean
   isEmpty: boolean
   isFocusVisible: boolean
   isFocused: boolean
   selectionMode: SelectionMode
 }

/react-aria-components:TreeEmptyStateRenderProps

 TreeEmptyStateRenderProps {
   allowsDragging: boolean
+  isDropTarget: boolean
   isFocusVisible: boolean
   isFocused: boolean
   selectionMode: SelectionMode
   state: TreeState<unknown>

@react-aria/grid

/@react-aria/grid:GridKeyboardDelegate

 GridKeyboardDelegate <C extends IGridCollection<T>, T> {
   collection: IGridCollection<T>
   constructor: (GridKeyboardDelegateOptions<IGridCollection<T>>) => void
   getFirstKey: (Key, boolean) => Key | null
-  getKeyAbove: (Key) => Key | null
-  getKeyBelow: (Key) => Key | null
+  getKeyAbove: (Key, {
+    includeDisabled?: boolean
+}) => Key | null
+  getKeyBelow: (Key, {
+    includeDisabled?: boolean
+}) => Key | null
   getKeyForSearch: (string, Key) => Key | null
   getKeyLeftOf: (Key) => Key | null
   getKeyPageAbove: (Key) => Key | null
   getKeyPageBelow: (Key) => Key | null
   getLastKey: (Key, boolean) => Key | null
 }

@react-aria/selection

/@react-aria/selection:ListKeyboardDelegate

 ListKeyboardDelegate <T> {
   constructor: (Array<any>) => void
   getFirstKey: () => Key | null
-  getKeyAbove: (Key) => Key | null
-  getKeyBelow: (Key) => Key | null
+  getKeyAbove: (Key, {
+    includeDisabled?: boolean
+}) => Key | null
+  getKeyBelow: (Key, {
+    includeDisabled?: boolean
+}) => Key | null
   getKeyForSearch: (string, Key) => Key | null
-  getKeyLeftOf: (Key) => Key | null
+  getKeyLeftOf: (Key, {
+    includeDisabled?: boolean
+}) => Key | null
   getKeyPageAbove: (Key) => Key | null
   getKeyPageBelow: (Key) => Key | null
-  getKeyRightOf: (Key) => Key | null
+  getKeyRightOf: (Key, {
+    includeDisabled?: boolean
+}) => Key | null
   getLastKey: () => Key | null
-  getNextKey: (Key) => Key | null
-  getPreviousKey: (Key) => Key | null
+  getNextKey: (Key, {
+    includeDisabled?: boolean
+}) => Key | null
+  getPreviousKey: (Key, {
+    includeDisabled?: boolean
+}) => Key | null
 }

/@react-aria/selection:SelectableItemOptions

 SelectableItemOptions {
   allowsDifferentPressOrigin?: boolean
   focus?: () => void
   id?: string
   isDisabled?: boolean
+  isDraggable?: boolean
   isVirtualized?: boolean
   key: Key
   linkBehavior?: 'action' | 'selection' | 'override' | 'none' = 'action'
   onAction?: () => void
   selectionManager: MultipleSelectionManager
   shouldSelectOnPressUp?: boolean
   shouldUseVirtualFocus?: boolean
 }

@react-spectrum/s2

/@react-spectrum/s2:ListView

 ListView <T extends {}> {
   UNSAFE_className?: UnsafeClassName
   UNSAFE_style?: CSSProperties
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   autoFocus?: boolean | FocusStrategy
   children: ReactNode | ({}) => ReactNode
   defaultSelectedKeys?: 'all' | Iterable<Key>
   dependencies?: ReadonlyArray<any>
   disabledBehavior?: DisabledBehavior = "all"
   disabledKeys?: Iterable<Key>
   disallowEmptySelection?: boolean
   disallowTypeAhead?: boolean = false
+  dragAndDropHooks?: DragAndDropHooks<NoInfer<T>>
   escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
   hideLinkOutIcon?: boolean
   id?: string
   isQuiet?: boolean
   loadingState?: LoadingState
   onAction?: (Key) => void
   onLoadMore?: () => void
   onSelectionChange?: (Selection) => void
   overflowMode?: 'wrap' | 'truncate' = 'truncate'
   renderActionBar?: ('all' | Set<Key>) => ReactElement
   renderEmptyState?: (GridListRenderProps) => ReactNode
   selectedKeys?: 'all' | Iterable<Key>
   selectionMode?: SelectionMode
   selectionStyle?: 'highlight' | 'checkbox' = 'checkbox'
   shouldSelectOnPressUp?: boolean
   slot?: string | null
   styles?: StylesPropWithHeight
 }

/@react-spectrum/s2:TableView

 TableView {
   UNSAFE_className?: UnsafeClassName
   UNSAFE_style?: CSSProperties
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   children?: ReactNode
   defaultExpandedKeys?: Iterable<Key>
   defaultSelectedKeys?: 'all' | Iterable<Key>
   density?: 'compact' | 'spacious' | 'regular' = 'regular'
   disabledBehavior?: DisabledBehavior = "all"
   disabledKeys?: Iterable<Key>
   disallowEmptySelection?: boolean
+  dragAndDropHooks?: DragAndDropHooks
   escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
   expandedKeys?: Iterable<Key>
   id?: string
   isQuiet?: boolean
   onAction?: (Key) => void
   onExpandedChange?: (Set<Key>) => any
   onLoadMore?: () => any
   onResize?: (Map<Key, ColumnSize>) => void
   onResizeEnd?: (Map<Key, ColumnSize>) => void
   onResizeStart?: (Map<Key, ColumnSize>) => void
   onSelectionChange?: (Selection) => void
   onSortChange?: (SortDescriptor) => any
   overflowMode?: 'wrap' | 'truncate' = 'truncate'
   renderActionBar?: ('all' | Set<Key>) => ReactElement
   selectedKeys?: 'all' | Iterable<Key>
   selectionMode?: SelectionMode
   shouldSelectOnPressUp?: boolean
   slot?: string | null
   sortDescriptor?: SortDescriptor
   styles?: StylesPropWithHeight
   treeColumn?: Key
 }

/@react-spectrum/s2:TreeView

 TreeView <T extends {}> {
   UNSAFE_className?: UnsafeClassName
   UNSAFE_style?: CSSProperties
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   autoFocus?: boolean | FocusStrategy
   children?: ReactNode | (T) => ReactNode
   defaultExpandedKeys?: Iterable<Key>
   defaultSelectedKeys?: 'all' | Iterable<Key>
   dependencies?: ReadonlyArray<any>
   disabledBehavior?: DisabledBehavior = 'all'
   disabledKeys?: Iterable<Key>
   disallowEmptySelection?: boolean
+  dragAndDropHooks?: DragAndDropHooks<NoInfer<T>>
   escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
   expandedKeys?: Iterable<Key>
   id?: string
   items?: Iterable<T>
   onExpandedChange?: (Set<Key>) => any
   onSelectionChange?: (Selection) => void
   renderActionBar?: ('all' | Set<Key>) => ReactElement
   renderEmptyState?: (TreeEmptyStateRenderProps) => ReactNode
   selectedKeys?: 'all' | Iterable<Key>
   selectionMode?: SelectionMode
   selectionStyle?: 'highlight' | 'checkbox' = 'checkbox'
   shouldSelectOnPressUp?: boolean
   slot?: string | null
   styles?: StylesPropWithHeight
 }

/@react-spectrum/s2:ListViewProps

 ListViewProps <T> {
   UNSAFE_className?: UnsafeClassName
   UNSAFE_style?: CSSProperties
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   autoFocus?: boolean | FocusStrategy
   children: ReactNode | (T) => ReactNode
   defaultSelectedKeys?: 'all' | Iterable<Key>
   dependencies?: ReadonlyArray<any>
   disabledBehavior?: DisabledBehavior = "all"
   disabledKeys?: Iterable<Key>
   disallowEmptySelection?: boolean
   disallowTypeAhead?: boolean = false
+  dragAndDropHooks?: DragAndDropHooks<NoInfer<T>>
   escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
   hideLinkOutIcon?: boolean
   id?: string
   isQuiet?: boolean
   loadingState?: LoadingState
   onAction?: (Key) => void
   onLoadMore?: () => void
   onSelectionChange?: (Selection) => void
   overflowMode?: 'wrap' | 'truncate' = 'truncate'
   renderActionBar?: ('all' | Set<Key>) => ReactElement
   renderEmptyState?: (GridListRenderProps) => ReactNode
   selectedKeys?: 'all' | Iterable<Key>
   selectionMode?: SelectionMode
   selectionStyle?: 'highlight' | 'checkbox' = 'checkbox'
   shouldSelectOnPressUp?: boolean
   slot?: string | null
   styles?: StylesPropWithHeight
 }

/@react-spectrum/s2:TableViewProps

 TableViewProps {
   UNSAFE_className?: UnsafeClassName
   UNSAFE_style?: CSSProperties
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   children?: ReactNode
   defaultExpandedKeys?: Iterable<Key>
   defaultSelectedKeys?: 'all' | Iterable<Key>
   density?: 'compact' | 'spacious' | 'regular' = 'regular'
   disabledBehavior?: DisabledBehavior = "all"
   disabledKeys?: Iterable<Key>
   disallowEmptySelection?: boolean
+  dragAndDropHooks?: DragAndDropHooks
   escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
   expandedKeys?: Iterable<Key>
   id?: string
   isQuiet?: boolean
   onAction?: (Key) => void
   onExpandedChange?: (Set<Key>) => any
   onLoadMore?: () => any
   onResize?: (Map<Key, ColumnSize>) => void
   onResizeEnd?: (Map<Key, ColumnSize>) => void
   onResizeStart?: (Map<Key, ColumnSize>) => void
   onSelectionChange?: (Selection) => void
   onSortChange?: (SortDescriptor) => any
   overflowMode?: 'wrap' | 'truncate' = 'truncate'
   renderActionBar?: ('all' | Set<Key>) => ReactElement
   selectedKeys?: 'all' | Iterable<Key>
   selectionMode?: SelectionMode
   shouldSelectOnPressUp?: boolean
   slot?: string | null
   sortDescriptor?: SortDescriptor
   styles?: StylesPropWithHeight
   treeColumn?: Key
 }

/@react-spectrum/s2:TreeViewProps

 TreeViewProps <T> {
   UNSAFE_className?: UnsafeClassName
   UNSAFE_style?: CSSProperties
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   autoFocus?: boolean | FocusStrategy
   children?: ReactNode | (T) => ReactNode
   defaultExpandedKeys?: Iterable<Key>
   defaultSelectedKeys?: 'all' | Iterable<Key>
   dependencies?: ReadonlyArray<any>
   disabledBehavior?: DisabledBehavior = 'all'
   disabledKeys?: Iterable<Key>
   disallowEmptySelection?: boolean
+  dragAndDropHooks?: DragAndDropHooks<NoInfer<T>>
   escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
   expandedKeys?: Iterable<Key>
   id?: string
   items?: Iterable<T>
   onExpandedChange?: (Set<Key>) => any
   onSelectionChange?: (Selection) => void
   renderActionBar?: ('all' | Set<Key>) => ReactElement
   renderEmptyState?: (TreeEmptyStateRenderProps) => ReactNode
   selectedKeys?: 'all' | Iterable<Key>
   selectionMode?: SelectionMode
   selectionStyle?: 'highlight' | 'checkbox' = 'checkbox'
   shouldSelectOnPressUp?: boolean
   slot?: string | null
   styles?: StylesPropWithHeight
 }

/@react-spectrum/s2:ListViewDragPreview

+ListViewDragPreview {
+  children?: ReactNode
+  items: Array<DragItem>
+  overflowMode: ListViewStylesProps['overflowMode']
+}

/@react-spectrum/s2:TableViewDragPreview

+TableViewDragPreview {
+  children?: ReactNode
+  items: Array<DragItem>
+  overflowMode: S2TableProps['overflowMode']
+}

/@react-spectrum/s2:ListViewDragPreviewProps

+ListViewDragPreviewProps {
+  children?: ReactNode
+  items: Array<DragItem>
+  overflowMode: ListViewStylesProps['overflowMode']
+}

/@react-spectrum/s2:TableDragPreviewProps

+TableDragPreviewProps {
+  children?: ReactNode
+  items: Array<DragItem>
+  overflowMode: S2TableProps['overflowMode']
+}

LFDanLu added 2 commits April 6, 2026 16:01
when we had the tree dimensions dictated by the wrapper, there was weird broken behavior when draging over items further down the list. This doesnt happen when the height and what not are applied on the tree itself
Comment on lines +1144 to +1146
<TreeView
{...props}
styles={style({width: 300, height: 300})}
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing to look into as a followup is that the mouse drop positioning becomes quite inaccurate when the Tree's height is determined by a wrapping div rather than being set directly on the Tree. That kind of setup is being used by the other tree stories so resizing can be tested, but I could see that also being the case in the wild

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants