diff --git a/packages/@react-spectrum/s2/chromatic/TableView.stories.tsx b/packages/@react-spectrum/s2/chromatic/TableView.stories.tsx index 10f47fa1dc3..d8d1019efeb 100644 --- a/packages/@react-spectrum/s2/chromatic/TableView.stories.tsx +++ b/packages/@react-spectrum/s2/chromatic/TableView.stories.tsx @@ -44,7 +44,7 @@ const meta: Meta = { export default meta; const StaticTable = (args: TableViewProps): ReactElement => ( - + Name Type @@ -117,6 +117,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 => ( diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index 8006a1f098f..0cd3b9deb21 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -75,8 +75,7 @@ import {Key} from '@react-types/shared'; import {LayoutInfo, Virtualizer} from 'react-aria-components/Virtualizer'; import LinkOutIcon from '../ui-icons/LinkOut'; import {ListLayout} from 'react-stately/useVirtualizerState'; -// @ts-ignore -import {ListState} from 'react-stately/useListState'; +import type {ListState} from 'react-stately/useListState'; import {ProgressCircle} from './ProgressCircle'; import {Text, TextContext} from './Content'; import {useActionBarContainer} from './ActionBar'; @@ -959,34 +958,6 @@ function ListSelectionCheckbox({isDisabled}: {isDisabled: boolean}) { ); } -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; @@ -1175,3 +1146,37 @@ 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; + + 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 f9768e58540..fd4d5fcfeb2 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -125,6 +125,7 @@ import React, { 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'; @@ -175,6 +176,12 @@ 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'; } // TODO: Note that loadMore and loadingState are now on the Table instead of on the TableBody @@ -204,6 +211,7 @@ let InternalTableContext = createContext< setIsInResizeMode?: (val: boolean) => void; isInResizeMode?: boolean; selectionMode?: 'none' | 'single' | 'multiple'; + selectionStyle?: 'checkbox' | 'highlight'; } >({}); @@ -427,6 +435,7 @@ export const TableView = forwardRef(function TableView( onAction, onLoadMore, selectionMode = 'none', + selectionStyle = 'checkbox', dragAndDropHooks, disabledBehavior = 'all', ...otherProps @@ -474,6 +483,7 @@ export const TableView = forwardRef(function TableView( isInResizeMode, setIsInResizeMode, selectionMode, + selectionStyle, disabledBehavior }), [ @@ -485,6 +495,7 @@ export const TableView = forwardRef(function TableView( isInResizeMode, setIsInResizeMode, selectionMode, + selectionStyle, disabledBehavior ] ); @@ -536,7 +547,7 @@ export const TableView = forwardRef(function TableView( isQuiet }) } - selectionBehavior="toggle" + selectionBehavior={selectionStyle === 'highlight' ? 'replace' : 'toggle'} selectionMode={selectionMode} onRowAction={onAction} dragAndDropHooks={dragAndDropHooks} @@ -653,9 +664,13 @@ const cellFocus = { outlineWidth: 2, outlineColor: { default: 'focus-ring', - forcedColors: 'Highlight' + forcedColors: { + default: 'Highlight' + } }, - borderRadius: '[6px]' + 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() { @@ -1141,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'); @@ -1166,9 +1181,9 @@ export const TableHeader = /*#__PURE__*/ (forwardRef as forwardRefType)(function )} {/* Add extra columns for selection. */} - {selectionBehavior === 'toggle' && ( - // Also isSticky prop is applied just for the layout, will decide what the RAC api should be later + {selectionBehavior === 'toggle' && selectionStyle === 'checkbox' && ( {({id, isFocusVisible, hasChildItems, isTreeColumn, isExpanded, isDisabled}) => ( <> + {showDivider &&
} {hasChildItems && isTreeColumn && ( )} @@ -1532,7 +1524,12 @@ const editableCell = style< ...treeColumnStyles, color: { default: baseColor('neutral'), - isSaving: baseColor('neutral-subdued') + isSaving: baseColor('neutral-subdued'), + forcedColors: 'ButtonText', + isDisabled: { + default: 'disabled', + forcedColors: 'GrayText' + } }, paddingY: centerPadding(), boxSizing: 'border-box', @@ -1914,7 +1911,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: { @@ -1923,6 +1919,19 @@ const rowBackgroundColor = { isHovered: selectedActiveBackground, // table-selected-row-background-color, opacity /15 isPressed: selectedActiveBackground // table-selected-row-background-color, opacity /15 }, + selectionStyle: { + highlight: { + default: 'gray-25', + isHovered: colorMix('gray-25', 'gray-900', 7), // table-row-hover-color + isPressed: colorMix('gray-25', 'gray-900', 10), // table-row-hover-color + isSelected: { + default: colorMix('gray-25', 'blue-900', 10), + isHovered: colorMix('gray-25', 'blue-900', 15), + isPressed: colorMix('gray-25', 'blue-900', 15), + forcedColors: 'Highlight' + } + } + }, isInFooter: 'gray-200', forcedColors: { default: 'Background' @@ -1934,15 +1943,18 @@ 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({ +const row = style< + RowRenderProps & + S2TableProps & {isInFooter?: boolean; isNextSelected?: boolean; isPrevSelected?: boolean} +>({ height: 'full', position: 'relative', boxSizing: 'border-box', @@ -1955,37 +1967,205 @@ const row = style({ type: 'color', value: rowTextColor }, - '--rowFocusIndicatorColor': { - type: 'outlineColor', + // TODO: outline here is to emulate v3 forcedColors experience but runs into the same problem where the sticky column covers the outline + // This doesn't quite work because it gets cut off by the checkbox cell background masking element, figure out another way. Could shrink the checkbox cell's content even more + // and offset it by margin top but that messes up the checkbox centering a bit + // outlineWidth: { + // forcedColors: { + // isFocusVisible: 2 + // } + // }, + // outlineOffset: { + // forcedColors: { + // isFocusVisible: -1 + // } + // }, + // outlineColor: { + // forcedColors: { + // isFocusVisible: 'ButtonBorder' + // } + // }, + // outlineStyle: { + // default: 'none', + // forcedColors: { + // isFocusVisible: 'solid' + // } + // }, + outlineStyle: 'none', + '--borderBottomRadius': { + type: 'borderBottomStartRadius', value: { - default: 'focus-ring', - forcedColors: 'Highlight' + default: 'none', + selectionStyle: { + highlight: { + default: 'none', + isSelected: '[5px]', + isNextSelected: 'none' + } + } } }, - 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]' + '--borderTopRadius': { + type: 'borderTopStartRadius', + value: { + default: 'none', + selectionStyle: { + highlight: { + default: 'none', + isSelected: '[5px]', + isPrevSelected: 'none' + } + } } }, + borderBottomRadius: 'var(--borderBottomRadius)', + borderTopRadius: 'var(--borderTopRadius)', + // We will only use these border values when it is highlight selection + '--borderTopWidth': { + type: 'width', + value: { + default: { + selectionStyle: { + checkbox: 0, + highlight: 1 + } + }, + isPrevSelected: 0 + } + }, + '--borderBottomWidth': { + type: 'width', + value: { + default: { + selectionStyle: { + checkbox: 0, + highlight: 1 + } + }, + isNextSelected: 0 + } + }, + '--borderStartEndWidth': { + type: 'width', + value: { + default: { + selectionStyle: { + checkbox: 0, + highlight: 1 + } + } + } + }, + // When checkbox selection, render the gray divider between rows as a border borderTopWidth: 0, - borderBottomWidth: 1, + borderBottomWidth: { + selectionStyle: { + highlight: 0, + checkbox: 1 + } + }, borderStartWidth: 0, borderEndWidth: 0, borderStyle: 'solid', borderColor: { - default: 'gray-300', - forcedColors: 'ButtonBorder' + selectionStyle: { + highlight: 'transparent', + checkbox: 'gray-300' + } + }, + '--borderColorGray': { + type: 'borderColor', + value: { + default: 'gray-300', + forcedColors: { + default: 'ButtonBorder', + isSelected: 'Highlight' + } + } + }, + '--borderColorBlue': { + type: 'borderColor', + value: { + default: 'blue-900', + forcedColors: 'Highlight' + } + }, + '--borderColor': { + type: 'borderColor', + value: { + default: 'transparent', + isSelected: '--borderColorBlue' + } + }, + // When highlight selection, render gray dividers as box shadow + boxShadow: { + selectionStyle: { + highlight: { + default: '[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)]' + // } + } + }, + forcedColors: { + isFocusVisible: '[inset 0 0 0 2px Highlight]' + } }, fontWeight: { default: 'normal', isInFooter: 'bold' }, + isolation: 'isolate', 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: ""; + width: 100%; + height: 100%; + position: absolute; + inset: 0; + z-index: 3; + box-sizing: border-box; + border-style: solid; + border-color: var(--borderColor); + border-top-width: var(--borderTopWidth); + border-bottom-width: var(--borderBottomWidth); + border-inline-start-width: var(--borderStartEndWidth); + border-inline-end-width: var(--borderStartEndWidth); + border-bottom-left-radius: var(--borderBottomRadius); + border-bottom-right-radius: var(--borderBottomRadius); + border-top-left-radius: var(--borderTopRadius); + border-top-right-radius: var(--borderTopRadius); + pointer-events: none; + } + ` +); + +const focusIndicator = css( + `&:after { + content: ""; + width: 100%; + height: 100%; + top: 0; + z-index: 2; + inset-inline-start: 0; + border-radius: 5px; + position: absolute; + outline-style: solid; + outline-color: var(--borderColorBlue); + outline-width: 2px; + outline-offset: -2px; + pointer-events: none; + } + ` +); + const selectionCheckbox = style({ visibility: { default: 'visible', @@ -2016,7 +2196,7 @@ export const Row = /*#__PURE__*/ (forwardRef as forwardRefType)(function Row ) { let {selectionBehavior, selectionMode, allowsDragging} = useTableOptions(); - let tableVisualOptions = useContext(InternalTableContext); + let {selectionStyle, ...tableVisualOptions} = useContext(InternalTableContext); let domRef = useDOMRef(ref); let isInFooter = useContext(FooterContext); @@ -2027,19 +2207,18 @@ export const Row = /*#__PURE__*/ (forwardRef as forwardRefType)(function Row row({ ...renderProps, ...tableVisualOptions, - isInFooter + selectionStyle, + isInFooter, + isNextSelected: isNextSelected(renderProps.id, renderProps.state), + isPrevSelected: isPrevSelected(renderProps.id, renderProps.state) }) + - (renderProps.isFocusVisible || renderProps.isDropTarget - ? ' ' + - css( - '&: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)}' - ) - : '') + (renderProps.isFocusVisible || renderProps.isDropTarget ? ' ' + focusIndicator : '') + + (selectionStyle === 'highlight' ? ' ' + highlightSelectionBorder : '') } {...otherProps}> {allowsDragging && ( @@ -2052,14 +2231,16 @@ export const Row = /*#__PURE__*/ (forwardRef as forwardRefType)(function Row )} - {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} @@ -2094,3 +2275,19 @@ 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 8dd82eda5b1..7d486abbc73 100644 --- a/packages/@react-spectrum/s2/src/TreeView.tsx +++ b/packages/@react-spectrum/s2/src/TreeView.tsx @@ -42,13 +42,7 @@ import { UnsafeStyles } from './style-utils' with {type: 'macro'}; import {IconContext} from './Icon'; -import { - insertionIndicatorBar, - insertionIndicatorCircle, - isFirstItem, - isPrevSelected, - S2ListLayout -} from './ListView'; +import {insertionIndicatorBar, insertionIndicatorCircle, S2ListLayout} from './ListView'; // @ts-ignore import intlMessages from '../intl/*.json'; import {ProgressCircle} from './ProgressCircle'; @@ -918,3 +912,18 @@ 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/stories/TableView.stories.tsx b/packages/@react-spectrum/s2/stories/TableView.stories.tsx index 3d26f8055f0..e076e9985e7 100644 --- a/packages/@react-spectrum/s2/stories/TableView.stories.tsx +++ b/packages/@react-spectrum/s2/stories/TableView.stories.tsx @@ -132,6 +132,67 @@ 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: HighlightTable, + args: { + selectionMode: 'multiple', + selectionStyle: 'highlight', + onResize: undefined, + onResizeStart: undefined, + onResizeEnd: undefined, + onLoadMore: undefined + } +}; + export const DisabledRows: StoryObj = { ...Example, args: { diff --git a/packages/dev/s2-docs/pages/s2/TableView.mdx b/packages/dev/s2-docs/pages/s2/TableView.mdx index 33dc24ee36e..1ba86cb4d17 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']} initialProps={{'aria-label': 'Files', selectionMode: 'multiple', 'treeColumn': 'name'}} type="s2" +```tsx render docs={docs.exports.TableView} links={docs.links} props={['selectionMode', 'overflowMode', 'density', 'isQuiet', 'selectionStyle']} initialProps={{'aria-label': 'Files', selectionMode: 'multiple', 'treeColumn': 'name', 'selectionStyle': 'checkbox'}} type="s2" "use client"; import {TableView, TableHeader, Column, TableBody, Row, Cell} from '@react-spectrum/s2/TableView'; import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; @@ -308,7 +308,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/TableView'; import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; @@ -323,18 +323,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 @@ -658,7 +658,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', 'disabledBehavior']} initialProps={{selectionMode: 'multiple', disabledBehavior: 'selection'}} wide type="s2" +```tsx render docs={docs.exports.TableView} links={docs.links} props={['selectionMode', 'disallowEmptySelection', 'disabledBehavior', 'selectionStyle']} initialProps={{selectionMode: 'multiple', disabledBehavior: 'selection', selectionStyle: 'checkbox'}} wide type="s2" "use client"; import {TableView, TableHeader, Column, TableBody, Row, Cell, type Selection} from '@react-spectrum/s2/TableView'; import {ActionBar, ActionButton} from '@react-spectrum/s2/ActionBar'; diff --git a/packages/react-aria-components/src/Table.tsx b/packages/react-aria-components/src/Table.tsx index 3ef8766dc66..861ae199a13 100644 --- a/packages/react-aria-components/src/Table.tsx +++ b/packages/react-aria-components/src/Table.tsx @@ -1669,6 +1669,10 @@ export interface RowRenderProps extends ItemRenderProps { * @selector [data-level] */ level: number; + /** + * State of the table. + */ + state: TableState; } export interface RowProps @@ -1830,6 +1834,7 @@ export const Row = /*#__PURE__*/ createBranchComponent( }, values: { ...states, + state, isHovered, isFocused, isFocusVisible,