Skip to content

Commit 00a1908

Browse files
feat(Table): add dynamic sticky footer support (#12664)
1 parent 6a48602 commit 00a1908

4 files changed

Lines changed: 115 additions & 0 deletions

File tree

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ export interface TableProps extends React.HTMLProps<HTMLTableElement>, OUIAProps
6060
isStickyHeaderStuck?: boolean;
6161
/** Flag indicating the table footer should stick to the bottom of its scroll container. */
6262
isStickyFooter?: boolean;
63+
/** @beta Flag indicating the table footer should have sticky positioning to the bottom of its scroll container. */
64+
isStickyFooterBase?: boolean;
65+
/** @beta Flag indicating the table footer should have stuck styling, when the footer is not at the bottom of the scroll container. */
66+
isStickyFooterStuck?: boolean;
6367
/** @hide Forwarded ref */
6468
innerRef?: React.RefObject<any>;
6569
/** Flag indicating table is a tree table */
@@ -107,6 +111,8 @@ const TableBase: React.FunctionComponent<TableProps> = ({
107111
isStickyHeaderBase = false,
108112
isStickyHeaderStuck = false,
109113
isStickyFooter = false,
114+
isStickyFooterBase = false,
115+
isStickyFooterStuck = false,
110116
isPlain = false,
111117
isNoPlainOnGlass = false,
112118
gridBreakPoint = TableGridBreakpoint.gridMd,
@@ -237,6 +243,8 @@ const TableBase: React.FunctionComponent<TableProps> = ({
237243
isStickyHeaderBase && styles.modifiers.stickyHeaderBase,
238244
isStickyHeaderStuck && styles.modifiers.stickyHeaderStuck,
239245
isStickyFooter && styles.modifiers.stickyFooter,
246+
isStickyFooterBase && styles.modifiers.stickyFooterBase,
247+
isStickyFooterStuck && styles.modifiers.stickyFooterStuck,
240248
isTreeTable && stylesTreeView.modifiers.treeView,
241249
isStriped && styles.modifiers.striped,
242250
isExpandable && styles.modifiers.expandable,

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,3 +234,39 @@ test(`Does not render with class ${styles.modifiers.stickyFooter} when isStickyF
234234

235235
expect(screen.getByRole('grid', { name: 'Test table' })).not.toHaveClass(styles.modifiers.stickyFooter);
236236
});
237+
238+
test(`Renders with class ${styles.modifiers.stickyFooterBase} when isStickyFooterBase is true`, () => {
239+
render(<Table isStickyFooterBase aria-label="Test table" />);
240+
241+
expect(screen.getByRole('grid', { name: 'Test table' })).toHaveClass(styles.modifiers.stickyFooterBase);
242+
});
243+
244+
test(`Does not render with class ${styles.modifiers.stickyFooterBase} when isStickyFooterBase is false`, () => {
245+
render(<Table isStickyFooterBase={false} aria-label="Test table" />);
246+
247+
expect(screen.getByRole('grid', { name: 'Test table' })).not.toHaveClass(styles.modifiers.stickyFooterBase);
248+
});
249+
250+
test(`Renders with class ${styles.modifiers.stickyFooterStuck} when isStickyFooterStuck is true`, () => {
251+
render(<Table isStickyFooterStuck aria-label="Test table" />);
252+
253+
expect(screen.getByRole('grid', { name: 'Test table' })).toHaveClass(styles.modifiers.stickyFooterStuck);
254+
});
255+
256+
test(`Does not render with class ${styles.modifiers.stickyFooterStuck} when isStickyFooterStuck is false`, () => {
257+
render(<Table isStickyFooterStuck={false} aria-label="Test table" />);
258+
259+
expect(screen.getByRole('grid', { name: 'Test table' })).not.toHaveClass(styles.modifiers.stickyFooterStuck);
260+
});
261+
262+
test(`Does not render with class ${styles.modifiers.stickyFooterBase} when isStickyFooterBase is undefined`, () => {
263+
render(<Table aria-label="Test table" />);
264+
265+
expect(screen.getByRole('grid', { name: 'Test table' })).not.toHaveClass(styles.modifiers.stickyFooterBase);
266+
});
267+
268+
test(`Does not render with class ${styles.modifiers.stickyFooterStuck} when isStickyFooterStuck is undefined`, () => {
269+
render(<Table aria-label="Test table" />);
270+
271+
expect(screen.getByRole('grid', { name: 'Test table' })).not.toHaveClass(styles.modifiers.stickyFooterStuck);
272+
});

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,16 @@ Use `Tfoot` for semantic table footer rows. Set `isStickyFooter` on `Table` to k
489489

490490
```
491491

492+
### Dynamic sticky footer
493+
494+
A sticky footer may alternatively be implemented with two properties: `isStickyFooterBase` and `isStickyFooterStuck` - which allows separate control of the sticky position and sticky styling. `isStickyFooterBase` should always be applied to make the footer position sticky, and `isStickyFooterStuck` may be applied dynamically to enable the sticky styling, such as when the sticky footer is not at the bottom of the scroll parent as shown in the example.
495+
496+
`isStickyFooter` acts as if both properties are present and true when applied, and is useful when dynamic sticky styling is not necessary.
497+
498+
```ts file="TableStickyFooterDynamic.tsx"
499+
500+
```
501+
492502
### Striped
493503

494504
To apply striping to a basic table, add the `isStriped` property to `Table`.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { useLayoutEffect, useRef, useState } from 'react';
2+
import { getResizeObserver } from '@patternfly/react-core';
3+
import { Table, Thead, Tfoot, Tr, Th, Tbody, Td, InnerScrollContainer } from '@patternfly/react-table';
4+
5+
const useIsStuckFromScrollParent = (scrollParentRef: React.RefObject<HTMLDivElement>): boolean => {
6+
const [isStuck, setIsStuck] = useState(false);
7+
8+
useLayoutEffect(() => {
9+
const scrollElement = scrollParentRef.current;
10+
if (!scrollElement) {
11+
return;
12+
}
13+
14+
const syncFromScroll = () => {
15+
setIsStuck(scrollElement.scrollTop + scrollElement.clientHeight < scrollElement.scrollHeight);
16+
};
17+
syncFromScroll();
18+
scrollElement.addEventListener('scroll', syncFromScroll, { passive: true });
19+
const observer = getResizeObserver(scrollElement, syncFromScroll, true);
20+
return () => {
21+
scrollElement.removeEventListener('scroll', syncFromScroll);
22+
observer();
23+
};
24+
}, [scrollParentRef]);
25+
26+
return isStuck;
27+
};
28+
29+
export const TableStickyFooterDynamic: React.FunctionComponent = () => {
30+
const scrollContainerRef = useRef<HTMLDivElement>(null);
31+
const isStuck = useIsStuckFromScrollParent(scrollContainerRef);
32+
const rows = Array.from({ length: 12 }, (_, index) => index + 1);
33+
34+
return (
35+
<div style={{ height: '400px' }}>
36+
<InnerScrollContainer ref={scrollContainerRef}>
37+
<Table aria-label="Dynamic sticky footer table" isStickyFooterBase isStickyFooterStuck={isStuck}>
38+
<Thead>
39+
<Tr>
40+
<Th>Item</Th>
41+
<Th>Value</Th>
42+
</Tr>
43+
</Thead>
44+
<Tbody>
45+
{rows.map((row) => (
46+
<Tr key={row}>
47+
<Td dataLabel="Item">Item {row}</Td>
48+
<Td dataLabel="Value">Value {row}</Td>
49+
</Tr>
50+
))}
51+
</Tbody>
52+
<Tfoot>
53+
<Tr>
54+
<Td colSpan={2}>Total: {rows.length} items</Td>
55+
</Tr>
56+
</Tfoot>
57+
</Table>
58+
</InnerScrollContainer>
59+
</div>
60+
);
61+
};

0 commit comments

Comments
 (0)