Skip to content

feat: add highlight selection to S2 TreeView#9769

Open
yihuiliao wants to merge 6 commits intomainfrom
highlight-selection
Open

feat: add highlight selection to S2 TreeView#9769
yihuiliao wants to merge 6 commits intomainfrom
highlight-selection

Conversation

@yihuiliao
Copy link
Member

@yihuiliao yihuiliao commented Mar 9, 2026

✅ 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 highlight selection in S2 TreeView, also updates the styles of selectionStyle="checkbox" as well

🧢 Your Project:

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

rspbot commented Mar 9, 2026

@rspbot
Copy link

rspbot commented Mar 9, 2026

@yihuiliao yihuiliao marked this pull request as ready for review March 10, 2026 00:00
@rspbot
Copy link

rspbot commented Mar 10, 2026

@rspbot
Copy link

rspbot commented Mar 10, 2026

Copy link
Member

@reidbarber reidbarber left a comment

Choose a reason for hiding this comment

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

Should we add a control to the docs example?

@rspbot
Copy link

rspbot commented Mar 10, 2026

@rspbot
Copy link

rspbot commented Mar 10, 2026

## API Changes

react-aria-components

/react-aria-components:Table

 Table {
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   children?: ReactNode
   className?: ClassNameOrFunction<TableRenderProps> = 'react-aria-Table'
-  defaultExpandedKeys?: Iterable<Key>
   defaultSelectedKeys?: 'all' | Iterable<Key>
   disabledBehavior?: DisabledBehavior = "all"
   disabledKeys?: Iterable<Key>
   disallowEmptySelection?: boolean
   dragAndDropHooks?: DragAndDropHooks
   escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
-  expandedKeys?: Iterable<Key>
-  onExpandedChange?: (Set<Key>) => any
   onRowAction?: (Key) => void
   onSelectionChange?: (Selection) => void
   onSortChange?: (SortDescriptor) => any
   render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, TableRenderProps>
   selectedKeys?: 'all' | Iterable<Key>
   selectionBehavior?: SelectionBehavior = "toggle"
   selectionMode?: SelectionMode
   shouldSelectOnPressUp?: boolean
   slot?: string | null
   sortDescriptor?: SortDescriptor
   style?: StyleOrFunction<TableRenderProps>
-  treeColumn?: Key
 }

/react-aria-components:Row

 Row <T extends {}> {
   children?: ReactNode | ({}) => ReactElement
   className?: ClassNameOrFunction<RowRenderProps> = 'react-aria-Row'
   columns?: Iterable<{}>
   dependencies?: ReadonlyArray<any>
   download?: boolean | string
-  hasChildItems?: boolean
   href?: Href
   hrefLang?: string
   id?: Key
   isDisabled?: boolean
   onClick?: (MouseEvent<FocusableElement>) => void
   onHoverChange?: (boolean) => void
   onHoverEnd?: (HoverEvent) => void
   onHoverStart?: (HoverEvent) => void
   onPress?: (PressEvent) => void
   onPressChange?: (boolean) => void
   onPressEnd?: (PressEvent) => void
   onPressStart?: (PressEvent) => void
   onPressUp?: (PressEvent) => void
   ping?: string
   referrerPolicy?: HTMLAttributeReferrerPolicy
   rel?: string
   render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, RowRenderProps>
   routerOptions?: RouterOptions
   style?: StyleOrFunction<RowRenderProps>
   target?: HTMLAttributeAnchorTarget
   textValue?: string
   value?: {}
 }

/react-aria-components:TableProps

 TableProps {
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   children?: ReactNode
   className?: ClassNameOrFunction<TableRenderProps> = 'react-aria-Table'
-  defaultExpandedKeys?: Iterable<Key>
   defaultSelectedKeys?: 'all' | Iterable<Key>
   disabledBehavior?: DisabledBehavior = "all"
   disabledKeys?: Iterable<Key>
   disallowEmptySelection?: boolean
   dragAndDropHooks?: DragAndDropHooks
   escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
-  expandedKeys?: Iterable<Key>
-  onExpandedChange?: (Set<Key>) => any
   onRowAction?: (Key) => void
   onSelectionChange?: (Selection) => void
   onSortChange?: (SortDescriptor) => any
   render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, TableRenderProps>
   selectedKeys?: 'all' | Iterable<Key>
   selectionBehavior?: SelectionBehavior = "toggle"
   selectionMode?: SelectionMode
   shouldSelectOnPressUp?: boolean
   slot?: string | null
   sortDescriptor?: SortDescriptor
   style?: StyleOrFunction<TableRenderProps>
-  treeColumn?: Key
 }

