Skip to content

feat: Add support for nested table rows#9740

Open
devongovett wants to merge 10 commits intomainfrom
treeble
Open

feat: Add support for nested table rows#9740
devongovett wants to merge 10 commits intomainfrom
treeble

Conversation

@devongovett
Copy link
Member

@devongovett devongovett commented Mar 5, 2026

Adds support for nested table rows to React Aria Components and S2. The new treeColumn prop designates a column key as the one with hierarchical data. Render a <Button slot="chevron"> within the cells in this column to allow expanding and collapsing. Several new render props are added to rows and cells to provide the level, whether the row has child rows, whether the cell is in the tree column, and whether the row is expanded.

It works by updating TableCollection to match how TreeCollection works, i.e. flattening based on the expanded keys. Updated the hooks according to this structure (useTableState now always supports expandedKeys, the separate useTreeGridState is not needed), and also updated the legacy TreeGridCollection from v3 to match this for backward compatibility. Also updated Table in RAC to support drag and drop with expandable rows using the TreeDropTargetDelegate.

📝 Test Instructions:

Test added stories for RAC Table and S2 TableView. Also test docs examples for both. Test docs and storybook for v3 TableView expandable rows.

Questions

  • Do we want to support any column being the treeColumn in S2, or only the first one?
  • How weird is it that TableCollection only returns cells for getChildren on a row (and not nested rows since they are flattened)?
  • Do we want to change idScope to only apply to cells?

}

