Skip to content

Commit fbd1a8c

Browse files
feat(Table): add composable sticky footer
1 parent a01cd63 commit fbd1a8c

7 files changed

Lines changed: 114 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: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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 a tfoot element with the table footer class', () => {
6+
render(
7+
<table>
8+
<Tfoot>
9+
<tr>
10+
<td>Footer</td>
11+
</tr>
12+
</Tfoot>
13+
</table>
14+
);
15+
16+
expect(screen.getByText('Footer').closest('tfoot')).toHaveClass(styles.tableTfoot);
17+
});
18+
19+
test('Forwards props, class names, and refs to the tfoot element', () => {
20+
const ref = { current: null } as React.RefObject<HTMLTableSectionElement>;
21+
22+
render(
23+
<table>
24+
<Tfoot ref={ref} className="custom-footer" data-testid="footer">
25+
<tr />
26+
</Tfoot>
27+
</table>
28+
);
29+
30+
expect(screen.getByTestId('footer')).toHaveClass(styles.tableTfoot, 'custom-footer');
31+
expect(ref.current).toBe(screen.getByTestId('footer'));
32+
});

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" gridBreakPoint="" 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)