/react-aria-components:RowProps

 RowProps <T> {
   children?: ReactNode | (T) => ReactElement
   className?: ClassNameOrFunction<RowRenderProps> = 'react-aria-Row'
   columns?: Iterable<T>
   dependencies?: ReadonlyArray<any>
   download?: boolean | string
-  hasChildItems?: boolean
   href?: Href
   hrefLang?: string
   id?: Key
   isDisabled?: boolean
   onClick?: (MouseEvent<FocusableElement>) => void
   onHoverChange?: (boolean) => void
   onHoverEnd?: (HoverEvent) => void
   onHoverStart?: (HoverEvent) => void
   onPress?: (PressEvent) => void
   onPressChange?: (boolean) => void
   onPressEnd?: (PressEvent) => void
   onPressStart?: (PressEvent) => void
   onPressUp?: (PressEvent) => void
   ping?: string
   referrerPolicy?: HTMLAttributeReferrerPolicy
   rel?: string
   render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, RowRenderProps>
   routerOptions?: RouterOptions
   style?: StyleOrFunction<RowRenderProps>
   target?: HTMLAttributeAnchorTarget
   textValue?: string
   value?: T
 }

/react-aria-components:RowRenderProps

 RowRenderProps {
   allowsDragging?: boolean
-  hasChildItems: boolean
   id?: Key
   isDisabled: boolean
   isDragging?: boolean
   isDropTarget?: boolean
-  isExpanded: boolean
   isFocusVisible: boolean
   isFocusVisibleWithin: boolean
   isFocused: boolean
   isHovered: boolean
   isPressed: boolean
   isSelected: boolean
-  level: number
   selectionBehavior: SelectionBehavior
   selectionMode: SelectionMode
 }

/react-aria-components:CellRenderProps

 CellRenderProps {
   columnIndex?: number | null
-  hasChildItems: boolean
   id?: Key
-  isDisabled: boolean
-  isExpanded: boolean
   isFocusVisible: boolean
   isFocused: boolean
   isHovered: boolean
   isPressed: boolean
   isSelected: boolean
-  isTreeColumn: boolean
-  level: number
 }

/react-aria-components:TableState

 TableState <T> {
   collection: TableCollection<T>
   disabledKeys: Set<Key>
-  expandedKeys: Set<Key>
   isKeyboardNavigationDisabled: boolean
   selectionManager: SelectionManager
   setKeyboardNavigationDisabled: (boolean) => void
   showSelectionCheckboxes: boolean
   sort: (Key, 'ascending' | 'descending') => void
   sortDescriptor: SortDescriptor | null
-  toggleKey: (Key) => void
-  treeColumn: Key | null
 }

@react-aria/tag

/@react-aria/tag:TagAria

 TagAria {
   allowsRemoving: boolean
   allowsSelection: boolean
   gridCellProps: DOMAttributes
-  hasAction: boolean
   isDisabled: boolean
   isFocused: boolean
   isPressed: boolean
   isSelected: boolean
   rowProps: DOMAttributes
 }

@react-aria/test-utils

/@react-aria/test-utils:TableTester

 TableTester {
   cells: ({
     element?: HTMLElement
 }) => Array<HTMLElement>
   columns: Array<HTMLElement>
   constructor: (TableTesterOpts) => void
   findCell: ({
     text: string
 }) => HTMLElement
   findRow: ({
     rowIndexOrText: number | string
 }) => HTMLElement
   rowGroups: Array<HTMLElement>
   rowHeaders: Array<HTMLElement>
   rows: Array<HTMLElement>
   selectedRows: Array<HTMLElement>
   setInteractionType: (UserOpts['interactionType']) => void
   table: HTMLElement
-  toggleRowExpansion: (TableToggleExpansionOpts) => Promise<void>
   toggleRowSelection: (TableToggleRowOpts) => Promise<void>
   toggleSelectAll: ({
     interactionType?: UserOpts['interactionType']
 }) => Promise<void>
   triggerColumnHeaderAction: (TableColumnHeaderActionOpts) => Promise<void>
   triggerRowAction: (TableRowActionOpts) => Promise<void>
 }