const editableCell = style<CellRenderProps & S2TableProps & {isDivider: boolean, selectionMode?: 'none' | 'single' | 'multiple', isSaving?: boolean}>({
const expandButton = style<ExpandableRowChevronProps>({
Copy link
Member Author

Choose a reason for hiding this comment

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

Copied these styles from TreeView. There is no design for nested table rows in S2 yet.

@rspbot
Copy link

rspbot commented Mar 5, 2026

parentKey: parentNode ? parentNode.key : null,
value: partialNode.value ?? null,
level: parentNode ? parentNode.level + 1 : 0,
level: (parentNode?.level ?? 0) + (parentNode?.type === 'item' ? 1 : 0),
Copy link
Member Author

Choose a reason for hiding this comment

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

Mirrors logic in new collections. We should only increase the level if the parent node is an item. Will this break anything?

Copy link
Member

Choose a reason for hiding this comment

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

I don't think it will, this should only affect nodes like items in sections or rows in tables, the latter of which you've handled and the former which doesn't translate the level prop into anything aria related I think

getDropOperation(e) {
let {target, isInternal, draggingKeys} = e;

// Prevent dropping items onto themselves or their descendants
Copy link
Member Author

Choose a reason for hiding this comment

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

Move from TreeView to here so it can be shared with TableView. Is there any case where we wouldn't want this?

Copy link
Member

Choose a reason for hiding this comment

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

nah, I don't think we'd ever want to support dropping something into itself or into its own descendant

// Clone row node and its children so modifications to the node for treegrid specific values aren't applied on the nodes provided
// to TableCollection. Index, level, and parent keys are all changed to reflect a flattened row structure rather than the treegrid structure
// values automatically calculated via CollectionBuilder
let visitNode = (node: GridNode<T>) => {
Copy link
Member Author

Choose a reason for hiding this comment

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

We don't need update the nodes when we flatten them, just need to collect them into an array. We rely on the nodes having their original properties, not their flattened ones. This matches how new collections works.

Copy link
Member

@snowystinger snowystinger left a comment

Choose a reason for hiding this comment

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

In https://reactspectrum.blob.core.windows.net/reactspectrum/df4fce641a37f6508e9edc5cccbff956ed1270a0/storybook-s2/index.html?path=/story/tableview--table-with-nested-rows
If I ArrowRight open both rows, the entire table scrolls to the right when i open the second one.

This expand button doesn't do anything? https://reactspectrum.blob.core.windows.net/reactspectrum/df4fce641a37f6508e9edc5cccbff956ed1270a0/storybook/index.html?path=/story/react-aria-components-table--table-nested-rows&providerSwitcher-express=false

Need a story with disabled keys that are the nested rows so i can check disabledBehaviour 'all' | 'selection'

Will continue reviewing soon

}

if (idScope != null) {
if (idScope != null && rendered.props.id == null) {
Copy link
Member

Choose a reason for hiding this comment

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

when does this happen? was it a bug we had not keeping the id as a key?

Copy link
Contributor

Choose a reason for hiding this comment

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

Nested rows without an explicit id would have an idScope prepended for every level. Since the collection is flattened anyways I suppose it makes sense to skip that.

Copy link
Member Author

Choose a reason for hiding this comment

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

yeah this was meant to scope the ids of the cells within a row so you can loop over the columns multiple times and not get duplicates. We don't want that for rows. If an explicit id is given, we should respect that. That was the change here. I was also considering moving this scoping into Cell itself so it would only apply there. wdyt?

Copy link
Member

Choose a reason for hiding this comment

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

I think moving this into Cell only makes sense, I can't really think of any other cases where we'd also want to do this scoping

Copy link
Member Author

Choose a reason for hiding this comment

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

will have to think about how to do this. createLeafComponent doesn't really provide a way to add that logic right now. maybe we could do it by creating a wrapper component or something.

}), [keyboardDelegate, state.collection, state.disabledKeys, disabledBehavior, ref, direction, collator, layoutDelegate, layout]);
let id = useId(props.id);
gridIds.set(state, id);
gridIds.set(state as TableState<T>, id);
Copy link
Member

Choose a reason for hiding this comment

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

safe cast? what's the issue here?

Copy link
Member Author

Choose a reason for hiding this comment

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

has to do with backward compatibility with the unstable version from v3. TreeGridState supports expandedKeys="all" but we don't have that in TableState.

};

if (isVirtualized && !(tableNestedRows() && 'expandedKeys' in state)) {
if (isVirtualized && state.treeColumn == null) {
Copy link
Member

Choose a reason for hiding this comment

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

bit of a strange check, could we put it behind some other state property? like state.isATreeRoot() or something that makes more sense

Copy link
Member Author

Choose a reason for hiding this comment

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

hmm why is it strange? if there is a tree column it is a tree grid.

delete rowProps['aria-rowindex'];
}

let isExpanded = state.treeColumn != null && (state.expandedKeys === 'all' || state.expandedKeys.has(node.key));
Copy link
Member

Choose a reason for hiding this comment

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

no expandedKeys === 'all' i thought? that's what we decided for tree, is this just backwards compat?

Copy link
Member Author

Choose a reason for hiding this comment

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

yes it was there before. I left it so as not to break v3.

let treeNode = state.keyMap.get(node.key);
let expandButtonProps: AriaButtonProps = {};
if (state.treeColumn != null) {
let treeNode = state.collection.getItem(node.key);
Copy link
Member

Choose a reason for hiding this comment

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

i assume this works with old collections given that tests pass?

Copy link
Member Author

Choose a reason for hiding this comment

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

what specifically are you concerned about? getItem existed before

<Cell>Games</Cell>
<Cell>Folder</Cell>
<Cell>6/7/2023</Cell>
<Row id="mario">
Copy link
Member

Choose a reason for hiding this comment

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

I assume order matters, and if someone did this instead, that would break?

        <Row id="games">
          <Cell>Games</Cell>
          <Cell>Folder</Cell>
          <Row id="mario">
          </Row>
          <Cell>6/7/2023</Cell>

Copy link
Member Author

Choose a reason for hiding this comment

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

yes order is important.


export const TableWithNestedRows: StoryObj<typeof TableView> = {
render: (args) => (
<TableView aria-label="Files" treeColumn="name" {...args} styles={style({width: 700, height: 320})}>
Copy link
Member

Choose a reason for hiding this comment

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

what happens if a cell that is in the treeColumn has a colSpan of more than 1? Or if the cell in a row is included but not the first cell of a colSpan?

Copy link
Member Author

Choose a reason for hiding this comment

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

there are ways to mess it up for yourself, yes. you could also not render the tree column. All this does is two things:

  1. Make it behave as a tree grid
  2. Provide the isTreeColumn render prop to cells if that cell's column key matches the treeColumn. If you don't render such a cell then this won't happen.

yarn.lock Outdated
linkType: soft

"@react-aria/button@npm:^3.14.5, @react-aria/button@workspace:packages/@react-aria/button":
"@react-aria/button@npm:^3.14.4, @react-aria/button@npm:^3.14.5, @react-aria/button@workspace:packages/@react-aria/button":
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
"@react-aria/button@npm:^3.14.4, @react-aria/button@npm:^3.14.5, @react-aria/button@workspace:packages/@react-aria/button":
"@react-aria/button@npm:^3.14.5, @react-aria/button@workspace:packages/@react-aria/button":

I think the version in the package.json just needs to be updated after the release

export function useTableState<T extends object>(props: TableStateProps<T>): TableState<T> {
let [isKeyboardNavigationDisabled, setKeyboardNavigationDisabled] = useState(false);
let {selectionMode = 'none', showSelectionCheckboxes, showDragButtons} = props;
let {selectionMode = 'none', showSelectionCheckboxes, showDragButtons, treeColumn = null} = props;
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm wondering whether we could traverse the collection instead to look for an isTreeColumn prop on a column node. A bit of a bummer to have isRowHeader match the compositional API but then carry treeColumn as a prop on the parent.

Copy link
Member Author

Choose a reason for hiding this comment

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

the reason for this is that there can only be one tree column, but there can be many row headers. It would be would be harder to enforce that there is only one with isTreeColumn.

} as const;

const cell = style<CellRenderProps & S2TableProps & {isDivider: boolean}>({
const treeColumnStyles = {
Copy link
Member

Choose a reason for hiding this comment

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

Should/can we add a height transition like we have in Disclosure?

Copy link
Member Author

Choose a reason for hiding this comment

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

it's hard because the rows are flattened, and at least in non-virtualized tables, you can't add a wrapper div (that's not valid in an HTML table). not sure if there's another way to achieve that...

let {dragAndDropHooks, dragState, dropState} = useContext(DragAndDropContext);
let {isVirtualized, CollectionBranch} = useContext(CollectionRendererContext);
let {rowProps, ...states} = useTableRow(
let {rowProps, expandButtonProps, ...states} = useTableRow(
Copy link
Member

Choose a reason for hiding this comment

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

If a row has no selection and no provided actions, I would kind of expect clicking anywhere on the row to expand it by default.

Copy link
Member Author

Choose a reason for hiding this comment

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

yeah we can discuss that. The APG example doesn't do that, but we do it in TreeView.

[ButtonContext, {
slots: {
[DEFAULT_SLOT]: {},
chevron: expandButtonProps,
Copy link
Member

Choose a reason for hiding this comment

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

Super-nit, but if you expand an item and one of it's children was previously expanded, I think the child's chevron should not animate.

child-expansion.mov

Copy link
Member Author

Choose a reason for hiding this comment

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

huh, must be due to virtualizer re-using the element from a different row or something

on external state (e.g. `columns` in this example).</Content>
</InlineAlert>

### Nested rows
Copy link
Member

Choose a reason for hiding this comment

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

I like the name "Expandable rows" better. I feel like nested rows doesn't imply that there is an expansion feature. You could theoretically have/want nested rows that don't expand/collapse.

Copy link
Member

@LFDanLu LFDanLu left a comment

Choose a reason for hiding this comment

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

initial review, still need to do some testing/look at the rest but figured I'd do a dive into some of the open questions

}

if (idScope != null) {
if (idScope != null && rendered.props.id == null) {
Copy link
Member

Choose a reason for hiding this comment

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

I think moving this into Cell only makes sense, I can't really think of any other cases where we'd also want to do this scoping

((getLastItem(state.collection.body.childNodes) as GridNode<T>)?.indexOfType ?? 0) + 1
'aria-level': treeNode.level + 1,
'aria-posinset': treeNode.index - (isParentBody ? 0 : state.collection.columnCount) + 1,
'aria-setsize': lastSibling.index - (isParentBody ? 0 : state.collection.columnCount) + 1
Copy link
Member

Choose a reason for hiding this comment

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

the aria-setsize seems to be too large by 1 in S2. https://reactspectrum.blob.core.windows.net/reactspectrum/df4fce641a37f6508e9edc5cccbff956ed1270a0/storybook-s2/index.html?path=/story/tableview--table-with-nested-rows shows a aria-setsize=5 on each top level row when there are only 4 rows in the table body

Copy link
Member

Choose a reason for hiding this comment

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

did some more digging here, this seems to be due to the loader element that we always include in the S2 table implementation. A bit unfortunate, but I guess we could either make sure to never count the loader row in the aria-rowcount/aria-setsize calculations? Not a blocker for this PR since it was actually already like this apparently.

Copy link
Member Author

Choose a reason for hiding this comment

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

good catch. I think I fixed it by traversing backwards to find the last row. This assumes the loader is at the end though. If we want to support other positions it'll be more complicated and performance intensive.

parentKey: parentNode ? parentNode.key : null,
value: partialNode.value ?? null,
level: parentNode ? parentNode.level + 1 : 0,
level: (parentNode?.level ?? 0) + (parentNode?.type === 'item' ? 1 : 0),
Copy link
Member

Choose a reason for hiding this comment

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

I don't think it will, this should only affect nodes like items in sections or rows in tables, the latter of which you've handled and the former which doesn't translate the level prop into anything aria related I think

getDropOperation(e) {
let {target, isInternal, draggingKeys} = e;

// Prevent dropping items onto themselves or their descendants
Copy link
Member

Choose a reason for hiding this comment

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

nah, I don't think we'd ever want to support dropping something into itself or into its own descendant

LFDanLu
LFDanLu previously approved these changes Mar 6, 2026
Copy link
Member

@LFDanLu LFDanLu left a comment

Choose a reason for hiding this comment

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

refactored logic looks good to me, approving for testing


export const TableNestedRows: TableStory = (args) => {
return (
<Table aria-label="Files" selectionMode="multiple" {...args}>
Copy link
Member

Choose a reason for hiding this comment

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

nit: missing the treeColumn prop here, story doesn't actually expand/collapse

((getLastItem(state.collection.body.childNodes) as GridNode<T>)?.indexOfType ?? 0) + 1
'aria-level': treeNode.level + 1,
'aria-posinset': treeNode.index - (isParentBody ? 0 : state.collection.columnCount) + 1,
'aria-setsize': lastSibling.index - (isParentBody ? 0 : state.collection.columnCount) + 1
Copy link
Member

Choose a reason for hiding this comment

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

did some more digging here, this seems to be due to the loader element that we always include in the S2 table implementation. A bit unfortunate, but I guess we could either make sure to never count the loader row in the aria-rowcount/aria-setsize calculations? Not a blocker for this PR since it was actually already like this apparently.

reidbarber
reidbarber previously approved these changes Mar 6, 2026
Copy link
Member

@reidbarber reidbarber left a comment

Choose a reason for hiding this comment

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

I'm good to get this in for testing.

@devongovett devongovett dismissed stale reviews from reidbarber and LFDanLu via e2eb072 March 7, 2026 00:56
@rspbot
Copy link

rspbot commented Mar 7, 2026

@rspbot
Copy link

rspbot commented Mar 7, 2026

## API Changes

react-aria-components

/react-aria-components:Table

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

/react-aria-components:Row

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

/react-aria-components:TableProps

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

/react-aria-components:RowProps

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

/react-aria-components:RowRenderProps

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

/react-aria-components:CellRenderProps

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

/react-aria-components:TableState

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

@react-aria/test-utils

/@react-aria/test-utils:TableTester

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

@react-spectrum/s2

/@react-spectrum/s2:TableView

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

/@react-spectrum/s2:TableViewProps

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

@react-spectrum/table

/@react-spectrum/table:TableView

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

@react-spectrum/test-utils

/@react-spectrum/test-utils:TableTester

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

@react-stately/table

/@react-stately/table:TableState

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

/@react-stately/table:TableStateProps

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

/@react-stately/table:TreeGridState

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

/@react-stately/table:TreeGridStateProps

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

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants