From fbd1a8c7a32c5522249acaec9d506a3d3ec50ec2 Mon Sep 17 00:00:00 2001 From: Gabi Podolnikova Date: Mon, 7 Sep 2026 13:26:55 +0200 Subject: [PATCH] feat(Table): add composable sticky footer --- .../src/components/Table/Table.tsx | 4 +++ .../src/components/Table/Tfoot.tsx | 23 +++++++++++++ .../components/Table/__tests__/Table.test.tsx | 12 +++++++ .../components/Table/__tests__/Tfoot.test.tsx | 32 ++++++++++++++++++ .../src/components/Table/examples/Table.md | 9 +++++ .../Table/examples/TableStickyFooter.tsx | 33 +++++++++++++++++++ .../react-table/src/components/Table/index.ts | 1 + 7 files changed, 114 insertions(+) create mode 100644 packages/react-table/src/components/Table/Tfoot.tsx create mode 100644 packages/react-table/src/components/Table/__tests__/Tfoot.test.tsx create mode 100644 packages/react-table/src/components/Table/examples/TableStickyFooter.tsx diff --git a/packages/react-table/src/components/Table/Table.tsx b/packages/react-table/src/components/Table/Table.tsx index 8eccba9f9ed..499f7b31d70 100644 --- a/packages/react-table/src/components/Table/Table.tsx +++ b/packages/react-table/src/components/Table/Table.tsx @@ -58,6 +58,8 @@ export interface TableProps extends React.HTMLProps, OUIAProps isStickyHeaderBase?: boolean; /** @beta Flag indicating the table header should have stuck styling, when the header is not at the top of the scroll container. */ isStickyHeaderStuck?: boolean; + /** Flag indicating the table footer should stick to the bottom of its scroll container. */ + isStickyFooter?: boolean; /** @hide Forwarded ref */ innerRef?: React.RefObject; /** Flag indicating table is a tree table */ @@ -104,6 +106,7 @@ const TableBase: React.FunctionComponent = ({ isStickyHeader = false, isStickyHeaderBase = false, isStickyHeaderStuck = false, + isStickyFooter = false, isPlain = false, isNoPlainOnGlass = false, gridBreakPoint = TableGridBreakpoint.gridMd, @@ -233,6 +236,7 @@ const TableBase: React.FunctionComponent = ({ isStickyHeader && styles.modifiers.stickyHeader, isStickyHeaderBase && styles.modifiers.stickyHeaderBase, isStickyHeaderStuck && styles.modifiers.stickyHeaderStuck, + isStickyFooter && styles.modifiers.stickyFooter, isTreeTable && stylesTreeView.modifiers.treeView, isStriped && styles.modifiers.striped, isExpandable && styles.modifiers.expandable, diff --git a/packages/react-table/src/components/Table/Tfoot.tsx b/packages/react-table/src/components/Table/Tfoot.tsx new file mode 100644 index 00000000000..8b4165184bb --- /dev/null +++ b/packages/react-table/src/components/Table/Tfoot.tsx @@ -0,0 +1,23 @@ +import { forwardRef } from 'react'; +import { css } from '@patternfly/react-styles'; +import styles from '@patternfly/react-styles/css/components/Table/table'; + +export interface TfootProps extends React.HTMLProps { + /** Content rendered inside the row group */ + children?: React.ReactNode; + /** Additional classes added to the element */ + className?: string; + /** @hide Forwarded ref */ + innerRef?: React.Ref; +} + +const TfootBase: React.FunctionComponent = ({ children, className, innerRef, ...props }: TfootProps) => ( + + {children} + +); + +export const Tfoot = forwardRef((props: TfootProps, ref: React.Ref) => ( + +)); +Tfoot.displayName = 'Tfoot'; diff --git a/packages/react-table/src/components/Table/__tests__/Table.test.tsx b/packages/react-table/src/components/Table/__tests__/Table.test.tsx index e49d1a9f6d1..8f79767c533 100644 --- a/packages/react-table/src/components/Table/__tests__/Table.test.tsx +++ b/packages/react-table/src/components/Table/__tests__/Table.test.tsx @@ -222,3 +222,15 @@ test(`Does not render with class ${styles.modifiers.stickyHeaderStuck} when isSt expect(screen.getByRole('grid', { name: 'Test table' })).not.toHaveClass(styles.modifiers.stickyHeaderStuck); }); + +test(`Renders with class ${styles.modifiers.stickyFooter} when isStickyFooter is true`, () => { + render(); + + expect(screen.getByRole('grid', { name: 'Test table' })).toHaveClass(styles.modifiers.stickyFooter); +}); + +test(`Does not render with class ${styles.modifiers.stickyFooter} when isStickyFooter is false`, () => { + render(
); + + expect(screen.getByRole('grid', { name: 'Test table' })).not.toHaveClass(styles.modifiers.stickyFooter); +}); diff --git a/packages/react-table/src/components/Table/__tests__/Tfoot.test.tsx b/packages/react-table/src/components/Table/__tests__/Tfoot.test.tsx new file mode 100644 index 00000000000..e0b7b38e338 --- /dev/null +++ b/packages/react-table/src/components/Table/__tests__/Tfoot.test.tsx @@ -0,0 +1,32 @@ +import { render, screen } from '@testing-library/react'; +import { Tfoot } from '../Tfoot'; +import styles from '@patternfly/react-styles/css/components/Table/table'; + +test('Renders a tfoot element with the table footer class', () => { + render( +
+ + + + + +
Footer
+ ); + + expect(screen.getByText('Footer').closest('tfoot')).toHaveClass(styles.tableTfoot); +}); + +test('Forwards props, class names, and refs to the tfoot element', () => { + const ref = { current: null } as React.RefObject; + + render( + + + + +
+ ); + + expect(screen.getByTestId('footer')).toHaveClass(styles.tableTfoot, 'custom-footer'); + expect(ref.current).toBe(screen.getByTestId('footer')); +}); diff --git a/packages/react-table/src/components/Table/examples/Table.md b/packages/react-table/src/components/Table/examples/Table.md index 771a45e5960..a5f2db70d8c 100644 --- a/packages/react-table/src/components/Table/examples/Table.md +++ b/packages/react-table/src/components/Table/examples/Table.md @@ -10,6 +10,7 @@ propComponents: 'Tr', 'Th', 'Td', + 'Tfoot', 'Caption', 'TableText', 'TdActionsType', @@ -480,6 +481,14 @@ The second `Tr` represents the second level of sub columns. The `Th` in this row ``` +### Sticky footer + +Use `Tfoot` for semantic table footer rows. Set `isStickyFooter` on `Table` to keep the footer visible while its scroll container is scrolling. + +```ts file="TableStickyFooter.tsx" + +``` + ### Striped To apply striping to a basic table, add the `isStriped` property to `Table`. diff --git a/packages/react-table/src/components/Table/examples/TableStickyFooter.tsx b/packages/react-table/src/components/Table/examples/TableStickyFooter.tsx new file mode 100644 index 00000000000..9cdffd9aa8f --- /dev/null +++ b/packages/react-table/src/components/Table/examples/TableStickyFooter.tsx @@ -0,0 +1,33 @@ +import { Table, Thead, Tfoot, Tr, Th, Tbody, Td, InnerScrollContainer } from '@patternfly/react-table'; + +export const TableStickyFooter: React.FunctionComponent = () => { + const rows = Array.from({ length: 12 }, (_, index) => index + 1); + + return ( +
+ + + + + + + + + + {rows.map((row) => ( + + + + + ))} + + + + + + +
ItemValue
Item {row}Value {row}
Total: {rows.length} items
+
+
+ ); +}; diff --git a/packages/react-table/src/components/Table/index.ts b/packages/react-table/src/components/Table/index.ts index e5260c213d8..f6ca11f5000 100644 --- a/packages/react-table/src/components/Table/index.ts +++ b/packages/react-table/src/components/Table/index.ts @@ -16,6 +16,7 @@ export * from './TreeRowWrapper'; export * from './Table'; export * from './Thead'; export * from './Tbody'; +export * from './Tfoot'; export * from './Tr'; export * from './Th'; export * from './Td';