Skip to content

feat: Add TableFooter component#9943

Merged
devongovett merged 9 commits into
mainfrom
table-footer
Apr 23, 2026
Merged

feat: Add TableFooter component#9943
devongovett merged 9 commits into
mainfrom
table-footer

Conversation

@devongovett

@devongovett devongovett commented Apr 17, 2026

Copy link
Copy Markdown
Member

Closes #4372, closes #7312

Adds a new <TableFooter> component to React Aria and S2, which may contain one or more rows. When rendered as an HTML table, this is a <tfoot> element, otherwise it has role="rowgroup".

It also allows rendering multiple <TableBody> elements, which enables sections to be implemented.

id={id}
dependencies={[...dependencies, columns]}
isDisabled={isInFooter}
disabledBehavior="selection"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

New feature: support for disabledBehavior at the individual Row level, not just at the Table level. This is so that the summary rows are not selectable but still focusable, regardless of the disabledBehavior of the rest of the table. Is this something we want to support?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think that makes sense.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I guess its fine, but feels a bit iffy to me to make it public api if it really is only used for the summary row case. Could we not just do a private prop or context or somethingto set this?

);
};

export const TableSectionDnd: TableStory = (args) => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm unable to drop at the end of a given section. I also cannot drop into an empty section

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

for some reason, in Chrome , each drag takes me two mouse clicks to initiate, I doubt that was caused by this PR, so just noting it

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I was trying to keep this PR simple. 😅 It was not my intent to work on drag and drop here but you asked for an example so I added one. The issue you mention is pre-existing: you can't drop before a disabled items (like the section headers). We should handle that separately.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

yeah, not going to hold anything up with it, all good

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I also noticed that just keyboard navigating up and down/through a row's elements in this table also causes keyboard focus to get stuck on the drag handle, just something to also track

id={id}
dependencies={[...dependencies, columns]}
isDisabled={isInFooter}
disabledBehavior="selection"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I guess its fine, but feels a bit iffy to me to make it public api if it really is only used for the summary row case. Could we not just do a private prop or context or somethingto set this?

);
};

export const TableSectionDnd: TableStory = (args) => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I also noticed that just keyboard navigating up and down/through a row's elements in this table also causes keyboard focus to get stuck on the drag handle, just something to also track

Comment thread packages/react-stately/src/layout/TableLayout.ts
Comment thread packages/react-aria/src/grid/GridKeyboardDelegate.ts Outdated
Comment thread packages/react-stately/src/selection/SelectionManager.ts Outdated
reidbarber
reidbarber previously approved these changes Apr 22, 2026
Co-authored-by: Daniel Lu <danilu@adobe.com>
snowystinger
snowystinger previously approved these changes Apr 22, 2026
* The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the element.
* @default 'react-aria-TableFooter'
*/
className?: string

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why not renderProps enabled? I guess it's non-breaking to add it in the future

@devongovett devongovett Apr 22, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

because there aren't any states

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

well, there aren't any states yet, but TableBody has isEmpty and isDropTarget. Which, maybe empty makes sense and drop target doesn't. Could also include id on both of them

</Row>
)}
</TableBody>
<TableFooter>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I checked and multiple footer rows appears to work, though row divider borders aren't a good contrast. I tried nesting rows in the footer as well in case someone wanted to use progressive disclosure for more complicated summaries, that also worked, though the expansion could end up in an odd spot, I made it the title column here.

Image

I'm assuming these aren't officially supported, but nice to know they won't be much trouble to eventually support. Though it might be nice to be able to tell a TableBody/Footer if it is expandable so that the alignment doesn't end up like this if only one of them actually has expansion. I don't think we're doing anything right now that would prevent that in the future though.

@rspbot

rspbot commented Apr 23, 2026

Copy link
Copy Markdown
## API Changes

react-aria-components

/react-aria-components:Row

 Row <T extends {}> {
   children?: ReactNode | ({}) => ReactElement
   className?: ClassNameOrFunction<RowRenderProps> = 'react-aria-Row'
   columns?: Iterable<{}>
   dependencies?: ReadonlyArray<any>
+  disabledBehavior?: DisabledBehavior
   download?: boolean | string
   hasChildItems?: boolean
   href?: Href
   hrefLang?: string
   isDisabled?: boolean
   onAction?: () => void
   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:RowProps

 RowProps <T> {
   children?: ReactNode | (T) => ReactElement
   className?: ClassNameOrFunction<RowRenderProps> = 'react-aria-Row'
   columns?: Iterable<T>
   dependencies?: ReadonlyArray<any>
+  disabledBehavior?: DisabledBehavior
   download?: boolean | string
   hasChildItems?: boolean
   href?: Href
   hrefLang?: string
   isDisabled?: boolean
   onAction?: () => void
   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:TableFooter

+TableFooter <T extends {}> {
+  children?: ReactNode | (T) => ReactNode
+  className?: string = 'react-aria-TableFooter'
+  dependencies?: ReadonlyArray<any>
+  items?: Iterable<T>
+  style?: CSSProperties
+}

/react-aria-components:TableFooterProps

+TableFooterProps <T> {
+  children?: ReactNode | (T) => ReactNode
+  className?: string = 'react-aria-TableFooter'
+  dependencies?: ReadonlyArray<any>
+  items?: Iterable<T>
+  style?: CSSProperties
+}

@react-spectrum/s2

/@react-spectrum/s2:TableFooter

+TableFooter <T extends {}> {
+  children?: ReactNode | (T) => ReactNode
+  dependencies?: ReadonlyArray<any>
+  items?: Iterable<T>
+}

/@react-spectrum/s2:TableFooterProps

+TableFooterProps <T> {
+  children?: ReactNode | (T) => ReactNode
+  dependencies?: ReadonlyArray<any>
+  items?: Iterable<T>
+}

@devongovett devongovett added this pull request to the merge queue Apr 23, 2026
Merged via the queue into main with commit 918f0ae Apr 23, 2026
31 checks passed
@devongovett devongovett deleted the table-footer branch April 23, 2026 23:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment