Skip to content

feat: add highlight selection to S2 TableView#9883

Merged
LFDanLu merged 49 commits into
mainfrom
design-prototype
May 18, 2026
Merged

feat: add highlight selection to S2 TableView#9883
LFDanLu merged 49 commits into
mainfrom
design-prototype

Conversation

@yihuiliao
Copy link
Copy Markdown
Member

@yihuiliao yihuiliao commented Apr 6, 2026

Closes

you can reference the figma but it might be out of date. notable differences are:

  1. highlight group should have rounded borders
  2. focus ring, no more focus indicator
  3. no more gray dividers inside a selected group

chromatic: https://www.chromatic.com/build?appId=5f0dd5ad2b5fc10022a2e320&number=1191

✅ 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 storybook + docs. we discussed with design to align the highlight selection styles with TreeView/ListView. the borders should have rounded borders (matching TreeView/ListView) and now have a focus ring rather than a focus indicator. there should not be gray dividers between rows when there is a selected group. spot check checkbox selection to make sure it hasn't changed. make sure to test dnd + highlight selection AND dnd + checkbox selection

🧢 Your Project:

@yihuiliao yihuiliao marked this pull request as draft April 6, 2026 23:43
@yihuiliao yihuiliao changed the base branch from main to highlight-selection-tableview April 6, 2026 23:49
@yihuiliao yihuiliao changed the base branch from highlight-selection-tableview to main April 6, 2026 23:50
@yihuiliao yihuiliao changed the base branch from main to highlight-selection-tableview April 6, 2026 23:50
@rspbot
Copy link
Copy Markdown

rspbot commented Apr 6, 2026

@yihuiliao yihuiliao changed the base branch from highlight-selection-tableview to main April 6, 2026 23:50
@rspbot
Copy link
Copy Markdown

rspbot commented Apr 14, 2026

Comment thread packages/dev/s2-docs/pages/s2/TableView.mdx
Comment thread packages/@react-spectrum/s2/src/TableView.tsx Outdated
Comment thread packages/@react-spectrum/s2/src/TableView.tsx Outdated
LFDanLu
LFDanLu previously approved these changes May 15, 2026
Copy link
Copy Markdown
Member

@LFDanLu LFDanLu left a comment

Choose a reason for hiding this comment

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

Approving for testing on Monday, some comments that can be follow up if need be

Comment on lines +708 to +714
function isPrevSelected(id: Key | undefined, state: TreeState<unknown>) {
if (id == null || !state) {
return false;
}
let keyBefore = state.collection.getKeyBefore(id);
return keyBefore != null && state.selectionManager.isSelected(keyBefore);
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

just to make sure, walking backwards doesn't have the same problem as walking forwards? Just noticing the difference in logic vs isNextSelected

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.

yeah pretty much. for getKeyBefore, i think you could really only get a content node returned if an expanded item's lastChildKey was a content node but that shouldn't happen

Comment thread packages/@react-spectrum/s2/src/TableView.tsx
Comment thread packages/@react-spectrum/s2/src/ListView.tsx
Comment on lines +990 to +994
// Sometimes the last key is a loader node, so we check the previous nodes
while (node && node.type !== 'item') {
let prevKey = node.prevKey;
node = prevKey ? state.collection.getItem(prevKey) : null;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I noticed this changed from the previous implementation, seems to have affected the border for the last row in a quiet ListView when loading

Image Image

I think this is the only style this affects, but I think the previous behavior was correct? Any other reason for this update in implementation?

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.

hm it was mainly done out of precaution but it wasn't broken before per say. if the before is what we wanted, we can remove this

@rspbot
Copy link
Copy Markdown

rspbot commented May 15, 2026

LFDanLu
LFDanLu previously approved these changes May 15, 2026
devongovett
devongovett previously approved these changes May 16, 2026
Copy link
Copy Markdown
Member

@devongovett devongovett left a comment

Choose a reason for hiding this comment

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

Looks good for testing

isSelected: '--borderColorBlue'
}
},
// When highlight selection, render gray dividers as box shadow
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why only for highlight selection?

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.

hmm i wanted to leave checkbox selection alone as much as possible and leaned towards the idea that if we could use borders we should use them. the box shadows feel more like a workaround

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

i mean why do we need box hadows for highlight selection?

forcedColorAdjust: 'none'
});

const highlightSelectionBorder = css(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

do we have to use a before for this or could we add an extra element?

return keyAfter != null && state.selectionManager.isSelected(keyAfter);
}

function isPrevSelected(id: Key | undefined, state: TreeState<unknown>) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we want to share these utils between collections? I see you exported them from TableView

Copy link
Copy Markdown
Member Author

@yihuiliao yihuiliao May 18, 2026

Choose a reason for hiding this comment

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

yeah i originally had them in a utils file but there were concerns that the collections getKeyAfter/Before/etc might not be consistent across each other. for example, TreeView had its own isNextSelected bc of the issue of running into content nodes which isn't the case for ListView or TableView

snowystinger
snowystinger previously approved these changes May 18, 2026
@yihuiliao yihuiliao added this pull request to the merge queue May 18, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch May 18, 2026
@yihuiliao yihuiliao dismissed stale reviews from snowystinger, devongovett, and LFDanLu via ca13c53 May 18, 2026 18:39
@rspbot
Copy link
Copy Markdown

rspbot commented May 18, 2026

@rspbot
Copy link
Copy Markdown

rspbot commented May 18, 2026

## API Changes

react-aria-components

/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
+  state: TableState<unknown>
 }

@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
   dragAndDropHooks?: DragAndDropHooks
   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
+  selectionStyle?: 'checkbox' | 'highlight' = 'checkbox'
   shouldSelectOnPressUp?: boolean
   slot?: string | null
   sortDescriptor?: SortDescriptor
   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
   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
+  selectionStyle?: 'checkbox' | 'highlight' = 'checkbox'
   shouldSelectOnPressUp?: boolean
   slot?: string | null
   sortDescriptor?: SortDescriptor
   styles?: StylesPropWithHeight
 }

@rspbot
Copy link
Copy Markdown

rspbot commented May 18, 2026

Agent Skills Changes

Modified (48)
Install

React Spectrum S2:

npx skills add https://d1pzu54gtk2aed.cloudfront.net/pr/dc6b6499b2f8a5623ac6b20ee1d2bc8060b0fcb8/

React Aria:

npx skills add https://d5iwopk28bdhl.cloudfront.net/pr/dc6b6499b2f8a5623ac6b20ee1d2bc8060b0fcb8/

@LFDanLu LFDanLu added this pull request to the merge queue May 18, 2026
Merged via the queue into main with commit 2acd443 May 18, 2026
30 checks passed
@LFDanLu LFDanLu deleted the design-prototype branch May 18, 2026 19:55
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.

7 participants