@react-spectrum/s2

/@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
   escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
-  expandedKeys?: Iterable<Key>
   id?: string
   isQuiet?: boolean
   loadingState?: LoadingState
   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
   escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
   expandedKeys?: Iterable<Key>
   id?: string
   items?: Iterable<T>
   onAction?: (Key) => void
   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: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
   escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
-  expandedKeys?: Iterable<Key>
   id?: string
   isQuiet?: boolean
   loadingState?: LoadingState
   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
   escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
   expandedKeys?: Iterable<Key>
   id?: string
   items?: Iterable<T>
   onAction?: (Key) => void
   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/table

/@react-spectrum/table:TableView

 TableView <T extends {}> {
   UNSAFE_className?: string
   UNSAFE_style?: CSSProperties
   alignSelf?: Responsive<'auto' | 'normal' | 'start' | 'end' | 'center' | 'flex-start' | 'flex-end' | 'self-start' | 'self-end' | 'stretch'>
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   bottom?: Responsive<DimensionValue>
-  children: [ReactElement<TableHeaderProps<T>>, ReactElement<TableBodyProps<T>>]
+  children: [ReactElement<TableHeaderProps<{}>>, ReactElement<TableBodyProps<{}>>]
   defaultSelectedKeys?: 'all' | Iterable<Key>
   density?: 'compact' | 'regular' | 'spacious' = 'regular'
   disabledBehavior?: DisabledBehavior = "selection"
   disabledKeys?: Iterable<Key>
   dragAndDropHooks?: DragAndDropHooks<NoInfer<{}>>['dragAndDropHooks']
   end?: Responsive<DimensionValue>
   escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
   flex?: Responsive<string | number | boolean>
   flexBasis?: Responsive<number | string>
   flexGrow?: Responsive<number>
   flexShrink?: Responsive<number>
   gridArea?: Responsive<string>
   gridColumn?: Responsive<string>
   gridColumnEnd?: Responsive<string>
   gridColumnStart?: Responsive<string>
   gridRow?: Responsive<string>
   gridRowEnd?: Responsive<string>
   gridRowStart?: Responsive<string>
   height?: Responsive<DimensionValue>
   id?: string
   isHidden?: Responsive<boolean>
   isQuiet?: boolean
   justifySelf?: Responsive<'auto' | 'normal' | 'start' | 'end' | 'flex-start' | 'flex-end' | 'self-start' | 'self-end' | 'center' | 'left' | 'right' | 'stretch'>
   left?: Responsive<DimensionValue>
   margin?: Responsive<DimensionValue>
   marginBottom?: Responsive<DimensionValue>
   marginEnd?: Responsive<DimensionValue>
   marginStart?: Responsive<DimensionValue>
   marginTop?: Responsive<DimensionValue>
   marginX?: Responsive<DimensionValue>
   marginY?: Responsive<DimensionValue>
   maxHeight?: Responsive<DimensionValue>
   maxWidth?: Responsive<DimensionValue>
   minHeight?: Responsive<DimensionValue>
   minWidth?: Responsive<DimensionValue>
   onAction?: (Key) => void
   onResize?: (Map<Key, ColumnSize>) => void
   onResizeEnd?: (Map<Key, ColumnSize>) => void
   onResizeStart?: (Map<Key, ColumnSize>) => void
   onSelectionChange?: (Selection) => void
   onSortChange?: (SortDescriptor) => any
   order?: Responsive<number>
   overflowMode?: 'wrap' | 'truncate' = 'truncate'
   position?: Responsive<'static' | 'relative' | 'absolute' | 'fixed' | 'sticky'>
   renderEmptyState?: () => JSX.Element
   right?: Responsive<DimensionValue>
   selectedKeys?: 'all' | Iterable<Key>
   selectionMode?: SelectionMode
   selectionStyle?: 'checkbox' | 'highlight'
   shouldSelectOnPressUp?: boolean
   sortDescriptor?: SortDescriptor
   start?: Responsive<DimensionValue>
   top?: Responsive<DimensionValue>
   width?: Responsive<DimensionValue>
   zIndex?: Responsive<number>
 }

@react-spectrum/test-utils

