Skip to content

Commit 1d8aa61

Browse files
committed
feat(Table): support dynamic sticky styling
1 parent a7a847c commit 1d8aa61

File tree

4 files changed

+169
-5
lines changed

4 files changed

+169
-5
lines changed
Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { forwardRef } from 'react';
12
import { css } from '@patternfly/react-styles';
23
import styles from '@patternfly/react-styles/css/components/Table/table-scrollable';
34

@@ -6,16 +7,22 @@ export interface InnerScrollContainerProps extends React.HTMLProps<HTMLDivElemen
67
children?: React.ReactNode;
78
/** Additional classes added to the container */
89
className?: string;
10+
/** @hide Forwarded ref */
11+
innerRef?: React.Ref<HTMLDivElement>;
912
}
1013

11-
export const InnerScrollContainer: React.FunctionComponent<InnerScrollContainerProps> = ({
14+
const InnerScrollContainerBase: React.FunctionComponent<InnerScrollContainerProps> = ({
1215
children,
1316
className,
17+
innerRef,
1418
...props
1519
}: InnerScrollContainerProps) => (
16-
<div className={css(className, styles.scrollInnerWrapper)} {...props}>
20+
<div ref={innerRef} className={css(className, styles.scrollInnerWrapper)} {...props}>
1721
{children}
1822
</div>
1923
);
2024

25+
export const InnerScrollContainer = forwardRef((props: InnerScrollContainerProps, ref: React.Ref<HTMLDivElement>) => (
26+
<InnerScrollContainerBase innerRef={ref} {...props} />
27+
));
2128
InnerScrollContainer.displayName = 'InnerScrollContainer';

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,12 @@ export interface TableProps extends React.HTMLProps<HTMLTableElement>, OUIAProps
5252
isPlain?: boolean;
5353
/** @beta Flag indicating if the table should not have plain styling when in the glass theme */
5454
isNoPlainOnGlass?: boolean;
55-
/** If set to true, the table header sticks to the top of its container */
55+
/** If set to true, the table header sticks to the top of its container. This property applies both the sticky position and styling. */
5656
isStickyHeader?: boolean;
57+
/** @beta Flag indicating the table header should have sticky positioning to the top of the parentInnerScrollContainer. */
58+
isStickyHeaderBase?: boolean;
59+
/** @beta Flag indicating the table header should have stuck styling, when the header is not at the top of the scroll container. */
60+
isStickyHeaderStuck?: boolean;
5761
/** @hide Forwarded ref */
5862
innerRef?: React.RefObject<any>;
5963
/** Flag indicating table is a tree table */
@@ -98,6 +102,8 @@ const TableBase: React.FunctionComponent<TableProps> = ({
98102
variant,
99103
borders = true,
100104
isStickyHeader = false,
105+
isStickyHeaderBase = false,
106+
isStickyHeaderStuck = false,
101107
isPlain = false,
102108
isNoPlainOnGlass = false,
103109
gridBreakPoint = TableGridBreakpoint.gridMd,
@@ -225,6 +231,8 @@ const TableBase: React.FunctionComponent<TableProps> = ({
225231
styles.modifiers[variant],
226232
!borders && styles.modifiers.noBorderRows,
227233
isStickyHeader && styles.modifiers.stickyHeader,
234+
isStickyHeaderBase && styles.modifiers.stickyHeaderBase,
235+
isStickyHeaderStuck && styles.modifiers.stickyHeaderStuck,
228236
isTreeTable && stylesTreeView.modifiers.treeView,
229237
isStriped && styles.modifiers.striped,
230238
isExpandable && styles.modifiers.expandable,

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ The `Table` component takes an explicit and declarative approach, and its implem
4141

4242
The documentation for the deprecated table implementation can be found under the [React deprecated](/components/table/react-deprecated) tab. It is configuration based and takes a less declarative and more implicit approach to laying out the table structure, such as the rows and cells within it.
4343

44-
import { Fragment, isValidElement, useCallback, useEffect, useRef, useState } from 'react';
44+
import { Fragment, isValidElement, useCallback, useEffect, useRef, useState, useLayoutEffect } from 'react';
4545
import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon';
4646
import CodeBranchIcon from '@patternfly/react-icons/dist/esm/icons/code-branch-icon';
4747
import CodeIcon from '@patternfly/react-icons/dist/esm/icons/code-icon';
@@ -327,7 +327,6 @@ To enable a tree table:
327327
- `checkAriaLabel` - (optional) accessible label for the checkbox
328328
- `showDetailsAriaLabel` - (optional) accessible label for the show row details button in the responsive view
329329
4. The first `Td` in each row will pass the following to the `treeRow` prop:
330-
331330
- `onCollapse` - Callback when user expands/collapses a row to reveal/hide the row's children.
332331
- `onCheckChange` - (optional) Callback when user changes the checkbox on a row.
333332
- `onToggleRowDetails` - (optional) Callback when user shows/hides the row details in responsive view.
@@ -419,6 +418,16 @@ To prevent the default text wrapping behavior and allow horizontal scrolling, al
419418

420419
```
421420

421+
### Dynamic sticky header
422+
423+
A sticky header may alternatively be implemented with two properties: `isStickyHeaderBase` and `isStickyHeaderStuck` - which allows separate control of the sticky position and sticky styling. `isStickyHeaderBase` should always be applied to make the header position sticky, and `isStickyHeaderStuck` may be applied dynamically to enable the sticky styling, such as when the sticky header is not at the top of the scroll parent as shown in the example.
424+
425+
`isStickyHeader` acts as if both properties are present and true when applied, and is useful when dynamic sticky styling is not necessary.
426+
427+
```ts file="TableStickyHeaderDynamic.tsx"
428+
429+
```
430+
422431
### Sticky columns and header
423432

424433
To maintain proper sticky behavior across sticky columns and header, `Table` must be wrapped with `OuterScrollContainer` and `InnerScrollContainer`.
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import { useLayoutEffect, useRef, useState } from 'react';
2+
import { Table, Thead, Tr, Th, Tbody, Td, InnerScrollContainer } from '@patternfly/react-table';
3+
import BlueprintIcon from '@patternfly/react-icons/dist/esm/icons/blueprint-icon';
4+
5+
interface Fact {
6+
name: string;
7+
state: string;
8+
detail1: string;
9+
detail2: string;
10+
detail3: string;
11+
detail4: string;
12+
detail5: string;
13+
detail6: string;
14+
detail7: string;
15+
}
16+
17+
const useIsStuckFromScrollParent = ({
18+
shouldTrack,
19+
scrollParentRef
20+
}: {
21+
/** Indicates whether to track the scroll top position of the scroll parent element */
22+
shouldTrack: boolean;
23+
/** Reference to the scroll parent element */
24+
scrollParentRef: React.RefObject<any>;
25+
}): boolean => {
26+
const [isStuck, setIsStuck] = useState(false);
27+
28+
useLayoutEffect(() => {
29+
if (!shouldTrack) {
30+
setIsStuck(false);
31+
return;
32+
}
33+
34+
const scrollElement = scrollParentRef.current;
35+
if (!scrollElement) {
36+
setIsStuck(false);
37+
return;
38+
}
39+
40+
const syncFromScroll = () => {
41+
setIsStuck(scrollElement.scrollTop > 0);
42+
};
43+
syncFromScroll();
44+
scrollElement.addEventListener('scroll', syncFromScroll, { passive: true });
45+
return () => scrollElement.removeEventListener('scroll', syncFromScroll);
46+
}, [shouldTrack, scrollParentRef]);
47+
48+
return isStuck;
49+
};
50+
51+
export const TableStickyHeaderDynamic: React.FunctionComponent = () => {
52+
const scrollContainerRef = useRef<HTMLDivElement>(null);
53+
const isStuck = useIsStuckFromScrollParent({ shouldTrack: true, scrollParentRef: scrollContainerRef });
54+
55+
// In real usage, this data would come from some external source like an API via props.
56+
const facts: Fact[] = Array.from({ length: 9 }, (_, index) => ({
57+
name: `Fact ${index + 1}`,
58+
state: `State ${index + 1}`,
59+
detail1: `Test cell ${index + 1}-3`,
60+
detail2: `Test cell ${index + 1}-4`,
61+
detail3: `Test cell ${index + 1}-5`,
62+
detail4: `Test cell ${index + 1}-6`,
63+
detail5: `Test cell ${index + 1}-7`,
64+
detail6: `Test cell ${index + 1}-8`,
65+
detail7: `Test cell ${index + 1}-9`
66+
}));
67+
68+
const columnNames = {
69+
name: 'Fact',
70+
state: 'State',
71+
header3: 'Header 3',
72+
header4: 'Header 4',
73+
header5: 'Header 5',
74+
header6: 'Header 6',
75+
header7: 'Header 7',
76+
header8: 'Header 8',
77+
header9: 'Header 9'
78+
};
79+
80+
return (
81+
<div style={{ height: '400px' }}>
82+
<InnerScrollContainer ref={scrollContainerRef}>
83+
<Table
84+
aria-label="Sticky columns and header table"
85+
gridBreakPoint=""
86+
isStickyHeaderBase
87+
isStickyHeaderStuck={isStuck}
88+
>
89+
<Thead>
90+
<Tr>
91+
<Th modifier="truncate">{columnNames.name}</Th>
92+
<Th modifier="truncate">{columnNames.state}</Th>
93+
<Th modifier="truncate">{columnNames.header3}</Th>
94+
<Th modifier="truncate">{columnNames.header4}</Th>
95+
<Th modifier="truncate">{columnNames.header5}</Th>
96+
<Th modifier="truncate">{columnNames.header6}</Th>
97+
<Th modifier="truncate">{columnNames.header7}</Th>
98+
<Th modifier="truncate">{columnNames.header8}</Th>
99+
<Th modifier="truncate">{columnNames.header9}</Th>
100+
</Tr>
101+
</Thead>
102+
<Tbody>
103+
{facts.map((fact) => (
104+
<Tr key={fact.name}>
105+
<Th modifier="nowrap" dataLabel={columnNames.name}>
106+
{fact.name}
107+
</Th>
108+
<Th modifier="nowrap" dataLabel={columnNames.state}>
109+
<BlueprintIcon />
110+
{` ${fact.state}`}
111+
</Th>
112+
<Td modifier="nowrap" dataLabel={columnNames.header3}>
113+
{fact.detail1}
114+
</Td>
115+
<Td modifier="nowrap" dataLabel={columnNames.header4}>
116+
{fact.detail2}
117+
</Td>
118+
<Td modifier="nowrap" dataLabel={columnNames.header5}>
119+
{fact.detail3}
120+
</Td>
121+
<Td modifier="nowrap" dataLabel={columnNames.header6}>
122+
{fact.detail4}
123+
</Td>
124+
<Td modifier="nowrap" dataLabel={columnNames.header7}>
125+
{fact.detail5}
126+
</Td>
127+
<Td modifier="nowrap" dataLabel={columnNames.header8}>
128+
{fact.detail6}
129+
</Td>
130+
<Td modifier="nowrap" dataLabel={columnNames.header9}>
131+
{fact.detail7}
132+
</Td>
133+
</Tr>
134+
))}
135+
</Tbody>
136+
</Table>
137+
</InnerScrollContainer>
138+
</div>
139+
);
140+
};

0 commit comments

Comments
 (0)