From 3b4b145ee6545175e3198249d65543d7d152ba44 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Fri, 13 Mar 2026 16:00:39 -0700 Subject: [PATCH 01/37] add selectionStyle prop --- packages/@react-spectrum/s2/src/ListView.tsx | 4 ++-- packages/@react-spectrum/s2/src/TableView.tsx | 12 ++++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index b749a5a4404..84b7e44a454 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -669,14 +669,14 @@ function ListSelectionCheckbox({isDisabled}: {isDisabled: boolean}) { ); } -function isNextSelected(id: Key | undefined, state: ListState) { +export function isNextSelected(id: Key | undefined, state: ListState) { if (id == null || !state) { return false; } let keyAfter = state.collection.getKeyAfter(id); return keyAfter != null && state.selectionManager.isSelected(keyAfter); } -function isPrevSelected(id: Key | undefined, state: ListState) { +export function isPrevSelected(id: Key | undefined, state: ListState) { if (id == null || !state) { return false; } diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index 94939aa6741..69ea02bde4c 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -66,6 +66,7 @@ import {GridNode} from '@react-types/grid'; import {IconContext} from './Icon'; // @ts-ignore import intlMessages from '../intl/*.json'; +import {isNextSelected, isPrevSelected} from './ListView'; import {LayoutNode} from '@react-stately/layout'; import {Menu, MenuItem, MenuSection, MenuTrigger} from './Menu'; import Nubbin from '../ui-icons/S2_MoveHorizontalTableWidget.svg'; @@ -118,7 +119,8 @@ interface S2TableProps { /** Handler that is called when more items should be loaded, e.g. while scrolling near the bottom. */ onLoadMore?: () => any, /** Provides the ActionBar to display when rows are selected in the TableView. */ - renderActionBar?: (selectedKeys: 'all' | Set) => ReactElement + renderActionBar?: (selectedKeys: 'all' | Set) => ReactElement, + selectionStyle?: 'checkbox' | 'highlight' } // TODO: Note that loadMore and loadingState are now on the Table instead of on the TableBody @@ -127,7 +129,7 @@ export interface TableViewProps extends Omit, setIsInResizeMode?:(val: boolean) => void, isInResizeMode?: boolean, selectionMode?: 'none' | 'single' | 'multiple'}>({}); +let InternalTableContext = createContext, setIsInResizeMode?:(val: boolean) => void, isInResizeMode?: boolean, selectionMode?: 'none' | 'single' | 'multiple', selectionStyle?: 'checkbox' | 'highlight'}>({}); const tableWrapper = style({ minHeight: 0, @@ -297,6 +299,7 @@ export const TableView = forwardRef(function TableView(props: TableViewProps, re onAction, onLoadMore, selectionMode = 'none', + selectionStyle = 'checkbox', ...otherProps } = props; @@ -322,8 +325,9 @@ export const TableView = forwardRef(function TableView(props: TableViewProps, re onLoadMore, isInResizeMode, setIsInResizeMode, - selectionMode - }), [isQuiet, density, overflowMode, loadingState, onLoadMore, isInResizeMode, setIsInResizeMode, selectionMode]); + selectionMode, + selectionStyle + }), [isQuiet, density, overflowMode, loadingState, onLoadMore, isInResizeMode, setIsInResizeMode, selectionMode, selectionStyle]); let scrollRef = useRef(null); let isCheckboxSelection = selectionMode === 'multiple' || selectionMode === 'single'; From 9adcfb266a200997afa21470962114805690d26c Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Wed, 18 Mar 2026 14:06:01 -0700 Subject: [PATCH 02/37] current state of highlight which sorta works but idk if i like the implementation --- packages/@react-spectrum/s2/src/ListView.tsx | 2 +- packages/@react-spectrum/s2/src/TableView.tsx | 183 ++++++++++++++---- .../s2/stories/TableView.stories.tsx | 13 ++ packages/react-aria-components/src/Table.tsx | 4 +- 4 files changed, 165 insertions(+), 37 deletions(-) diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index 84b7e44a454..ef2eaa3f65c 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -684,7 +684,7 @@ export function isPrevSelected(id: Key | undefined, state: ListState) { return keyBefore != null && state.selectionManager.isSelected(keyBefore); } -function isFirstItem(id: Key | undefined, state: ListState) { +export function isFirstItem(id: Key | undefined, state: ListState) { if (id == null || !state) { return false; } diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index 69ea02bde4c..1cb480c15fe 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -43,7 +43,6 @@ import { TableProps as RACTableProps, Rect, ResizableTableContainer, - RowRenderProps, TableBodyRenderProps, TableLayout, TableLoadMoreItem, @@ -66,7 +65,7 @@ import {GridNode} from '@react-types/grid'; import {IconContext} from './Icon'; // @ts-ignore import intlMessages from '../intl/*.json'; -import {isNextSelected, isPrevSelected} from './ListView'; +import {isFirstItem, isNextSelected, isPrevSelected} from './ListView'; import {LayoutNode} from '@react-stately/layout'; import {Menu, MenuItem, MenuSection, MenuTrigger} from './Menu'; import Nubbin from '../ui-icons/S2_MoveHorizontalTableWidget.svg'; @@ -485,7 +484,7 @@ const cellFocus = { } as const; function CellFocusRing() { - return
; + return
; } const columnStyles = style({ @@ -550,14 +549,14 @@ export interface ColumnProps extends Omit`. */ export const Column = forwardRef(function Column(props: ColumnProps, ref: DOMRef) { - let {isQuiet} = useContext(InternalTableContext); + let {isQuiet, selectionStyle} = useContext(InternalTableContext); let {allowsResizing, children, align = 'start'} = props; let domRef = useDOMRef(ref); let isMenu = allowsResizing || !!props.menuItems; return ( - columnStyles({...renderProps, isMenu, align, isQuiet})}> + columnStyles({...renderProps, isMenu, align, isQuiet, selectionStyle})}> {({allowsSorting, sortDirection, isFocusVisible, sort, startResize}) => ( <> {/* Note this is mainly for column's without a dropdown menu. If there is a dropdown menu, the button is styled to have a focus ring for simplicity @@ -901,7 +900,7 @@ export interface TableHeaderProps extends Omit, 'style export const TableHeader = /*#__PURE__*/ (forwardRef as forwardRefType)(function TableHeader({columns, dependencies, children}: TableHeaderProps, ref: DOMRef) { let scale = useScale(); let {selectionBehavior, selectionMode} = useTableOptions(); - let {isQuiet} = useContext(InternalTableContext); + let {isQuiet, selectionStyle} = useContext(InternalTableContext); let domRef = useDOMRef(ref); return ( @@ -910,7 +909,7 @@ export const TableHeader = /*#__PURE__*/ (forwardRef as forwardRefType)(function ref={domRef} className={tableHeader}> {/* Add extra columns for selection. */} - {selectionBehavior === 'toggle' && ( + {selectionBehavior === 'toggle' && selectionStyle === 'checkbox' && ( // Also isSticky prop is applied just for the layout, will decide what the RAC api should be later // @ts-ignore @@ -990,20 +989,38 @@ const cell = style({ +const row = style({ height: 'full', position: 'relative', boxSizing: 'border-box', - backgroundColor: '--rowBackgroundColor', + backgroundColor: { + default: '--rowBackgroundColor', + selectionStyle: { + highlight: { + default: '--rowBackgroundColor', + isSelected: { + default: colorMix('gray-25', 'blue-900', 10), + isHovered: colorMix('gray-25', 'blue-900', 15), + isPressed: colorMix('gray-25', 'blue-900', 15) + } + } + } + }, '--rowBackgroundColor': { type: 'backgroundColor', value: rowBackgroundColor @@ -1546,18 +1576,95 @@ const row = style({ // } // }, outlineStyle: 'none', - borderTopWidth: 0, - borderBottomWidth: 1, + // kinda unfortunate but the border is really only needed for the first item case. the issue is related to the divider + // essentially, the gray box shadow from the divider would appear on top of the blue box shadow but only for the first item + // in order for it to be on the bottom, i used border...couldn't figure out why this was only happening with box shadow + borderTopWidth: { + default: 0, + // selectionStyle: { + // highlight: { + // default: 0, + // isFirstItem: 1 + // } + // } + }, + borderBottomWidth: { + selectionStyle: { + highlight: 0, + checkbox: 1 + } + }, borderStartWidth: 0, borderEndWidth: 0, borderStyle: 'solid', borderColor: { - default: 'gray-300', - forcedColors: 'ButtonBorder' + selectionStyle: { + highlight: { + default: 'transparent', + // isFirstItem: { + // default: 'transparent', + // isSelected: 'blue-900' + // } + }, + checkbox: 'gray-300' + } + }, + '--borderColorGray': { + type: 'borderColor', + value: 'gray-300' + }, + '--borderColorBlue': { + type: 'borderColor', + value: 'blue-900' + }, + // to avoid affecting the layout, use box shadow instead + // also selected groups still have gray borders in between the items, hard to have two colors with borders because you'll get a diagonal line where the two borders meet + // have more control over how the borders are rendered using box shadow instead + // couldn't add an absolute positioned div because it would mess up the cell count + boxShadow: { + selectionStyle: { + highlight: { + default: '[inset 0px -1px 0px var(--borderColorGray)]', + isNextSelected: '[inset 0px -1px 0px var(--borderColorBlue)]', + isSelected: { + default: '[inset 0px -1px 0px var(--borderColorBlue), inset 1px 0px 0px var(--borderColorBlue), inset -1px 0px var(--borderColorBlue)]', + isNextSelected: '[inset 1px 0px 0px var(--borderColorBlue), inset -1px 0px var(--borderColorBlue), inset 0px -1px 0px var(--borderColorGray)]', + isFirstItem: { + default: '[inset 0px -1px 0px var(--borderColorBlue), inset 1px 0px 0px var(--borderColorBlue), inset -1px 0px var(--borderColorBlue)]', + isNextSelected: '[inset 1px 0px 0px var(--borderColorBlue), inset -1px 0px 0px var(--borderColorBlue), inset 0px -1px 0px var(--borderColorGray)]' + } + } + } + } + }, + '--focusIndicatorHeight': { + type: 'top', + value: { + default: 'calc(self(height) - 1px)' + } }, forcedColorAdjust: 'none' }); +const border = raw( + `&:after { + content: ""; + width: 100%; + height: 100%; + top: 0; + left: 0; + z-index: 3; + position: absolute; + box-sizing: border-box; + border-top-width: 1px; + border-bottom-width: 0px; + border-inline-start-width: 0px; + border-inline-end-width: 0px; + border-style: solid; + border-color: var(--borderColorBlue); + ` +); + const selectionCheckbox = style({ visibility: { default: 'visible', @@ -1572,7 +1679,7 @@ export interface RowProps extends Pick, 'id' | 'columns' | 'is */ export const Row = /*#__PURE__*/ (forwardRef as forwardRefType)(function Row({id, columns, children, dependencies = [], ...otherProps}: RowProps, ref: DOMRef) { let {selectionBehavior, selectionMode} = useTableOptions(); - let tableVisualOptions = useContext(InternalTableContext); + let {selectionStyle, ...tableVisualOptions} = useContext(InternalTableContext); let domRef = useDOMRef(ref); return ( @@ -1583,17 +1690,23 @@ export const Row = /*#__PURE__*/ (forwardRef as forwardRefType)(function Row row({ ...renderProps, - ...tableVisualOptions - }) + (renderProps.isFocusVisible ? ' ' + raw('&:before { content: ""; display: inline-block; position: sticky; inset-inline-start: 0; width: 3px; height: 100%; margin-inline-end: -3px; margin-block-end: 1px; z-index: 3; background-color: var(--rowFocusIndicatorColor)') : '')} + ...tableVisualOptions, + selectionStyle, + isNextSelected: isNextSelected(id, renderProps.state), + isPrevSelected: isPrevSelected(id, renderProps.state), + isFirstItem: isFirstItem(id, renderProps.state) + }) + (renderProps.isFocusVisible ? ' ' + raw('&:before { content: ""; display: inline-block; position: sticky; inset-inline-start: 0; width: 3px; height: var(--focusIndicatorHeight); margin-inline-end: -3px; margin-block-end: 1px; z-index: 3; background-color: var(--rowFocusIndicatorColor)') : '') + + (isFirstItem(id, renderProps.state) && renderProps.isSelected && selectionStyle === 'highlight' ? ' ' + border : '') + } {...otherProps}> - {selectionMode !== 'none' && selectionBehavior === 'toggle' && ( - // Not sure what we want to do with this className, in Cell it currently overrides the className that would have been applied. - // The `spread` otherProps must be after className in Cell. - // @ts-ignore - - - - )} + {selectionMode !== 'none' && selectionBehavior === 'toggle' && selectionStyle === 'checkbox' && ( + // Not sure what we want to do with this className, in Cell it currently overrides the className that would have been applied. + // The `spread` otherProps must be after className in Cell. + // @ts-ignore + + + + )} {children} diff --git a/packages/@react-spectrum/s2/stories/TableView.stories.tsx b/packages/@react-spectrum/s2/stories/TableView.stories.tsx index 1bee8605955..a7edd6d87bc 100644 --- a/packages/@react-spectrum/s2/stories/TableView.stories.tsx +++ b/packages/@react-spectrum/s2/stories/TableView.stories.tsx @@ -129,6 +129,19 @@ export const Example: StoryObj = { } }; +export const Highlight: StoryObj = { + render: StaticTable, + args: { + selectionMode: 'multiple', + selectionStyle: 'highlight', + onResize: undefined, + onResizeStart: undefined, + onResizeEnd: undefined, + onLoadMore: undefined + } +}; + + export const DisabledRows: StoryObj = { ...Example, args: { diff --git a/packages/react-aria-components/src/Table.tsx b/packages/react-aria-components/src/Table.tsx index 3e68b7bc624..abc36ffad79 100644 --- a/packages/react-aria-components/src/Table.tsx +++ b/packages/react-aria-components/src/Table.tsx @@ -1247,7 +1247,8 @@ export interface RowRenderProps extends ItemRenderProps { * What level the row has within the table. * @selector [data-level] */ - level: number + level: number, + state: TableState } export interface RowProps extends StyleRenderProps, LinkDOMProps, HoverEvents, PressEvents, Omit, 'onClick'> { @@ -1377,6 +1378,7 @@ export const Row = /*#__PURE__*/ createBranchComponent( }, values: { ...states, + state, isHovered, isFocused, isFocusVisible, From 6a79fa6df7170a5570b721dfcf6f38bdebdfc76e Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Wed, 18 Mar 2026 16:22:35 -0700 Subject: [PATCH 03/37] fix lint --- packages/@react-spectrum/s2/src/TableView.tsx | 28 ++++++++----------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index 1cb480c15fe..b01fc9354be 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -65,7 +65,7 @@ import {GridNode} from '@react-types/grid'; import {IconContext} from './Icon'; // @ts-ignore import intlMessages from '../intl/*.json'; -import {isFirstItem, isNextSelected, isPrevSelected} from './ListView'; +import {isFirstItem, isNextSelected} from './ListView'; import {LayoutNode} from '@react-stately/layout'; import {Menu, MenuItem, MenuSection, MenuTrigger} from './Menu'; import Nubbin from '../ui-icons/S2_MoveHorizontalTableWidget.svg'; @@ -371,7 +371,7 @@ export const TableView = forwardRef(function TableView(props: TableViewProps, re isCheckboxSelection, isQuiet })} - selectionBehavior="toggle" + selectionBehavior={selectionStyle === 'highlight' ? 'replace' : 'toggle'} selectionMode={selectionMode} onRowAction={onAction} {...otherProps} @@ -1003,7 +1003,7 @@ const cell = style - {selectionMode !== 'none' && selectionBehavior === 'toggle' && selectionStyle === 'checkbox' && ( + {selectionMode !== 'none' && selectionBehavior === 'toggle' && ( // Not sure what we want to do with this className, in Cell it currently overrides the className that would have been applied. // The `spread` otherProps must be after className in Cell. // @ts-ignore From e59fe39ea829241dabae0ca14b73dc99fa9c0be5 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Mon, 23 Mar 2026 17:17:52 -0700 Subject: [PATCH 04/37] code cleanup + comments --- packages/@react-spectrum/s2/src/ListView.tsx | 30 +--------- packages/@react-spectrum/s2/src/TableView.tsx | 55 ++++--------------- packages/@react-spectrum/s2/src/TreeView.tsx | 3 +- packages/@react-spectrum/s2/src/utils.ts | 33 +++++++++++ 4 files changed, 47 insertions(+), 74 deletions(-) diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index b43987f513f..8a2c3933704 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -38,6 +38,7 @@ import { import {IconContext} from './Icon'; import {ImageContext} from './Image'; import intlMessages from '../intl/*.json'; +import {isFirstItem, isLastItem, isNextSelected, isPrevSelected, useScale} from './utils'; import {Key} from '@react-types/shared'; import LinkOutIcon from '../ui-icons/LinkOut'; import {ListLayout} from 'react-stately/private/layout/ListLayout'; @@ -49,7 +50,6 @@ import {useActionBarContainer} from './ActionBar'; import {useDOMRef} from './useDOMRef'; import {useLocale} from 'react-aria/I18nProvider'; import {useLocalizedStringFormatter} from 'react-aria/useLocalizedStringFormatter'; -import {useScale} from './utils'; import {useSpectrumContextProps} from './useSpectrumContextProps'; import {Virtualizer} from 'react-aria-components/Virtualizer'; @@ -707,34 +707,6 @@ function ListSelectionCheckbox({isDisabled}: {isDisabled: boolean}) { ); } -export function isNextSelected(id: Key | undefined, state: ListState) { - if (id == null || !state) { - return false; - } - let keyAfter = state.collection.getKeyAfter(id); - return keyAfter != null && state.selectionManager.isSelected(keyAfter); -} -export function isPrevSelected(id: Key | undefined, state: ListState) { - if (id == null || !state) { - return false; - } - let keyBefore = state.collection.getKeyBefore(id); - return keyBefore != null && state.selectionManager.isSelected(keyBefore); -} - -export function isFirstItem(id: Key | undefined, state: ListState) { - if (id == null || !state) { - return false; - } - return state.collection.getFirstKey() === id; -} -function isLastItem(id: Key | undefined, state: ListState) { - if (id == null || !state) { - return false; - } - return state.collection.getLastKey() === id; -} - export function ListViewItem(props: ListViewItemProps): ReactNode { let ref = useRef(null); let {hasChildItems, ...otherProps} = props; diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index 07159fbe7af..3fb2c5b7ba8 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -58,7 +58,7 @@ import {getOwnerDocument} from 'react-aria/private/utils/domHelpers'; import {GridNode} from 'react-stately/private/grid/GridCollection'; import {IconContext} from './Icon'; import intlMessages from '../intl/*.json'; -import {isFirstItem, isNextSelected} from './ListView'; +import {isFirstItem, isNextSelected, useScale} from './utils'; import {Key} from '@react-types/shared'; import {LayoutNode} from 'react-stately/private/layout/ListLayout'; import {Menu, MenuItem, MenuSection, MenuTrigger} from './Menu'; @@ -80,7 +80,6 @@ import {useLocale} from 'react-aria/I18nProvider'; import {useLocalizedStringFormatter} from 'react-aria/useLocalizedStringFormatter'; import {useMediaQuery} from './useMediaQuery'; import {useObjectRef} from 'react-aria/useObjectRef'; -import {useScale} from './utils'; import {useSpectrumContextProps} from './useSpectrumContextProps'; import {Virtualizer} from 'react-aria-components/Virtualizer'; import {VisuallyHidden} from 'react-aria/VisuallyHidden'; @@ -995,38 +994,13 @@ const cell = style | TableState | TreeState) { + if (id == null || !state) { + return false; + } + let keyAfter = state.collection.getKeyAfter(id); + return keyAfter != null && state.selectionManager.isSelected(keyAfter); +} + +export function isPrevSelected(id: Key | undefined, state:ListState | TableState | TreeState) { + if (id == null || !state) { + return false; + } + let keyBefore = state.collection.getKeyBefore(id); + return keyBefore != null && state.selectionManager.isSelected(keyBefore); +} + +export function isFirstItem(id: Key | undefined, state: ListState | TableState | TreeState) { + if (id == null || !state) { + return false; + } + return state.collection.getFirstKey() === id; +} +export function isLastItem(id: Key | undefined, state: ListState | TableState | TreeState) { + if (id == null || !state) { + return false; + } + return state.collection.getLastKey() === id; +} From 0cb910e1a10bb502f632bfe48d92d31c582380df Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Mon, 23 Mar 2026 17:21:34 -0700 Subject: [PATCH 05/37] fix lint --- packages/@react-spectrum/s2/src/ListView.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index 8a2c3933704..d787f21b6a0 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -42,8 +42,6 @@ import {isFirstItem, isLastItem, isNextSelected, isPrevSelected, useScale} from import {Key} from '@react-types/shared'; import LinkOutIcon from '../ui-icons/LinkOut'; import {ListLayout} from 'react-stately/private/layout/ListLayout'; -// @ts-ignore -import {ListState} from 'react-stately/useListState'; import {ProgressCircle} from './ProgressCircle'; import {Text, TextContext} from './Content'; import {useActionBarContainer} from './ActionBar'; From 33cbc66ce9f949d877ec3258959d07b36b107a00 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Tue, 24 Mar 2026 12:01:58 -0700 Subject: [PATCH 06/37] add highlight selection to the docs --- packages/dev/s2-docs/pages/s2/TableView.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/dev/s2-docs/pages/s2/TableView.mdx b/packages/dev/s2-docs/pages/s2/TableView.mdx index d8ffc282050..fa74a20d996 100644 --- a/packages/dev/s2-docs/pages/s2/TableView.mdx +++ b/packages/dev/s2-docs/pages/s2/TableView.mdx @@ -14,7 +14,7 @@ export const description = 'Displays data in rows and columns, with row selectio {docs.exports.TableView.description} -```tsx render docs={docs.exports.TableView} links={docs.links} props={['selectionMode', 'overflowMode', 'density', 'isQuiet', 'disabledBehavior']} initialProps={{'aria-label': 'Files', selectionMode: 'multiple', 'treeColumn': 'name', disabledBehavior: 'selection'}} type="s2" +```tsx render docs={docs.exports.TableView} links={docs.links} props={['selectionMode', 'overflowMode', 'density', 'isQuiet', 'disabledBehavior', 'selectionStyle']} initialProps={{'aria-label': 'Files', selectionMode: 'multiple', 'treeColumn': 'name', disabledBehavior: 'selection', selectionStyle: 'checkbox'}} type="s2" "use client"; import {TableView, TableHeader, Column, TableBody, Row, Cell} from '@react-spectrum/s2'; import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; @@ -293,7 +293,7 @@ function AsyncSortTable() { Use the `href` prop on a Row to create a link. See the [getting started guide](getting-started) to learn how to integrate with your framework. Link interactions vary depending on the selection behavior. See the [selection guide](selection) for more details. -```tsx render docs={docs.exports.TableView} links={docs.links} props={['selectionMode']} initialProps={{'aria-label': 'Bookmarks', selectionMode: 'multiple'}} wide type="s2" +```tsx render docs={docs.exports.TableView} links={docs.links} props={['selectionMode', 'selectionStyle']} initialProps={{'aria-label': 'Bookmarks', selectionMode: 'multiple', selectionStyle: 'checkbox'}} wide type="s2" "use client"; import {TableView, TableHeader, Column, TableBody, Row, Cell} from '@react-spectrum/s2'; import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; @@ -635,7 +635,7 @@ export default function EditableTable(props) { Use `selectionMode` to enable single or multiple selection, and `selectedKeys` (matching each row's `id`) to control the selected rows. Return an [ActionBar](ActionBar) from `renderActionBar` to handle bulk actions, and use `onAction` for row navigation. Disable rows with `isDisabled`. See the [selection guide](selection) for details. -```tsx render docs={docs.exports.TableView} links={docs.links} props={['selectionMode', 'disallowEmptySelection']} initialProps={{selectionMode: 'multiple'}} wide type="s2" +```tsx render docs={docs.exports.TableView} links={docs.links} props={['selectionMode', 'disallowEmptySelection', 'selectionStyle']} initialProps={{selectionMode: 'multiple', selectionStyle: 'checkbox'}} wide type="s2" "use client"; import {TableView, TableHeader, Column, TableBody, Row, Cell, ActionBar, ActionButton, Text, type Selection} from '@react-spectrum/s2'; import Edit from '@react-spectrum/s2/icons/Edit'; From 8646952b67614f6fbfc17d1f18e29431baeee2c7 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Tue, 24 Mar 2026 12:02:19 -0700 Subject: [PATCH 07/37] add jsdoc comment --- packages/react-aria-components/src/Table.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/react-aria-components/src/Table.tsx b/packages/react-aria-components/src/Table.tsx index 2fad401fa65..888cadb836a 100644 --- a/packages/react-aria-components/src/Table.tsx +++ b/packages/react-aria-components/src/Table.tsx @@ -1298,6 +1298,9 @@ export interface RowRenderProps extends ItemRenderProps { * @selector [data-level] */ level: number, + /** + * State of the table. + */ state: TableState } From 042ce49c21a1df11a91317c5787efc0f23e8fa42 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Tue, 24 Mar 2026 13:08:31 -0700 Subject: [PATCH 08/37] update hcm --- packages/@react-spectrum/s2/src/TableView.tsx | 55 +++++++++++++------ 1 file changed, 39 insertions(+), 16 deletions(-) diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index 3fb2c5b7ba8..cb2e6b374de 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -483,13 +483,18 @@ const cellFocus = { outlineWidth: 2, outlineColor: { default: 'focus-ring', - forcedColors: 'Highlight' + forcedColors: { + default: 'Highlight', + selectionStyle: { + highlight: 'ButtonBorder' + } + } }, borderRadius: '[6px]' } as const; -function CellFocusRing() { - return
; +function CellFocusRing({selectionStyle} : {selectionStyle?: 'checkbox' | 'highlight'}) { + return
; } const columnStyles = style({ @@ -559,7 +564,6 @@ export const Column = forwardRef(function Column(props: ColumnProps, ref: DOMRef let domRef = useDOMRef(ref); let isMenu = allowsResizing || !!props.menuItems; - return ( columnStyles({...renderProps, isMenu, align, isQuiet, selectionStyle})}> {({allowsSorting, sortDirection, isFocusVisible, sort, startResize}) => ( @@ -567,7 +571,7 @@ export const Column = forwardRef(function Column(props: ColumnProps, ref: DOMRef {/* Note this is mainly for column's without a dropdown menu. If there is a dropdown menu, the button is styled to have a focus ring for simplicity (no need to juggle showing this focus ring if focus is on the menu button and not if it is on the resizer) */} {/* Separate absolutely positioned element because appyling the ring on the column directly via outline means the ring's required borderRadius will cause the bottom gray border to curve as well */} - {isFocusVisible && } + {isFocusVisible && } {isMenu ? ( @@ -1083,7 +1087,7 @@ export const Cell = forwardRef(function Cell(props: CellProps, ref: DOMRef } {children} - {isFocusVisible && } + {isFocusVisible && } )} @@ -1164,7 +1168,12 @@ const editableCell = style {children} - {isFocusVisible && } + {isFocusVisible && } because it messes up the cell count +// As a result, we rely on adding a css pseudo element when the row is selected + first item + highlight selection const border = css( `&:after { content: ""; From a31ba79d55201c23cc360690f4d330cdbfd65154 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Tue, 24 Mar 2026 14:23:03 -0700 Subject: [PATCH 09/37] add chromatic stories --- .../s2/chromatic/TableView.stories.tsx | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/@react-spectrum/s2/chromatic/TableView.stories.tsx b/packages/@react-spectrum/s2/chromatic/TableView.stories.tsx index 23057e6083b..09a46467371 100644 --- a/packages/@react-spectrum/s2/chromatic/TableView.stories.tsx +++ b/packages/@react-spectrum/s2/chromatic/TableView.stories.tsx @@ -33,7 +33,7 @@ const meta: Meta = { export default meta; const StaticTable = (args: TableViewProps): ReactElement => ( - + Name Type @@ -106,6 +106,34 @@ let items = [ {id: 10, foo: 'Foo 10', bar: 'Bar 10', baz: 'Baz 10', yah: 'Yah long long long 10'} ]; +export const CheckboxSelection: StoryObj = { + render: StaticTable, + args: { + selectionMode: 'multiple', + selectionStyle: 'checkbox', + selectedKeys: ['1', '2'], + styles: style({width: 500}), + onResize: undefined, + onResizeStart: undefined, + onResizeEnd: undefined, + onLoadMore: undefined + } +}; + +export const HighlightSelection: StoryObj = { + ...Example, + args: { + selectionMode: 'multiple', + selectionStyle: 'highlight', + selectedKeys: ['1', '2'], + styles: style({width: 500}), + onResize: undefined, + onResizeStart: undefined, + onResizeEnd: undefined, + onLoadMore: undefined + } +}; + const DynamicTable = (args: TableViewProps): ReactElement => ( From d7be568f1d76057fe55753417e56862b5a26a5b2 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Wed, 25 Mar 2026 13:28:51 -0700 Subject: [PATCH 10/37] add ids to table with links to fix styles --- packages/dev/s2-docs/pages/s2/TableView.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/dev/s2-docs/pages/s2/TableView.mdx b/packages/dev/s2-docs/pages/s2/TableView.mdx index fa74a20d996..98209dc4761 100644 --- a/packages/dev/s2-docs/pages/s2/TableView.mdx +++ b/packages/dev/s2-docs/pages/s2/TableView.mdx @@ -308,18 +308,18 @@ import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; {/*- begin highlight -*/} - + {/*- end highlight -*/} Adobe https://adobe.com/ January 28, 2023 - + Google https://google.com/ April 5, 2023 - + New York Times https://nytimes.com/ July 12, 2023 From 8a2ea101b704c41e17bb1d9900171b91c80aa327 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Wed, 25 Mar 2026 13:29:35 -0700 Subject: [PATCH 11/37] minor cleanup --- packages/@react-spectrum/s2/src/TableView.tsx | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index cb2e6b374de..54b43218d03 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -121,6 +121,10 @@ interface S2TableProps { onLoadMore?: () => any, /** Provides the ActionBar to display when rows are selected in the TableView. */ renderActionBar?: (selectedKeys: 'all' | Set) => ReactElement, + /** + * How selection should be displayed. + * @default 'checkbox' + */ selectionStyle?: 'checkbox' | 'highlight' } @@ -1064,7 +1068,6 @@ export const Cell = forwardRef(function Cell(props: CellProps, ref: DOMRef Date: Mon, 30 Mar 2026 10:07:58 -0700 Subject: [PATCH 12/37] fix import --- packages/@react-spectrum/s2/src/ListView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index a1bd25b92a6..df9e755de49 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -39,7 +39,7 @@ import intlMessages from '../intl/*.json'; import {isFirstItem, isLastItem, isNextSelected, isPrevSelected, useScale} from './utils'; import {Key} from '@react-types/shared'; import LinkOutIcon from '../ui-icons/LinkOut'; -import {ListLayout} from 'react-stately/private/layout/ListLayout'; +import {ListLayout} from 'react-stately/useVirtualizerState'; import {ProgressCircle} from './ProgressCircle'; import {Text, TextContext} from './Content'; import {useActionBarContainer} from './ActionBar'; From 13ee5188259ac9f37be1475e9e9718cac96a75a3 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Mon, 6 Apr 2026 16:42:32 -0700 Subject: [PATCH 13/37] prototype different version of highlight selection --- packages/@react-spectrum/s2/src/TableView.tsx | 177 +++++++++++++++--- packages/@react-spectrum/s2/src/utils.ts | 12 +- 2 files changed, 162 insertions(+), 27 deletions(-) diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index 5399694b704..f8b1c2aa5fa 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -55,7 +55,7 @@ import {getOwnerDocument} from 'react-aria/private/utils/domHelpers'; import {GridNode} from 'react-stately/private/grid/GridCollection'; import {IconContext} from './Icon'; import intlMessages from '../intl/*.json'; -import {isFirstItem, isNextSelected, useScale} from './utils'; +import {isFirstItem, isLastItem, isNextSelected, isPrevSelected, useScale} from './utils'; import {Key} from '@react-types/shared'; import {LayoutNode} from 'react-stately/useVirtualizerState'; import {Menu, MenuItem, MenuSection, MenuTrigger} from './Menu'; @@ -962,9 +962,14 @@ function VisuallyHiddenSelectAllLabel() { ); } + +// maybe for cell, i can add an absolute positioned div that only renders the divider (basically), +// i can update the border width so that the divider won't lay on top of the blue border +// updating the border width shouldn't cause any layout changes since it will be on a absolute positioned div + const commonCellStyles = { borderColor: 'transparent', - borderBottomWidth: 1, + borderBottomWidth: 0, borderTopWidth: 0, borderXWidth: 0, borderStyle: 'solid', @@ -1070,6 +1075,10 @@ const cellContent = style({ } }); +const divider = style({ + +}); + export interface CellProps extends Omit, Pick { /** @private */ isSticky?: boolean, @@ -1109,6 +1118,7 @@ export const Cell = forwardRef(function Cell(props: CellProps, ref: DOMRef {({id, isFocusVisible, hasChildItems, isTreeColumn, isExpanded, isDisabled}) => ( <> +
{hasChildItems && isTreeColumn && } @@ -1549,6 +1559,8 @@ const rowTextColor = { } as const; const row = style({ + // ...focusRing(), + // outlineOffset: -2, height: 'full', position: 'relative', boxSizing: 'border-box', @@ -1603,6 +1615,54 @@ const row = style({ // } // }, outlineStyle: 'none', + // Another issue with not being able to absolute position a div inside of Row is that we + // can't have rounded borders on all four edges when you hover over a row that is within a selected group. + // We want straight edges when the next or previous row is selected, while simultaneously wanting rounded borders + // so that when you hover over a row, you get the colored background with rounded border + '--borderBottomRadius': { + type: 'borderBottomStartRadius', + value: { + default: 'none', + selectionStyle: { + highlight: { + default: 'none', + isSelected: 'default', + isNextSelected: 'none' + } + } + } + }, + '--borderTopRadius': { + type: 'borderTopStartRadius', + value: { + default: 'none', + selectionStyle: { + highlight: { + default: 'none', + isSelected: 'default', + isPrevSelected: 'none' + } + } + } + }, + borderBottomRadius: { + selectionStyle: { + highlight: { + default: 'none', + isSelected: 'default', + isNextSelected: 'none' + } + } + }, + borderTopRadius: { + selectionStyle: { + highlight: { + default: 'none', + isSelected: 'default', + isPrevSelected: 'none' + } + } + }, borderTopWidth: 0, borderBottomWidth: { selectionStyle: { @@ -1637,22 +1697,49 @@ const row = style({ // In highlight mode, selected groups also have gray borders between the items in addition to having a blue outer border // Having a border have two colors is possible, the issue is that the browser will render a diagonal line where the two borders meet // Using box shadows gives us a bit more control on how the border colors appear - boxShadow: { - selectionStyle: { - highlight: { - default: '[inset 0px -1px 0px var(--borderColorGray)]', - isNextSelected: '[inset 0px -1px 0px var(--borderColorBlue)]', - isSelected: { - default: '[inset 0px -1px 0px var(--borderColorBlue), inset 1px 0px 0px var(--borderColorBlue), inset -1px 0px var(--borderColorBlue)]', - isNextSelected: '[inset 1px 0px 0px var(--borderColorBlue), inset -1px 0px var(--borderColorBlue), inset 0px -1px 0px var(--borderColorGray)]', - isFirstItem: { - default: '[inset 0px 1px 0px var(--borderColorBlue), inset 0px -1px 0px var(--borderColorBlue), inset 1px 0px 0px var(--borderColorBlue), inset -1px 0px var(--borderColorBlue)]', - isNextSelected: '[inset 1px 0px 0px var(--borderColorBlue), inset -1px 0px 0px var(--borderColorBlue), inset 0px -1px 0px var(--borderColorGray)]' + '--boxShadowBorder': { + type: 'borderColor', + value: { + selectionStyle: { + highlight: { + default: '[inset 0px 1px 0px var(--borderColorGray)]', + isFirstItem: '[inset 0 0 0]', + isPrevSelected: '[inset 0 0 0]', + isLastItem: '[inset 0px 1px 0px var(--borderColorGray), inset 0 -1px 0 var(--borderColorGray)]', + isSelected: { + default: '[inset 0px -1px 0px var(--borderColorBlue), inset 0px 1px 0px var(--borderColorBlue), inset 1px 0px 0px var(--borderColorBlue), inset -1px 0px var(--borderColorBlue)]', + isPrevSelected: { + default: '[inset 0px -1px 0px var(--borderColorBlue), inset 1px 0px 0px var(--borderColorBlue), inset -1px 0px var(--borderColorBlue)]' + }, + isNextSelected: { + default: '[inset 0px 1px 0px var(--borderColorBlue), inset 1px 0px 0px var(--borderColorBlue), inset -1px 0px var(--borderColorBlue)]', + isPrevSelected: '[inset 1px 0px 0px var(--borderColorBlue), inset -1px 0px var(--borderColorBlue)]' + } } } } } }, + // boxShadow: { + // selectionStyle: { + // highlight: { + // default: '[inset 0px 1px 0px var(--borderColorGray)]', + // isFirstItem: '[inset 0 0 0]', + // isPrevSelected: '[inset 0 0 0]', + // isLastItem: '[inset 0px 1px 0px var(--borderColorGray), inset 0 -1px 0 var(--borderColorGray)]', + // isSelected: { + // default: '[inset 0px -1px 0px var(--borderColorBlue), inset 0px 1px 0px var(--borderColorBlue), inset 1px 0px 0px var(--borderColorBlue), inset -1px 0px var(--borderColorBlue)]', + // isPrevSelected: { + // default: '[inset 0px -1px 0px var(--borderColorBlue), inset 1px 0px 0px var(--borderColorBlue), inset -1px 0px var(--borderColorBlue)]' + // }, + // isNextSelected: { + // default: '[inset 0px 1px 0px var(--borderColorBlue), inset 1px 0px 0px var(--borderColorBlue), inset -1px 0px var(--borderColorBlue)]', + // isPrevSelected: '[inset 1px 0px 0px var(--borderColorBlue), inset -1px 0px var(--borderColorBlue)]' + // } + // } + // } + // } + // }, '--focusIndicatorHeight': { type: 'top', value: { @@ -1667,26 +1754,61 @@ const row = style({ // Unlike the other rows, the first row needs to render a border on top when it is selected in highlight mode // Unfortunately, we can't add a position: absolute div to because it messes up the cell count // As a result, we rely on adding a css pseudo element when the row is selected + first item + highlight selection -const border = css( - `&:after { +// const border = css( +// `&:after { +// content: ""; +// width: 100%; +// height: 100%; +// top: 0; +// inset-inline-start: 0; +// z-index: 3; +// position: absolute; +// box-sizing: border-box; +// border-top-width: 1px; +// border-bottom-width: 0px; +// border-inline-start-width: 0px; +// border-inline-end-width: 0px; +// border-style: solid; +// border-color: var(--borderColorBlue); +// } +// ` +// ); + +const focusIndicator = css( + `&:before { content: ""; width: 100%; height: 100%; top: 0; inset-inline-start: 0; z-index: 3; + border-radius: 6px; position: absolute; - box-sizing: border-box; - border-top-width: 1px; - border-bottom-width: 0px; - border-inline-start-width: 0px; - border-inline-end-width: 0px; - border-style: solid; - border-color: var(--borderColorBlue); + outline-style: solid; + outline-color: var(--borderColorBlue); + outline-width: 2px; + outline-offset: -2px } ` ); +const boxShadowBorder = css( + `&:after { + content: ""; + width: 100%; + height: 100%; + position: absolute; + box-shadow: var(--boxShadowBorder); + inset: 0; + z-index: 1; + border-bottom-left-radius: var(--borderBottomRadius); + border-bottom-right-radius: var(--borderBottomRadius); + border-top-left-radius: var(--borderTopRadius); + border-top-right-radius: var(--borderTopRadius); + } + ` +); + const selectionCheckbox = style({ visibility: { default: 'visible', @@ -1701,7 +1823,9 @@ export interface RowProps extends Pick, 'id' | 'columns' | 'is */ export const Row = /*#__PURE__*/ (forwardRef as forwardRefType)(function Row({id, columns, children, dependencies = [], ...otherProps}: RowProps, ref: DOMRef) { let {selectionBehavior, selectionMode} = useTableOptions(); - let {selectionStyle, ...tableVisualOptions} = useContext(InternalTableContext); + let {selectionStyle, ...tableVisualOptions + + } = useContext(InternalTableContext); let domRef = useDOMRef(ref); return ( @@ -1715,9 +1839,10 @@ export const Row = /*#__PURE__*/ (forwardRef as forwardRefType)(function Row {selectionMode !== 'none' && selectionBehavior === 'toggle' && ( diff --git a/packages/@react-spectrum/s2/src/utils.ts b/packages/@react-spectrum/s2/src/utils.ts index aa59fafe392..9fda2637a6e 100644 --- a/packages/@react-spectrum/s2/src/utils.ts +++ b/packages/@react-spectrum/s2/src/utils.ts @@ -57,5 +57,15 @@ export function isLastItem(id: Key | undefined, state: ListState | Tabl if (id == null || !state) { return false; } - return state.collection.getLastKey() === id; + + let key = state.collection.getLastKey(); + let node = key ? state.collection.getItem(key) : null; + + // sometimes the last key is a loader node! so we check we the previous nodes + while (node && node.type !== 'item') { + let prevKey = node.prevKey; + node = prevKey ? state.collection.getItem(prevKey) : null; + } + + return node ? node.key === id : false; } From a12ae635dfa306bc4dfd0182fa41ee9070dfd34e Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Mon, 6 Apr 2026 17:00:30 -0700 Subject: [PATCH 14/37] add a comment with my new brilliant idea so i don't forget --- packages/@react-spectrum/s2/src/TableView.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index f8b1c2aa5fa..eb1df13662a 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -1693,6 +1693,13 @@ const row = style({ forcedColors: 'ButtonBorder' } }, + + // So actually...I kinda like this route of creating variables like this and then just setting them on the pseudo element. Is it ideal? Probably not...but hey, I don't have a better solution + // So if this is the case and I can do it like this, then maybe I can proceed to use borders. The main reason for using boxShadows is because I needed both the blue border on the sides and + // gray border on the bottom. If I used borders, then it would render a weird diagonal line so then I opted for box shadows. But if we do update the highlight selection design to better align + // with ListView and TreeView, then it should be possible to do this with just borders. Just define everything here as a variable and set it on the pseudo element... + // That said, if they still want the gray borders in between...then we might be out of luck. But we didn't have them in ListView so maybe they can be convinced... + // In order to prevent layout shifts, we use box shadows to render the borders since we can't add an absolute position div (it messes up the cell count due to the way Table collections are built) // In highlight mode, selected groups also have gray borders between the items in addition to having a blue outer border // Having a border have two colors is possible, the issue is that the browser will render a diagonal line where the two borders meet From 83a37a56bb389a8f0fb2f57dcca2ecfb83784fd7 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Tue, 14 Apr 2026 13:52:23 -0700 Subject: [PATCH 15/37] fix rounded borders, focus ring --- packages/@react-spectrum/s2/src/TableView.tsx | 24 +++++---- .../s2/stories/TableView.stories.tsx | 53 ++++++++++++++++++- 2 files changed, 66 insertions(+), 11 deletions(-) diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index f500e16f455..12417b7d2a4 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -490,11 +490,12 @@ const cellFocus = { } } }, - borderRadius: '[6px]' + borderRadius: '[5px]', + zIndex: 5 } as const; function CellFocusRing({selectionStyle} : {selectionStyle?: 'checkbox' | 'highlight'}) { - return
; + return
; } const columnStyles = style({ @@ -1626,7 +1627,7 @@ const row = style({ selectionStyle: { highlight: { default: 'none', - isSelected: 'default', + isSelected: '[5px]', isNextSelected: 'none' } } @@ -1639,7 +1640,7 @@ const row = style({ selectionStyle: { highlight: { default: 'none', - isSelected: 'default', + isSelected: '[5px]', isPrevSelected: 'none' } } @@ -1649,7 +1650,7 @@ const row = style({ selectionStyle: { highlight: { default: 'none', - isSelected: 'default', + isSelected: '[5px]', isNextSelected: 'none' } } @@ -1658,7 +1659,7 @@ const row = style({ selectionStyle: { highlight: { default: 'none', - isSelected: 'default', + isSelected: '[5px]', isPrevSelected: 'none' } } @@ -1712,7 +1713,10 @@ const row = style({ default: '[inset 0px 1px 0px var(--borderColorGray)]', isFirstItem: '[inset 0 0 0]', isPrevSelected: '[inset 0 0 0]', - isLastItem: '[inset 0px 1px 0px var(--borderColorGray), inset 0 -1px 0 var(--borderColorGray)]', + isLastItem: { + default: '[inset 0px 1px 0px var(--borderColorGray), inset 0 -1px 0 var(--borderColorGray)]', + isPrevSelected: '[inset 0 -1px 0 var(--borderColorGray)]' + }, isSelected: { default: '[inset 0px -1px 0px var(--borderColorBlue), inset 0px 1px 0px var(--borderColorBlue), inset 1px 0px 0px var(--borderColorBlue), inset -1px 0px var(--borderColorBlue)]', isPrevSelected: { @@ -1782,14 +1786,14 @@ const row = style({ // ); const focusIndicator = css( - `&:before { + `&:after { content: ""; width: 100%; height: 100%; top: 0; inset-inline-start: 0; z-index: 3; - border-radius: 6px; + border-radius: 5px; position: absolute; outline-style: solid; outline-color: var(--borderColorBlue); @@ -1800,7 +1804,7 @@ const focusIndicator = css( ); const boxShadowBorder = css( - `&:after { + `&:before { content: ""; width: 100%; height: 100%; diff --git a/packages/@react-spectrum/s2/stories/TableView.stories.tsx b/packages/@react-spectrum/s2/stories/TableView.stories.tsx index 2c18cb9eb3d..52af1e4b34d 100644 --- a/packages/@react-spectrum/s2/stories/TableView.stories.tsx +++ b/packages/@react-spectrum/s2/stories/TableView.stories.tsx @@ -129,8 +129,59 @@ export const Example: StoryObj = { } }; + +const HighlightTable = (args: any) => ( + + + Name + Type + Date Modified + Size + + + + Games + File folder + 6/7/2020 + 74 GB + + + Program Files + File folder + 4/7/2021 + 1.2 GB + + + bootmgr + System file + 11/20/2010 + 0.2 GB + + + bootmgr + System file + 11/20/2010 + 0.2 GB + + + bootmgr + System file + 11/20/2010 + 0.2 GB + + + bootmgr + System file + 11/20/2010 + 0.2 GB + + + +); + + export const Highlight: StoryObj = { - render: StaticTable, + render: HighlightTable, args: { selectionMode: 'multiple', selectionStyle: 'highlight', From 896f6ba5165e2f5422851d968b942800c574315b Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Fri, 24 Apr 2026 14:34:40 -0700 Subject: [PATCH 16/37] support gray dividers between rows --- packages/@react-spectrum/s2/src/TableView.tsx | 84 +++---------------- 1 file changed, 13 insertions(+), 71 deletions(-) diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index b0810edd5f9..db6361d7331 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -1013,12 +1013,7 @@ const cell = style, Pick { @@ -1119,7 +1119,7 @@ export const Cell = forwardRef(function Cell(props: CellProps, ref: DOMRef {({id, isFocusVisible, hasChildItems, isTreeColumn, isExpanded, isDisabled}) => ( <> -
+ {showDivider &&
} {hasChildItems && isTreeColumn && } @@ -1561,7 +1561,7 @@ const rowTextColor = { forcedColors: 'ButtonText' } as const; -const row = style({ +const row = style({ height: 'full', position: 'relative', boxSizing: 'border-box', @@ -1616,10 +1616,6 @@ const row = style because it messes up the cell count -// As a result, we rely on adding a css pseudo element when the row is selected + first item + highlight selection -// const border = css( -// `&:after { -// content: ""; -// width: 100%; -// height: 100%; -// top: 0; -// inset-inline-start: 0; -// z-index: 3; -// position: absolute; -// box-sizing: border-box; -// border-top-width: 1px; -// border-bottom-width: 0px; -// border-inline-start-width: 0px; -// border-inline-end-width: 0px; -// border-style: solid; -// border-color: var(--borderColorBlue); -// } -// ` -// ); - const focusIndicator = css( `&:after { content: ""; @@ -1796,7 +1738,7 @@ const focusIndicator = css( height: 100%; top: 0; inset-inline-start: 0; - z-index: 3; + z-index: 4; border-radius: 5px; position: absolute; outline-style: solid; @@ -1815,7 +1757,7 @@ const boxShadowBorder = css( position: absolute; box-shadow: var(--boxShadowBorder); inset: 0; - z-index: 1; + z-index: 2; border-bottom-left-radius: var(--borderBottomRadius); border-bottom-right-radius: var(--borderBottomRadius); border-top-left-radius: var(--borderTopRadius); From 24bf07ac97c6a3bcd8c9d8ace32ffd32361c0c69 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Mon, 4 May 2026 16:45:46 -0700 Subject: [PATCH 17/37] use borders (mainly) for highlight selection over box shadow --- packages/@react-spectrum/s2/src/TableView.tsx | 119 ++++++++++-------- 1 file changed, 68 insertions(+), 51 deletions(-) diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index db6361d7331..57f89d4fe41 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -963,14 +963,9 @@ function VisuallyHiddenSelectAllLabel() { ); } - -// maybe for cell, i can add an absolute positioned div that only renders the divider (basically), -// i can update the border width so that the divider won't lay on top of the blue border -// updating the border width shouldn't cause any layout changes since it will be on a absolute positioned div - const commonCellStyles = { borderColor: 'transparent', - borderBottomWidth: 0, + borderBottomWidth: 1, borderTopWidth: 0, borderXWidth: 0, borderStyle: 'solid', @@ -1013,7 +1008,19 @@ const cell = style, Pick { /** @private */ isSticky?: boolean, @@ -1119,7 +1117,6 @@ export const Cell = forwardRef(function Cell(props: CellProps, ref: DOMRef {({id, isFocusVisible, hasChildItems, isTreeColumn, isExpanded, isDisabled}) => ( <> - {showDivider &&
} {hasChildItems && isTreeColumn && } @@ -1642,24 +1639,45 @@ const row = style Date: Thu, 7 May 2026 11:37:24 -0700 Subject: [PATCH 18/37] fix hcm to match v3 and cleanup --- packages/@react-spectrum/s2/src/TableView.tsx | 112 ++++++++---------- 1 file changed, 48 insertions(+), 64 deletions(-) diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index db0719c66c7..3b7047703b8 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -486,18 +486,15 @@ const cellFocus = { outlineColor: { default: 'focus-ring', forcedColors: { - default: 'Highlight', - selectionStyle: { - highlight: 'ButtonBorder' - } + default: 'Highlight' } }, borderRadius: '[5px]', zIndex: 5 } as const; -function CellFocusRing({selectionStyle} : {selectionStyle?: 'checkbox' | 'highlight'}) { - return
; +function CellFocusRing() { + return
; } const columnStyles = style({ @@ -574,7 +571,7 @@ export const Column = forwardRef(function Column(props: ColumnProps, ref: DOMRef {/* Note this is mainly for column's without a dropdown menu. If there is a dropdown menu, the button is styled to have a focus ring for simplicity (no need to juggle showing this focus ring if focus is on the menu button and not if it is on the resizer) */} {/* Separate absolutely positioned element because appyling the ring on the column directly via outline means the ring's required borderRadius will cause the bottom gray border to curve as well */} - {isFocusVisible && } + {isFocusVisible && } {isMenu ? ( @@ -967,7 +964,7 @@ function VisuallyHiddenSelectAllLabel() { const commonCellStyles = { borderColor: 'transparent', - borderBottomWidth: 1, + borderBottomWidth: 0, borderTopWidth: 0, borderXWidth: 0, borderStyle: 'solid', @@ -1010,21 +1007,19 @@ const cell = style {({id, isFocusVisible, hasChildItems, isTreeColumn, isExpanded, isDisabled}) => ( <> + {showDivider &&
} {hasChildItems && isTreeColumn && } {children} - {isFocusVisible && } + {isFocusVisible && } )} @@ -1430,7 +1426,7 @@ function EditableCellInner(props: EditableCellProps & {isFocusVisible: boolean, }] ]}> {children} - {isFocusVisible && } + {isFocusVisible && } {selectionMode !== 'none' && selectionBehavior === 'toggle' && ( From 886652327dbaca7b2328b4a494880cacb3778534 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Thu, 7 May 2026 15:47:22 -0700 Subject: [PATCH 19/37] thanks copilot --- packages/@react-spectrum/s2/src/TableView.tsx | 20 +++++++------------ packages/@react-spectrum/s2/src/utils.ts | 10 +++++----- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index 3b7047703b8..a5ea4cb8efd 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -58,7 +58,7 @@ import {getOwnerDocument} from 'react-aria/private/utils/domHelpers'; import {GridNode} from 'react-stately/private/grid/GridCollection'; import {IconContext} from './Icon'; import intlMessages from '../intl/*.json'; -import {isFirstItem, isLastItem, isNextSelected, isPrevSelected, useScale} from './utils'; +import {isNextSelected, isPrevSelected, useScale} from './utils'; import {Key} from '@react-types/shared'; import {LayoutNode} from 'react-stately/useVirtualizerState'; import {Menu, MenuItem, MenuSection, MenuTrigger} from './Menu'; @@ -1556,7 +1556,7 @@ const rowTextColor = { forcedColors: 'ButtonText' } as const; -const row = style({ +const row = style({ height: 'full', position: 'relative', boxSizing: 'border-box', @@ -1713,12 +1713,6 @@ const row = style diff --git a/packages/@react-spectrum/s2/src/utils.ts b/packages/@react-spectrum/s2/src/utils.ts index 9fda2637a6e..41d3b1c1c4f 100644 --- a/packages/@react-spectrum/s2/src/utils.ts +++ b/packages/@react-spectrum/s2/src/utils.ts @@ -10,10 +10,10 @@ * governing permissions and limitations under the License. */ -import {Key} from '@react-types/shared'; -import {ListState} from 'react-stately/useListState'; -import {TableState} from 'react-stately/useTableState'; -import {TreeState} from 'react-stately/useTreeState'; +import type {Key} from '@react-types/shared'; +import type {ListState} from 'react-stately/useListState'; +import type {TableState} from 'react-stately/useTableState'; +import type {TreeState} from 'react-stately/useTreeState'; import {useMediaQuery} from './useMediaQuery'; export type Scale = 'large' | 'medium'; @@ -61,7 +61,7 @@ export function isLastItem(id: Key | undefined, state: ListState | Tabl let key = state.collection.getLastKey(); let node = key ? state.collection.getItem(key) : null; - // sometimes the last key is a loader node! so we check we the previous nodes + // 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; From 5b37c63ecbb1a29c960cc97932367918b18336d6 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Thu, 7 May 2026 16:07:56 -0700 Subject: [PATCH 20/37] cleanup --- packages/@react-spectrum/s2/src/TableView.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index a5ea4cb8efd..ae7a57832ff 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -494,7 +494,7 @@ const cellFocus = { } as const; function CellFocusRing() { - return
; + return
; } const columnStyles = style({ @@ -1779,9 +1779,7 @@ export interface RowProps extends Pick, 'id' | 'columns' | 'is */ export const Row = /*#__PURE__*/ (forwardRef as forwardRefType)(function Row({id, columns, children, dependencies = [], ...otherProps}: RowProps, ref: DOMRef) { let {selectionBehavior, selectionMode} = useTableOptions(); - let {selectionStyle, ...tableVisualOptions - - } = useContext(InternalTableContext); + let {selectionStyle, ...tableVisualOptions} = useContext(InternalTableContext); let domRef = useDOMRef(ref); let isInFooter = useContext(FooterContext); @@ -1800,7 +1798,7 @@ export const Row = /*#__PURE__*/ (forwardRef as forwardRefType)(function Row {selectionMode !== 'none' && selectionBehavior === 'toggle' && ( From 7508ef72df3dbe2c5fa0cd5e120fdd8779173742 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Fri, 8 May 2026 16:59:55 -0700 Subject: [PATCH 21/37] format --- packages/@react-spectrum/s2/src/utils.ts | 22 ++++++++++++++----- .../s2/stories/TableView.stories.tsx | 3 --- packages/react-aria-components/src/Table.tsx | 4 ++-- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/packages/@react-spectrum/s2/src/utils.ts b/packages/@react-spectrum/s2/src/utils.ts index 41d3b1c1c4f..f2c962f510a 100644 --- a/packages/@react-spectrum/s2/src/utils.ts +++ b/packages/@react-spectrum/s2/src/utils.ts @@ -31,7 +31,10 @@ export function useScale(): Scale { return 'medium'; } -export function isNextSelected(id: Key | undefined, state: ListState | TableState | TreeState) { +export function isNextSelected( + id: Key | undefined, + state: ListState | TableState | TreeState +) { if (id == null || !state) { return false; } @@ -39,7 +42,10 @@ export function isNextSelected(id: Key | undefined, state: ListState | return keyAfter != null && state.selectionManager.isSelected(keyAfter); } -export function isPrevSelected(id: Key | undefined, state:ListState | TableState | TreeState) { +export function isPrevSelected( + id: Key | undefined, + state: ListState | TableState | TreeState +) { if (id == null || !state) { return false; } @@ -47,13 +53,19 @@ export function isPrevSelected(id: Key | undefined, state:ListState | T return keyBefore != null && state.selectionManager.isSelected(keyBefore); } -export function isFirstItem(id: Key | undefined, state: ListState | TableState | TreeState) { +export function isFirstItem( + id: Key | undefined, + state: ListState | TableState | TreeState +) { if (id == null || !state) { return false; } return state.collection.getFirstKey() === id; } -export function isLastItem(id: Key | undefined, state: ListState | TableState | TreeState) { +export function isLastItem( + id: Key | undefined, + state: ListState | TableState | TreeState +) { if (id == null || !state) { return false; } @@ -61,7 +73,7 @@ export function isLastItem(id: Key | undefined, state: ListState | Tabl let key = state.collection.getLastKey(); let node = key ? state.collection.getItem(key) : null; - // Sometimes the last key is a loader node, so we check the previous nodes + // 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; diff --git a/packages/@react-spectrum/s2/stories/TableView.stories.tsx b/packages/@react-spectrum/s2/stories/TableView.stories.tsx index 284a44cc8d9..63a3a97435a 100644 --- a/packages/@react-spectrum/s2/stories/TableView.stories.tsx +++ b/packages/@react-spectrum/s2/stories/TableView.stories.tsx @@ -130,7 +130,6 @@ export const Example: StoryObj = { } }; - const HighlightTable = (args: any) => ( @@ -180,7 +179,6 @@ const HighlightTable = (args: any) => ( ); - export const Highlight: StoryObj = { render: HighlightTable, args: { @@ -193,7 +191,6 @@ export const Highlight: StoryObj = { } }; - export const DisabledRows: StoryObj = { ...Example, args: { diff --git a/packages/react-aria-components/src/Table.tsx b/packages/react-aria-components/src/Table.tsx index a9d3369c79d..a74c7d05556 100644 --- a/packages/react-aria-components/src/Table.tsx +++ b/packages/react-aria-components/src/Table.tsx @@ -1606,11 +1606,11 @@ export interface RowRenderProps extends ItemRenderProps { * What level the row has within the table. * @selector [data-level] */ - level: number, + level: number; /** * State of the table. */ - state: TableState + state: TableState; } export interface RowProps From 7acee0d33e4337acede3068c570bc66cace00b0a Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Fri, 8 May 2026 17:09:53 -0700 Subject: [PATCH 22/37] more formatting --- packages/@react-spectrum/s2/src/TableView.tsx | 125 +++++++++++++----- 1 file changed, 92 insertions(+), 33 deletions(-) diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index 5552c75e6bc..545265cf3ed 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -166,12 +166,12 @@ interface S2TableProps { /** Handler that is called when more items should be loaded, e.g. while scrolling near the bottom. */ onLoadMore?: () => any; /** Provides the ActionBar to display when rows are selected in the TableView. */ - renderActionBar?: (selectedKeys: 'all' | Set) => ReactElement, + renderActionBar?: (selectedKeys: 'all' | Set) => ReactElement; /** * How selection should be displayed. * @default 'checkbox' */ - selectionStyle?: 'checkbox' | 'highlight' + selectionStyle?: 'checkbox' | 'highlight'; } // TODO: Note that loadMore and loadingState are now on the Table instead of on the TableBody @@ -196,7 +196,15 @@ export interface TableViewProps styles?: StylesPropWithHeight; } -let InternalTableContext = createContext, setIsInResizeMode?:(val: boolean) => void, isInResizeMode?: boolean, selectionMode?: 'none' | 'single' | 'multiple', selectionStyle?: 'checkbox' | 'highlight'}>({}); +let InternalTableContext = createContext< + TableViewProps & { + layout?: S2TableLayout; + setIsInResizeMode?: (val: boolean) => void; + isInResizeMode?: boolean; + selectionMode?: 'none' | 'single' | 'multiple'; + selectionStyle?: 'checkbox' | 'highlight'; + } +>({}); const tableWrapper = style( { @@ -401,17 +409,30 @@ export const TableView = forwardRef(function TableView( [propsOnResizeEnd, setIsInResizeMode] ); - let context = useMemo(() => ({ - isQuiet, - density, - overflowMode, - loadingState, - onLoadMore, - isInResizeMode, - setIsInResizeMode, - selectionMode, - selectionStyle - }), [isQuiet, density, overflowMode, loadingState, onLoadMore, isInResizeMode, setIsInResizeMode, selectionMode, selectionStyle]); + let context = useMemo( + () => ({ + isQuiet, + density, + overflowMode, + loadingState, + onLoadMore, + isInResizeMode, + setIsInResizeMode, + selectionMode, + selectionStyle + }), + [ + isQuiet, + density, + overflowMode, + loadingState, + onLoadMore, + isInResizeMode, + setIsInResizeMode, + selectionMode, + selectionStyle + ] + ); let scrollRef = useRef(null); let isCheckboxSelection = selectionMode === 'multiple' || selectionMode === 'single'; @@ -450,11 +471,13 @@ export const TableView = forwardRef(function TableView( paddingBottom: actionBarHeight > 0 ? actionBarHeight + 8 : 0, scrollPaddingBottom: actionBarHeight > 0 ? actionBarHeight + 8 : 0 }} - className={renderProps => table({ - ...renderProps, - isCheckboxSelection, - isQuiet - })} + className={renderProps => + table({ + ...renderProps, + isCheckboxSelection, + isQuiet + }) + } selectionBehavior={selectionStyle === 'highlight' ? 'replace' : 'toggle'} selectionMode={selectionMode} onRowAction={onAction} @@ -464,12 +487,23 @@ export const TableView = forwardRef(function TableView( onSelectionChange={onSelectionChange} /> + {actionBar} ); }); const centeredWrapper = style({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: 'full', + height: 'full' +}); + +export interface TableBodyProps extends Omit< + RACTableBodyProps, + 'style' | 'className' | 'render' | keyof GlobalDOMAttributes > {} /** @@ -649,10 +683,24 @@ export const Column = forwardRef(function Column(props: ColumnProps, ref: DOMRef let isMenu = allowsResizing || !!props.menuItems; return ( - columnStyles({...renderProps, isMenu, align, isQuiet, selectionStyle})}> + + columnStyles({...renderProps, isMenu, align, isQuiet, selectionStyle}) + }> {({allowsSorting, sortDirection, isFocusVisible, sort, startResize}) => ( <> + {/* Note this is mainly for column's without a dropdown menu. If there is a dropdown menu, the button is styled to have a focus ring for simplicity (no need to juggle showing this focus ring if focus is on the menu button and not if it is on the resizer) */} + {/* Separate absolutely positioned element because appyling the ring on the column directly via outline means the ring's required borderRadius will cause the bottom gray border to curve as well */} + {isFocusVisible && } + {isMenu ? ( + {/* Add extra columns for selection. */} - {selectionBehavior === 'toggle' && selectionStyle === 'checkbox' && ( + {selectionBehavior === 'toggle' && selectionStyle === 'checkbox' && ( // Also isSticky prop is applied just for the layout, will decide what the RAC api should be later + // @ts-ignore ( <> {showDivider &&
} - {hasChildItems && isTreeColumn && + {hasChildItems && isTreeColumn && ( )} @@ -1249,8 +1296,12 @@ export const Cell = forwardRef(function Cell(props: CellProps, ref: DOMRef({ gridArea: 'expand-button', color: { @@ -1334,7 +1385,7 @@ const editableCell = style< default: 'disabled', forcedColors: 'GrayText' }, - forcedColors: 'ButtonText' + forcedColors: 'ButtonText' }, paddingY: centerPadding(), boxSizing: 'border-box', @@ -1737,7 +1788,10 @@ const rowTextColor = { forcedColors: 'ButtonText' } as const; -const row = style({ +const row = style< + RowRenderProps & + S2TableProps & {isInFooter?: boolean; isNextSelected?: boolean; isPrevSelected?: boolean} +>({ height: 'full', position: 'relative', boxSizing: 'border-box', @@ -1988,16 +2042,21 @@ export const Row = /*#__PURE__*/ (forwardRef as forwardRefType)(function Row row({ - ...renderProps, - ...tableVisualOptions, - isInFooter, - isNextSelected: isNextSelected(renderProps.id, renderProps.state), - isPrevSelected: isPrevSelected(renderProps.id, renderProps.state) - }) + (renderProps.isFocusVisible ? ' ' + focusIndicator : '') + (selectionStyle === 'highlight' ? ' ' + highlightSelectionBorder : '') className={renderProps => row({ ...renderProps, + ...tableVisualOptions, + selectionStyle, + isInFooter, + isNextSelected: isNextSelected(renderProps.id, renderProps.state), + isPrevSelected: isPrevSelected(renderProps.id, renderProps.state) + }) + + (renderProps.isFocusVisible ? ' ' + focusIndicator : '') + + (selectionStyle === 'highlight' ? ' ' + highlightSelectionBorder : '') + } + {...otherProps}> + {selectionMode !== 'none' && selectionBehavior === 'toggle' && ( + // Not sure what we want to do with this className, in Cell it currently overrides the className that would have been applied. // The `spread` otherProps must be after className in Cell. // @ts-ignore From caa8942374c80607958e8ddc3416f2210db84294 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Fri, 8 May 2026 17:11:56 -0700 Subject: [PATCH 23/37] fix ts error --- packages/@react-spectrum/s2/src/TableView.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index 545265cf3ed..56d42496c5e 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -1095,9 +1095,9 @@ export const TableHeader = /*#__PURE__*/ (forwardRef as forwardRefType)(function className={tableHeader}> {/* Add extra columns for selection. */} {selectionBehavior === 'toggle' && selectionStyle === 'checkbox' && ( - // Also isSticky prop is applied just for the layout, will decide what the RAC api should be later - // @ts-ignore Date: Tue, 12 May 2026 14:03:05 -0700 Subject: [PATCH 24/37] hcm fixes --- packages/@react-spectrum/s2/src/TableView.tsx | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index 56d42496c5e..f09601967a0 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -1381,11 +1381,11 @@ const editableCell = style< color: { default: baseColor('neutral'), isSaving: baseColor('neutral-subdued'), + forcedColors: 'ButtonText', isDisabled: { default: 'disabled', forcedColors: 'GrayText' - }, - forcedColors: 'ButtonText' + } }, paddingY: centerPadding(), boxSizing: 'border-box', @@ -1748,7 +1748,6 @@ const rowBackgroundColor = { default: 'gray-25', isQuiet: '--s2-container-bg' }, - isFocusVisibleWithin: colorMix('gray-25', 'gray-900', 7), // table-row-hover-color isHovered: colorMix('gray-25', 'gray-900', 7), // table-row-hover-color isPressed: colorMix('gray-25', 'gray-900', 10), // table-row-hover-color isSelected: { @@ -1760,7 +1759,6 @@ const rowBackgroundColor = { selectionStyle: { highlight: { default: 'gray-25', - isFocusVisibleWithin: colorMix('gray-25', 'gray-900', 7), // table-row-hover-color isHovered: colorMix('gray-25', 'gray-900', 7), // table-row-hover-color isPressed: colorMix('gray-25', 'gray-900', 10), // table-row-hover-color isSelected: { @@ -1780,12 +1778,12 @@ const rowBackgroundColor = { const rowTextColor = { default: baseColor('neutral-subdued'), isSelected: baseColor('neutral'), + forcedColors: 'ButtonText', isDisabled: { default: 'disabled', forcedColors: 'GrayText' }, - isInFooter: 'neutral', - forcedColors: 'ButtonText' + isInFooter: 'neutral' } as const; const row = style< @@ -1941,10 +1939,11 @@ const row = style< selectionStyle: { highlight: { default: '[inset 0 -1px 0px var(--borderColorGray)]', - isNextSelected: '[inset 0 0 0 var(--borderColorGray)]', - isSelected: { - isNextSelected: '[inset 0 -1px 0px var(--borderColorGray)]' - } + isNextSelected: '[inset 0 0 0 var(--borderColorGray)]' + // TODO: Determine if we want to support gray dividers between selected grouped rows + // isSelected: { + // isNextSelected: '[inset 0 -1px 0px var(--borderColorGray)]' + // } } } }, @@ -2041,7 +2040,7 @@ export const Row = /*#__PURE__*/ (forwardRef as forwardRefType)(function Row row({ ...renderProps, From 09ca445dad9c353f7eb37fdce3205a344406d22d Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Tue, 12 May 2026 14:19:21 -0700 Subject: [PATCH 25/37] adjust z index --- packages/@react-spectrum/s2/src/TableView.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index f09601967a0..c874690ec90 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -598,7 +598,6 @@ const cellFocus = { } }, borderRadius: '[5px]', - zIndex: 5 } as const; function CellFocusRing() { @@ -1186,7 +1185,9 @@ const divider = style({ width: '[1px]', height: 'full', insetEnd: 0, - backgroundColor: 'var(--borderColorGray)' + backgroundColor: 'var(--borderColorGray)', + // set a z index on the divider so the highlight selection border and focus ring can override it + zIndex: 1 }); const stickyCell = { @@ -1952,7 +1953,6 @@ const row = style< isInFooter: 'bold' }, isolation: 'isolate', - zIndex: 3, forcedColorAdjust: 'none' }); @@ -1986,8 +1986,8 @@ const focusIndicator = css( width: 100%; height: 100%; top: 0; + z-index: 2 ; inset-inline-start: 0; - z-index: 4; border-radius: 5px; position: absolute; outline-style: solid; From f5327709649e02875f2218c415f142fe3414fe6f Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Tue, 12 May 2026 14:22:54 -0700 Subject: [PATCH 26/37] cleanup cell styles --- packages/@react-spectrum/s2/src/TableView.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index c874690ec90..a28a55fb095 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -1130,11 +1130,6 @@ function VisuallyHiddenSelectAllLabel() { } const commonCellStyles = { - borderColor: 'transparent', - borderBottomWidth: 0, - borderTopWidth: 0, - borderXWidth: 0, - borderStyle: 'solid', position: 'relative', color: '--rowTextColor', outlineStyle: 'none', From a5a30237b2fd337d91bd082e6234db58c6be8b2a Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Tue, 12 May 2026 14:52:56 -0700 Subject: [PATCH 27/37] update utils --- packages/@react-spectrum/s2/src/ListView.tsx | 55 ++++++++++++++++++- packages/@react-spectrum/s2/src/TableView.tsx | 26 ++++++++- packages/@react-spectrum/s2/src/TreeView.tsx | 23 +++++++- packages/@react-spectrum/s2/src/utils.ts | 55 ------------------- 4 files changed, 101 insertions(+), 58 deletions(-) diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index fa91d79107d..c1c28078ef9 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -64,16 +64,17 @@ import { import {IconContext} from './Icon'; import {ImageContext} from './Image'; import intlMessages from '../intl/*.json'; -import {isFirstItem, isLastItem, isNextSelected, isPrevSelected, useScale} from './utils'; import {Key} from '@react-types/shared'; import LinkOutIcon from '../ui-icons/LinkOut'; import {ListLayout} from 'react-stately/useVirtualizerState'; +import type {ListState} from 'react-stately/useListState'; import {ProgressCircle} from './ProgressCircle'; import {Text, TextContext} from './Content'; import {useActionBarContainer} from './ActionBar'; import {useDOMRef} from './useDOMRef'; import {useLocale} from 'react-aria/I18nProvider'; import {useLocalizedStringFormatter} from 'react-aria/useLocalizedStringFormatter'; +import {useScale} from './utils'; import {useSpectrumContextProps} from './useSpectrumContextProps'; import {Virtualizer} from 'react-aria-components/Virtualizer'; @@ -951,3 +952,55 @@ export function ListViewItem(props: ListViewItemProps): ReactNode { ); } + +function isNextSelected( + id: Key | undefined, + state: ListState +) { + if (id == null || !state) { + return false; + } + let keyAfter = state.collection.getKeyAfter(id); + return keyAfter != null && state.selectionManager.isSelected(keyAfter); +} + +function isPrevSelected( + id: Key | undefined, + state: ListState +) { + if (id == null || !state) { + return false; + } + let keyBefore = state.collection.getKeyBefore(id); + return keyBefore != null && state.selectionManager.isSelected(keyBefore); +} + +function isFirstItem( + id: Key | undefined, + state: ListState +) { + if (id == null || !state) { + return false; + } + return state.collection.getFirstKey() === id; +} + +function isLastItem( + id: Key | undefined, + state: ListState +) { + if (id == null || !state) { + return false; + } + + let key = state.collection.getLastKey(); + let node = key ? state.collection.getItem(key) : null; + + // 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; + } + + return node ? node.key === id : false; +} diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index a28a55fb095..32296401f74 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -89,7 +89,6 @@ import {getOwnerDocument} from 'react-aria/private/utils/domHelpers'; import {GridNode} from 'react-stately/private/grid/GridCollection'; import {IconContext} from './Icon'; import intlMessages from '../intl/*.json'; -import {isNextSelected, isPrevSelected, useScale} from './utils'; import {Key} from '@react-types/shared'; import {LayoutNode} from 'react-stately/useVirtualizerState'; import {Menu, MenuItem, MenuSection, MenuTrigger} from './Menu'; @@ -120,6 +119,7 @@ import {Rect, TableLayout, Virtualizer} from 'react-aria-components/Virtualizer' import SortDownArrow from '../s2wf-icons/S2_Icon_SortDown_20_N.svg'; import SortUpArrow from '../s2wf-icons/S2_Icon_SortUp_20_N.svg'; import {Button as SpectrumButton} from './Button'; +import type {TableState} from 'react-stately/useTableState'; import {useActionBarContainer} from './ActionBar'; import {useDOMRef} from './useDOMRef'; import {useLayoutEffect} from 'react-aria/private/utils/useLayoutEffect'; @@ -127,6 +127,7 @@ import {useLocale} from 'react-aria/I18nProvider'; import {useLocalizedStringFormatter} from 'react-aria/useLocalizedStringFormatter'; import {useMediaQuery} from './useMediaQuery'; import {useObjectRef} from 'react-aria/useObjectRef'; +import {useScale} from './utils'; import {useSpectrumContextProps} from './useSpectrumContextProps'; import {VisuallyHidden} from 'react-aria/VisuallyHidden'; @@ -2091,3 +2092,26 @@ export const TableFooter = /*#__PURE__*/ (forwardRef as forwardRefType)(function ); }); + +export function isNextSelected( + id: Key | undefined, + state: TableState +) { + if (id == null || !state) { + return false; + } + let keyAfter = state.collection.getKeyAfter(id); + return keyAfter != null && state.selectionManager.isSelected(keyAfter); +} + +export function isPrevSelected( + id: Key | undefined, + state: TableState +) { + if (id == null || !state) { + return false; + } + let keyBefore = state.collection.getKeyBefore(id); + return keyBefore != null && state.selectionManager.isSelected(keyBefore); +} + diff --git a/packages/@react-spectrum/s2/src/TreeView.tsx b/packages/@react-spectrum/s2/src/TreeView.tsx index 3e99d6611a3..a7250be51c6 100644 --- a/packages/@react-spectrum/s2/src/TreeView.tsx +++ b/packages/@react-spectrum/s2/src/TreeView.tsx @@ -27,7 +27,6 @@ import { } from './style-utils' with {type: 'macro'}; import {IconContext} from './Icon'; import intlMessages from '../intl/*.json'; -import {isFirstItem, isPrevSelected, useScale} from './utils'; import {ListLayout} from 'react-stately/useVirtualizerState'; import {ProgressCircle} from './ProgressCircle'; import {Provider, useContextProps} from 'react-aria-components/slots'; @@ -57,6 +56,7 @@ import {useActionBarContainer} from './ActionBar'; import {useDOMRef} from './useDOMRef'; import {useLocale} from 'react-aria/I18nProvider'; import {useLocalizedStringFormatter} from 'react-aria/useLocalizedStringFormatter'; +import {useScale} from './utils'; import {Virtualizer} from 'react-aria-components/Virtualizer'; interface S2TreeProps { @@ -701,3 +701,24 @@ function isNextSelected(id: Key | undefined, state: TreeState) { return keyAfter != null && state.selectionManager.isSelected(keyAfter); } + +function isPrevSelected( + id: Key | undefined, + state: TreeState +) { + if (id == null || !state) { + return false; + } + let keyBefore = state.collection.getKeyBefore(id); + return keyBefore != null && state.selectionManager.isSelected(keyBefore); +} + +function isFirstItem( + id: Key | undefined, + state: TreeState +) { + if (id == null || !state) { + return false; + } + return state.collection.getFirstKey() === id; +} diff --git a/packages/@react-spectrum/s2/src/utils.ts b/packages/@react-spectrum/s2/src/utils.ts index f2c962f510a..d2957cb9ae5 100644 --- a/packages/@react-spectrum/s2/src/utils.ts +++ b/packages/@react-spectrum/s2/src/utils.ts @@ -10,10 +10,6 @@ * governing permissions and limitations under the License. */ -import type {Key} from '@react-types/shared'; -import type {ListState} from 'react-stately/useListState'; -import type {TableState} from 'react-stately/useTableState'; -import type {TreeState} from 'react-stately/useTreeState'; import {useMediaQuery} from './useMediaQuery'; export type Scale = 'large' | 'medium'; @@ -30,54 +26,3 @@ export function useScale(): Scale { return 'medium'; } - -export function isNextSelected( - id: Key | undefined, - state: ListState | TableState | TreeState -) { - if (id == null || !state) { - return false; - } - let keyAfter = state.collection.getKeyAfter(id); - return keyAfter != null && state.selectionManager.isSelected(keyAfter); -} - -export function isPrevSelected( - id: Key | undefined, - state: ListState | TableState | TreeState -) { - if (id == null || !state) { - return false; - } - let keyBefore = state.collection.getKeyBefore(id); - return keyBefore != null && state.selectionManager.isSelected(keyBefore); -} - -export function isFirstItem( - id: Key | undefined, - state: ListState | TableState | TreeState -) { - if (id == null || !state) { - return false; - } - return state.collection.getFirstKey() === id; -} -export function isLastItem( - id: Key | undefined, - state: ListState | TableState | TreeState -) { - if (id == null || !state) { - return false; - } - - let key = state.collection.getLastKey(); - let node = key ? state.collection.getItem(key) : null; - - // 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; - } - - return node ? node.key === id : false; -} From e758d36ea155e22fbc096a014b1a693e55efbfed Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Tue, 12 May 2026 14:53:36 -0700 Subject: [PATCH 28/37] format --- packages/@react-spectrum/s2/src/ListView.tsx | 20 ++++--------------- packages/@react-spectrum/s2/src/TableView.tsx | 15 ++++---------- packages/@react-spectrum/s2/src/TreeView.tsx | 10 ++-------- 3 files changed, 10 insertions(+), 35 deletions(-) diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index c1c28078ef9..b20d4b32912 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -953,10 +953,7 @@ export function ListViewItem(props: ListViewItemProps): ReactNode { ); } -function isNextSelected( - id: Key | undefined, - state: ListState -) { +function isNextSelected(id: Key | undefined, state: ListState) { if (id == null || !state) { return false; } @@ -964,10 +961,7 @@ function isNextSelected( return keyAfter != null && state.selectionManager.isSelected(keyAfter); } -function isPrevSelected( - id: Key | undefined, - state: ListState -) { +function isPrevSelected(id: Key | undefined, state: ListState) { if (id == null || !state) { return false; } @@ -975,20 +969,14 @@ function isPrevSelected( return keyBefore != null && state.selectionManager.isSelected(keyBefore); } -function isFirstItem( - id: Key | undefined, - state: ListState -) { +function isFirstItem(id: Key | undefined, state: ListState) { if (id == null || !state) { return false; } return state.collection.getFirstKey() === id; } -function isLastItem( - id: Key | undefined, - state: ListState -) { +function isLastItem(id: Key | undefined, state: ListState) { if (id == null || !state) { return false; } diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index 32296401f74..09d97908370 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -598,7 +598,7 @@ const cellFocus = { default: 'Highlight' } }, - borderRadius: '[5px]', + borderRadius: '[5px]' } as const; function CellFocusRing() { @@ -1182,7 +1182,7 @@ const divider = style({ height: 'full', insetEnd: 0, backgroundColor: 'var(--borderColorGray)', - // set a z index on the divider so the highlight selection border and focus ring can override it + // set a z index on the divider so the highlight selection border and focus ring can override it zIndex: 1 }); @@ -2093,10 +2093,7 @@ export const TableFooter = /*#__PURE__*/ (forwardRef as forwardRefType)(function ); }); -export function isNextSelected( - id: Key | undefined, - state: TableState -) { +export function isNextSelected(id: Key | undefined, state: TableState) { if (id == null || !state) { return false; } @@ -2104,14 +2101,10 @@ export function isNextSelected( return keyAfter != null && state.selectionManager.isSelected(keyAfter); } -export function isPrevSelected( - id: Key | undefined, - state: TableState -) { +export function isPrevSelected(id: Key | undefined, state: TableState) { if (id == null || !state) { return false; } let keyBefore = state.collection.getKeyBefore(id); return keyBefore != null && state.selectionManager.isSelected(keyBefore); } - diff --git a/packages/@react-spectrum/s2/src/TreeView.tsx b/packages/@react-spectrum/s2/src/TreeView.tsx index a7250be51c6..d8e78dea1ba 100644 --- a/packages/@react-spectrum/s2/src/TreeView.tsx +++ b/packages/@react-spectrum/s2/src/TreeView.tsx @@ -702,10 +702,7 @@ function isNextSelected(id: Key | undefined, state: TreeState) { return keyAfter != null && state.selectionManager.isSelected(keyAfter); } -function isPrevSelected( - id: Key | undefined, - state: TreeState -) { +function isPrevSelected(id: Key | undefined, state: TreeState) { if (id == null || !state) { return false; } @@ -713,10 +710,7 @@ function isPrevSelected( return keyBefore != null && state.selectionManager.isSelected(keyBefore); } -function isFirstItem( - id: Key | undefined, - state: TreeState -) { +function isFirstItem(id: Key | undefined, state: TreeState) { if (id == null || !state) { return false; } From 2487380ad0f4cd776fe71a3e300d05087a99fc7b Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Tue, 12 May 2026 16:25:35 -0700 Subject: [PATCH 29/37] format --- packages/@react-spectrum/s2/src/TableView.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index b5d4a164a58..3b9a1521988 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -172,6 +172,7 @@ interface S2TableProps { renderActionBar?: (selectedKeys: 'all' | Set) => ReactElement; /** * How selection should be displayed. + * * @default 'checkbox' */ selectionStyle?: 'checkbox' | 'highlight'; From 25683b20d6b50687f38ff25a72705d0465e2c2a3 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Fri, 15 May 2026 15:01:30 -0700 Subject: [PATCH 30/37] address review comments --- packages/@react-spectrum/s2/src/ListView.tsx | 6 ------ packages/@react-spectrum/s2/src/TableView.tsx | 13 +++++++------ 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index 2e484dc3361..fad05f7ed39 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -987,11 +987,5 @@ function isLastItem(id: Key | undefined, state: ListState) { let key = state.collection.getLastKey(); let node = key ? state.collection.getItem(key) : null; - // 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; - } - return node ? node.key === id : false; } diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index 3b9a1521988..cb5986220e2 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -88,6 +88,7 @@ import { import {getOwnerDocument} from 'react-aria/private/utils/domHelpers'; import {GridNode} from 'react-stately/private/grid/GridCollection'; import {IconContext} from './Icon'; +// @ts-ignore import intlMessages from '../intl/*.json'; import {Key} from '@react-types/shared'; import {LayoutNode} from 'react-stately/useVirtualizerState'; @@ -602,7 +603,9 @@ const cellFocus = { default: 'Highlight' } }, - borderRadius: '[5px]' + borderRadius: '[5px]', + // We need to render the cell focus ring on top of the cell divider if there is one. The divider has a z-index of 1. + zIndex: 2 } as const; function CellFocusRing() { @@ -693,7 +696,7 @@ export const Column = forwardRef(function Column(props: ColumnProps, ref: DOMRef ref={domRef} style={{borderInlineEndColor: 'transparent'}} className={renderProps => - columnStyles({...renderProps, isMenu, align, isQuiet, selectionStyle}) + columnStyles({...renderProps, isMenu, align, isQuiet}) }> {({allowsSorting, sortDirection, isFocusVisible, sort, startResize}) => ( <> @@ -1795,9 +1798,7 @@ const row = style< height: 'full', position: 'relative', boxSizing: 'border-box', - backgroundColor: { - default: '--rowBackgroundColor' - }, + backgroundColor: '--rowBackgroundColor', '--rowBackgroundColor': { type: 'backgroundColor', value: rowBackgroundColor @@ -2055,7 +2056,7 @@ export const Row = /*#__PURE__*/ (forwardRef as forwardRefType)(function Row - {selectionMode !== 'none' && selectionBehavior === 'toggle' && ( + {selectionMode !== 'none' && selectionBehavior === 'toggle' && selectionStyle === 'checkbox' && ( // Not sure what we want to do with this className, in Cell it currently overrides the className that would have been applied. // The `spread` otherProps must be after className in Cell. // @ts-ignore From 0f226422bb9ba099762caa1ba882e40f1d1f1c2d Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Fri, 15 May 2026 15:02:25 -0700 Subject: [PATCH 31/37] fix formatting --- packages/@react-spectrum/s2/src/TableView.tsx | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index cb5986220e2..266787e4992 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -695,9 +695,7 @@ export const Column = forwardRef(function Column(props: ColumnProps, ref: DOMRef {...props} ref={domRef} style={{borderInlineEndColor: 'transparent'}} - className={renderProps => - columnStyles({...renderProps, isMenu, align, isQuiet}) - }> + className={renderProps => columnStyles({...renderProps, isMenu, align, isQuiet})}> {({allowsSorting, sortDirection, isFocusVisible, sort, startResize}) => ( <> {/* Note this is mainly for column's without a dropdown menu. If there is a dropdown menu, the button is styled to have a focus ring for simplicity @@ -2056,14 +2054,16 @@ export const Row = /*#__PURE__*/ (forwardRef as forwardRefType)(function Row - {selectionMode !== 'none' && selectionBehavior === 'toggle' && selectionStyle === 'checkbox' && ( - // Not sure what we want to do with this className, in Cell it currently overrides the className that would have been applied. - // The `spread` otherProps must be after className in Cell. - // @ts-ignore - - - - )} + {selectionMode !== 'none' && + selectionBehavior === 'toggle' && + selectionStyle === 'checkbox' && ( + // Not sure what we want to do with this className, in Cell it currently overrides the className that would have been applied. + // The `spread` otherProps must be after className in Cell. + // @ts-ignore + + + + )} {children} From 5d04f4281a6da169a923813a2cd258e6466df965 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Fri, 15 May 2026 15:10:05 -0700 Subject: [PATCH 32/37] fix lint --- packages/@react-spectrum/s2/src/TableView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index 266787e4992..fbbc027f3f7 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -685,7 +685,7 @@ export interface ColumnProps extends Omit< * A column within a ``. */ export const Column = forwardRef(function Column(props: ColumnProps, ref: DOMRef) { - let {isQuiet, selectionStyle} = useContext(InternalTableContext); + let {isQuiet} = useContext(InternalTableContext); let {allowsResizing, children, align = 'start'} = props; let domRef = useDOMRef(ref); let isMenu = allowsResizing || !!props.menuItems; From 7ec644b377f6b49dfade9b057a1460eb14b3b91d Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Mon, 18 May 2026 10:25:48 -0700 Subject: [PATCH 33/37] fix --- packages/@react-spectrum/s2/src/TableView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index 66dee5e2d9a..db51ef5a5fa 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -1156,7 +1156,7 @@ export const TableHeader = /*#__PURE__*/ (forwardRef as forwardRefType)(function >({columns, dependencies, children}: TableHeaderProps, ref: DOMRef) { let scale = useScale(); let {selectionBehavior, selectionMode, allowsDragging} = useTableOptions(); - let {isQuiet} = useContext(InternalTableContext); + let {isQuiet, selectionStyle} = useContext(InternalTableContext); let domRef = useDOMRef(ref); let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/s2'); From ca13c537fbf78d563833c63ea759a85694f3e126 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Mon, 18 May 2026 11:39:19 -0700 Subject: [PATCH 34/37] fix dnd with highlight selection --- packages/@react-spectrum/s2/src/TableView.tsx | 46 ++++--------------- 1 file changed, 9 insertions(+), 37 deletions(-) diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index db51ef5a5fa..fd4d5fcfeb2 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -483,7 +483,7 @@ export const TableView = forwardRef(function TableView( isInResizeMode, setIsInResizeMode, selectionMode, - selectionStyle + selectionStyle, disabledBehavior }), [ @@ -495,7 +495,7 @@ export const TableView = forwardRef(function TableView( isInResizeMode, setIsInResizeMode, selectionMode, - selectionStyle + selectionStyle, disabledBehavior ] ); @@ -1277,26 +1277,9 @@ const stickyCell = { backgroundColor: 'gray-25' } as const; -// Bit gross but this is needed because the sticky cells currently cover/partially cover styles that the row applies so that -// they don't appear when the table is scrolled. The below basically just continues the inset box-shadow that the row has when -// it is focused as a drop target -const rowDropTargetStickyOutline = { - boxShadow: { - default: 'none', - ':is([role="row"][data-drop-target] *)': { - default: `[inset 0 2px 0 0 ${color('blue-800')}, inset 0 -2px 0 0 ${color('blue-800')}]`, - forcedColors: '[inset 0 2px 0 0 Highlight, inset 0 -2px 0 0 Highlight]' - }, - ':is([role="row"][data-focus-visible] *)': { - forcedColors: '[inset 0 2px 0 0 Highlight, inset 0 -2px 0 0 Highlight]' - } - } -} as const; - const checkboxCellStyle = style({ ...commonCellStyles, ...stickyCell, - ...rowDropTargetStickyOutline, display: 'flex', paddingStart: 16, paddingEnd: 8, @@ -1311,7 +1294,6 @@ const checkboxCellStyle = style({ const dragCellStyle = style({ ...commonCellStyles, ...stickyCell, - ...rowDropTargetStickyOutline, paddingStart: 4, paddingEnd: 4, alignContent: 'center', @@ -2075,21 +2057,6 @@ const row = style< } }, // When checkbox selection, render the gray divider between rows as a border - '--rowFocusIndicatorColor': { - type: 'outlineColor', - value: { - default: 'focus-ring', - forcedColors: 'Highlight' - } - }, - outlineStyle: 'none', - boxShadow: { - isDropTarget: `[inset 0 0 0 2px ${color('blue-800')}]`, - forcedColors: { - isDropTarget: '[inset 0 0 0 2px Highlight]', - isFocusVisible: '[inset 0 0 0 2px Highlight]' - } - }, borderTopWidth: 0, borderBottomWidth: { selectionStyle: { @@ -2141,6 +2108,9 @@ const row = style< // isNextSelected: '[inset 0 -1px 0px var(--borderColorGray)]' // } } + }, + forcedColors: { + isFocusVisible: '[inset 0 0 0 2px Highlight]' } }, fontWeight: { @@ -2151,6 +2121,8 @@ const row = style< forcedColorAdjust: 'none' }); +// Sticky cells (the drag cell, and the checkbox cell when present) get an inline z-index=2 applied by the virtualizer's layout +// To ensure that the highlight selection border is painted above the stick cells, set z-index to 3 const highlightSelectionBorder = css( `&:before { content: ""; @@ -2158,7 +2130,7 @@ const highlightSelectionBorder = css( height: 100%; position: absolute; inset: 0; - z-index: 2; + z-index: 3; box-sizing: border-box; border-style: solid; border-color: var(--borderColor); @@ -2181,7 +2153,7 @@ const focusIndicator = css( width: 100%; height: 100%; top: 0; - z-index: 2 ; + z-index: 2; inset-inline-start: 0; border-radius: 5px; position: absolute; From dd8c7b249722fdb476de18e43828a21cc8f4e5cd Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Mon, 18 May 2026 11:45:38 -0700 Subject: [PATCH 35/37] fix lint --- packages/@react-spectrum/s2/src/TreeView.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/@react-spectrum/s2/src/TreeView.tsx b/packages/@react-spectrum/s2/src/TreeView.tsx index 78bbd0c46b8..4a500c46fac 100644 --- a/packages/@react-spectrum/s2/src/TreeView.tsx +++ b/packages/@react-spectrum/s2/src/TreeView.tsx @@ -51,7 +51,6 @@ import { } from './ListView'; // @ts-ignore import intlMessages from '../intl/*.json'; -import {ListLayout} from 'react-stately/useVirtualizerState'; import {ProgressCircle} from './ProgressCircle'; import {Provider, useContextProps} from 'react-aria-components/slots'; import { From 5bbbb47c0b16dc0e2aa53af951bd499c97b54c9e Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Mon, 18 May 2026 11:47:06 -0700 Subject: [PATCH 36/37] fix typecheck --- packages/@react-spectrum/s2/src/TreeView.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/@react-spectrum/s2/src/TreeView.tsx b/packages/@react-spectrum/s2/src/TreeView.tsx index 4a500c46fac..5b2ecaca14d 100644 --- a/packages/@react-spectrum/s2/src/TreeView.tsx +++ b/packages/@react-spectrum/s2/src/TreeView.tsx @@ -45,8 +45,6 @@ import {IconContext} from './Icon'; import { insertionIndicatorBar, insertionIndicatorCircle, - isFirstItem, - isPrevSelected, S2ListLayout } from './ListView'; // @ts-ignore From dc6b6499b2f8a5623ac6b20ee1d2bc8060b0fcb8 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Mon, 18 May 2026 11:51:46 -0700 Subject: [PATCH 37/37] formating.... --- packages/@react-spectrum/s2/src/TreeView.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/@react-spectrum/s2/src/TreeView.tsx b/packages/@react-spectrum/s2/src/TreeView.tsx index 5b2ecaca14d..7d486abbc73 100644 --- a/packages/@react-spectrum/s2/src/TreeView.tsx +++ b/packages/@react-spectrum/s2/src/TreeView.tsx @@ -42,11 +42,7 @@ import { UnsafeStyles } from './style-utils' with {type: 'macro'}; import {IconContext} from './Icon'; -import { - insertionIndicatorBar, - insertionIndicatorCircle, - S2ListLayout -} from './ListView'; +import {insertionIndicatorBar, insertionIndicatorCircle, S2ListLayout} from './ListView'; // @ts-ignore import intlMessages from '../intl/*.json'; import {ProgressCircle} from './ProgressCircle';