Skip to content

Commit aa905db

Browse files
feat(Table): add composable sticky footer (#12645)
1 parent 008744f commit aa905db

7 files changed

Lines changed: 147 additions & 0 deletions

File tree

packages/react-table/src/components/Table/Table.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ export interface TableProps extends React.HTMLProps<HTMLTableElement>, OUIAProps
5858
isStickyHeaderBase?: boolean;
5959
/** @beta Flag indicating the table header should have stuck styling, when the header is not at the top of the scroll container. */
6060
isStickyHeaderStuck?: boolean;
61+
/** Flag indicating the table footer should stick to the bottom of its scroll container. */
62+
isStickyFooter?: boolean;
6163
/** @hide Forwarded ref */
6264
innerRef?: React.RefObject<any>;
6365
/** Flag indicating table is a tree table */
@@ -104,6 +106,7 @@ const TableBase: React.FunctionComponent<TableProps> = ({
104106
isStickyHeader = false,
105107
isStickyHeaderBase = false,
106108
isStickyHeaderStuck = false,
109+
isStickyFooter = false,
107110
isPlain = false,
108111
isNoPlainOnGlass = false,
109112
gridBreakPoint = TableGridBreakpoint.gridMd,
@@ -233,6 +236,7 @@ const TableBase: React.FunctionComponent<TableProps> = ({
233236
isStickyHeader && styles.modifiers.stickyHeader,
234237
isStickyHeaderBase && styles.modifiers.stickyHeaderBase,
235238
isStickyHeaderStuck && styles.modifiers.stickyHeaderStuck,
239+
isStickyFooter && styles.modifiers.stickyFooter,
236240
isTreeTable && stylesTreeView.modifiers.treeView,
237241
isStriped && styles.modifiers.striped,
238242
isExpandable && styles.modifiers.expandable,
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { forwardRef } from 'react';
2+
import { css } from '@patternfly/react-styles';
3+
import styles from '@patternfly/react-styles/css/components/Table/table';
4+
5+
export interface TfootProps extends React.HTMLProps<HTMLTableSectionElement> {
6+
/** Content rendered inside the <tfoot> row group */
7+
children?: React.ReactNode;
8+
/** Additional classes added to the <tfoot> element */
9+
className?: string;
10+
/** @hide Forwarded ref */
11+
innerRef?: React.Ref<any>;
12+
}
13+
14+
const TfootBase: React.FunctionComponent<TfootProps> = ({ children, className, innerRef, ...props }: TfootProps) => (
15+
<tfoot className={css(styles.tableTfoot, className)} ref={innerRef} {...props}>
16+
{children}
17+
</tfoot>
18+
);
19+
20+
export const Tfoot = forwardRef((props: TfootProps, ref: React.Ref<HTMLTableSectionElement>) => (
21+
<TfootBase {...props} innerRef={ref} />
22+
));
23+
Tfoot.displayName = 'Tfoot';

packages/react-table/src/components/Table/__tests__/Table.test.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,3 +222,15 @@ test(`Does not render with class ${styles.modifiers.stickyHeaderStuck} when isSt
222222

223223
expect(screen.getByRole('grid', { name: 'Test table' })).not.toHaveClass(styles.modifiers.stickyHeaderStuck);
224224
});
225+
226+
test(`Renders with class ${styles.modifiers.stickyFooter} when isStickyFooter is true`, () => {
227+
render(<Table isStickyFooter aria-label="Test table" />);
228+
229+
expect(screen.getByRole('grid', { name: 'Test table' })).toHaveClass(styles.modifiers.stickyFooter);
230+
});
231+
232+
test(`Does not render with class ${styles.modifiers.stickyFooter} when isStickyFooter is false`, () => {
233+
render(<Table isStickyFooter={false} aria-label="Test table" />);
234+
235+
expect(screen.getByRole('grid', { name: 'Test table' })).not.toHaveClass(styles.modifiers.stickyFooter);
236+
});
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { render, screen } from '@testing-library/react';
2+
import { Tfoot } from '../Tfoot';
3+
import styles from '@patternfly/react-styles/css/components/Table/table';
4+
5+
test('Renders without children', () => {
6+
render(
7+
<table>
8+
<Tfoot />
9+
</table>
10+
);
11+
12+
expect(screen.getByRole('rowgroup')).toBeInTheDocument();
13+
});
14+
15+
test('Renders with children', () => {
16+
render(
17+
<table>
18+
<Tfoot>Footer content</Tfoot>
19+
</table>
20+
);
21+
22+
expect(screen.getByRole('rowgroup')).toHaveTextContent('Footer content');
23+
});
24+
25+
test(`Renders with class ${styles.tableTfoot} only by default`, () => {
26+
render(
27+
<table>
28+
<Tfoot />
29+
</table>
30+
);
31+
32+
expect(screen.getByRole('rowgroup')).toHaveClass(styles.tableTfoot, { exact: true });
33+
});
34+
35+
test('Forwards refs to the tfoot element', () => {
36+
const ref = { current: null } as React.RefObject<HTMLTableSectionElement>;
37+
38+
render(
39+
<table>
40+
<Tfoot ref={ref} />
41+
</table>
42+
);
43+
44+
expect(ref.current).toBe(screen.getByRole('rowgroup'));
45+
});
46+
47+
test('Renders with custom class names provided via prop', () => {
48+
render(
49+
<table>
50+
<Tfoot className="custom-footer" />
51+
</table>
52+
);
53+
54+
expect(screen.getByRole('rowgroup')).toHaveClass('custom-footer');
55+
});
56+
57+
test('Spreads additional props', () => {
58+
render(
59+
<table>
60+
<Tfoot data-custom="true" />
61+
</table>
62+
);
63+
64+
expect(screen.getByRole('rowgroup')).toHaveAttribute('data-custom', 'true');
65+
});

packages/react-table/src/components/Table/examples/Table.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ propComponents:
1010
'Tr',
1111
'Th',
1212
'Td',
13+
'Tfoot',
1314
'Caption',
1415
'TableText',
1516
'TdActionsType',
@@ -480,6 +481,14 @@ The second `Tr` represents the second level of sub columns. The `Th` in this row
480481

481482
```
482483

484+
### Sticky footer
485+
486+
Use `Tfoot` for semantic table footer rows. Set `isStickyFooter` on `Table` to keep the footer visible while its scroll container is scrolling.
487+
488+
```ts file="TableStickyFooter.tsx"
489+
490+
```
491+
483492
### Striped
484493

485494
To apply striping to a basic table, add the `isStriped` property to `Table`.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Table, Thead, Tfoot, Tr, Th, Tbody, Td, InnerScrollContainer } from '@patternfly/react-table';
2+
3+
export const TableStickyFooter: React.FunctionComponent = () => {
4+
const rows = Array.from({ length: 12 }, (_, index) => index + 1);
5+
6+
return (
7+
<div style={{ height: '400px' }}>
8+
<InnerScrollContainer>
9+
<Table aria-label="Sticky footer table" isStickyFooter>
10+
<Thead>
11+
<Tr>
12+
<Th>Item</Th>
13+
<Th>Value</Th>
14+
</Tr>
15+
</Thead>
16+
<Tbody>
17+
{rows.map((row) => (
18+
<Tr key={row}>
19+
<Td dataLabel="Item">Item {row}</Td>
20+
<Td dataLabel="Value">Value {row}</Td>
21+
</Tr>
22+
))}
23+
</Tbody>
24+
<Tfoot>
25+
<Tr>
26+
<Td colSpan={2}>Total: {rows.length} items</Td>
27+
</Tr>
28+
</Tfoot>
29+
</Table>
30+
</InnerScrollContainer>
31+
</div>
32+
);
33+
};

packages/react-table/src/components/Table/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export * from './TreeRowWrapper';
1616
export * from './Table';
1717
export * from './Thead';
1818
export * from './Tbody';
19+
export * from './Tfoot';
1920
export * from './Tr';
2021
export * from './Th';
2122
export * from './Td';

0 commit comments

Comments
 (0)