/@react-spectrum/test-utils:TableTester

 TableTester {
   cells: ({
     element?: HTMLElement
 }) => Array<HTMLElement>
   columns: Array<HTMLElement>
   constructor: (TableTesterOpts) => void
   findCell: ({
     text: string
 }) => HTMLElement
   findRow: ({
     rowIndexOrText: number | string
 }) => HTMLElement
   rowGroups: Array<HTMLElement>
   rowHeaders: Array<HTMLElement>
   rows: Array<HTMLElement>
   selectedRows: Array<HTMLElement>
   setInteractionType: (UserOpts['interactionType']) => void
   table: HTMLElement
-  toggleRowExpansion: (TableToggleExpansionOpts) => Promise<void>
   toggleRowSelection: (TableToggleRowOpts) => Promise<void>
   toggleSelectAll: ({
     interactionType?: UserOpts['interactionType']
 }) => Promise<void>
   triggerColumnHeaderAction: (TableColumnHeaderActionOpts) => Promise<void>
   triggerRowAction: (TableRowActionOpts) => Promise<void>
 }

@react-stately/table

/@react-stately/table:TableState

 TableState <T> {
   collection: TableCollection<T>
   disabledKeys: Set<Key>
-  expandedKeys: Set<Key>
   isKeyboardNavigationDisabled: boolean
   selectionManager: SelectionManager
   setKeyboardNavigationDisabled: (boolean) => void
   showSelectionCheckboxes: boolean
   sort: (Key, 'ascending' | 'descending') => void
   sortDescriptor: SortDescriptor | null
-  toggleKey: (Key) => void
-  treeColumn: Key | null
 }

/@react-stately/table:TableStateProps

 TableStateProps <T> {
   allowDuplicateSelectionEvents?: boolean
   children?: [ReactElement<TableHeaderProps<T>>, ReactElement<TableBodyProps<T>>]
   collection?: TableCollection<T>
-  defaultExpandedKeys?: Iterable<Key>
   defaultSelectedKeys?: 'all' | Iterable<Key>
   disabledBehavior?: DisabledBehavior
   disabledKeys?: Iterable<Key>
   disallowEmptySelection?: boolean
-  expandedKeys?: Iterable<Key>
-  onExpandedChange?: (Set<Key>) => any
   onSelectionChange?: (Selection) => void
   onSortChange?: (SortDescriptor) => any
   selectedKeys?: 'all' | Iterable<Key>
   selectionBehavior?: SelectionBehavior
   selectionMode?: SelectionMode
   showSelectionCheckboxes?: boolean
   sortDescriptor?: SortDescriptor
-  treeColumn?: Key
 }

/@react-stately/table:TreeGridState

 TreeGridState <T> {
   collection: TableCollection<T>
   disabledKeys: Set<Key>
   expandedKeys: 'all' | Set<Key>
   isKeyboardNavigationDisabled: boolean
   keyMap: Map<Key, GridNode<T>>
   selectionManager: SelectionManager
   setKeyboardNavigationDisabled: (boolean) => void
   showSelectionCheckboxes: boolean
   sort: (Key, 'ascending' | 'descending') => void
   sortDescriptor: SortDescriptor | null
   toggleKey: (Key) => void
-  treeColumn: Key | null
   userColumnCount: number
 }

/@react-stately/table:TreeGridStateProps

 TreeGridStateProps <T> {
   UNSTABLE_defaultExpandedKeys?: 'all' | Iterable<Key>
   UNSTABLE_expandedKeys?: 'all' | Iterable<Key>
   UNSTABLE_onExpandedChange?: (Set<Key>) => any
   allowDuplicateSelectionEvents?: boolean
   children?: [ReactElement<TableHeaderProps<T>>, ReactElement<TableBodyProps<T>>]
-  defaultExpandedKeys?: Iterable<Key>
   defaultSelectedKeys?: 'all' | Iterable<Key>
   disabledBehavior?: DisabledBehavior
   disabledKeys?: Iterable<Key>
   disallowEmptySelection?: boolean
-  expandedKeys?: Iterable<Key>
-  onExpandedChange?: (Set<Key>) => any
   onSelectionChange?: (Selection) => void
   onSortChange?: (SortDescriptor) => any
   selectedKeys?: 'all' | Iterable<Key>
   selectionBehavior?: SelectionBehavior
   selectionMode?: SelectionMode
   showSelectionCheckboxes?: boolean
   sortDescriptor?: SortDescriptor
-  treeColumn?: Key
 }

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants