Skip to content

Commit 73590e1

Browse files
committed
feat(Table): separate sticky and pinned styles
1 parent 93e7816 commit 73590e1

File tree

2 files changed

+88
-17
lines changed

2 files changed

+88
-17
lines changed

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,14 @@ interface TableContextProps {
8282
registerSelectableRow?: () => void;
8383
hasAnimations?: boolean;
8484
variant?: TableVariant | 'compact';
85+
isStickyHeader?: boolean;
8586
}
8687

8788
export const TableContext = createContext<TableContextProps>({
8889
registerSelectableRow: () => {},
8990
hasAnimations: false,
90-
variant: undefined
91+
variant: undefined,
92+
isStickyHeader: false
9193
});
9294

9395
const TableBase: React.FunctionComponent<TableProps> = ({
@@ -211,7 +213,7 @@ const TableBase: React.FunctionComponent<TableProps> = ({
211213
};
212214

213215
return (
214-
<TableContext.Provider value={{ registerSelectableRow, hasAnimations, variant }}>
216+
<TableContext.Provider value={{ registerSelectableRow, hasAnimations, variant, isStickyHeader }}>
215217
<table
216218
aria-label={ariaLabel}
217219
role={role}

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

Lines changed: 84 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,33 @@
1-
import { forwardRef } from 'react';
1+
import { forwardRef, useCallback, useContext, useEffect, useRef, useState } from 'react';
22
import { css } from '@patternfly/react-styles';
33
import styles from '@patternfly/react-styles/css/components/Table/table';
4+
import { TableContext } from './Table';
5+
6+
/** Ratio must be below this to count as “pinned” (avoids doc-layout subpixel + strict threshold: [1] never hitting exactly 1). */
7+
const PINNED_INTERSECTION_RATIO = 0.999;
8+
9+
const getOverflowScrollParent = (node: HTMLElement): Element | null => {
10+
let parent = node.parentElement;
11+
while (parent) {
12+
const style = getComputedStyle(parent);
13+
if (/(auto|scroll|overlay)/.test(style.overflowY) || /(auto|scroll|overlay)/.test(style.overflowX)) {
14+
return parent;
15+
}
16+
parent = parent.parentElement;
17+
}
18+
return null;
19+
};
20+
21+
const assignRef = <T,>(ref: React.Ref<T> | undefined, value: T | null) => {
22+
if (!ref) {
23+
return;
24+
}
25+
if (typeof ref === 'function') {
26+
ref(value);
27+
} else {
28+
(ref as React.MutableRefObject<T | null>).current = value;
29+
}
30+
};
431

532
export interface TheadProps extends React.HTMLProps<HTMLTableSectionElement> {
633
/** Content rendered inside the <thead> row group */
@@ -22,20 +49,62 @@ const TheadBase: React.FunctionComponent<TheadProps> = ({
2249
innerRef,
2350
hasNestedHeader,
2451
...props
25-
}: TheadProps) => (
26-
<thead
27-
className={css(
28-
styles.tableThead,
29-
className,
30-
noWrap && styles.modifiers.nowrap,
31-
hasNestedHeader && styles.modifiers.nestedColumnHeader
32-
)}
33-
ref={innerRef}
34-
{...props}
35-
>
36-
{children}
37-
</thead>
38-
);
52+
}: TheadProps) => {
53+
const { isStickyHeader } = useContext(TableContext);
54+
const observeStickyPin = !!isStickyHeader;
55+
const [isPinned, setIsPinned] = useState(false);
56+
const theadElRef = useRef<HTMLTableSectionElement | null>(null);
57+
58+
const setTheadRef = useCallback(
59+
(node: HTMLTableSectionElement | null) => {
60+
theadElRef.current = node;
61+
assignRef(innerRef, node);
62+
},
63+
[innerRef]
64+
);
65+
66+
useEffect(() => {
67+
if (!observeStickyPin || typeof IntersectionObserver === 'undefined') {
68+
setIsPinned(false);
69+
return;
70+
}
71+
72+
const el = theadElRef.current;
73+
if (!el) {
74+
return;
75+
}
76+
77+
const scrollRoot = getOverflowScrollParent(el);
78+
79+
// Requires sticky thead `inset-block-start: -1px` in CSS (see table.css).
80+
const observer = new IntersectionObserver(
81+
([entry]) => {
82+
// console.log(scrollRoot, entry, entry.intersectionRatio);
83+
setIsPinned(entry.intersectionRatio < PINNED_INTERSECTION_RATIO);
84+
},
85+
{ threshold: [0, 1], root: scrollRoot }
86+
);
87+
88+
observer.observe(el);
89+
return () => observer.disconnect();
90+
}, [observeStickyPin]);
91+
92+
return (
93+
<thead
94+
className={css(
95+
styles.tableThead,
96+
className,
97+
noWrap && styles.modifiers.nowrap,
98+
hasNestedHeader && styles.modifiers.nestedColumnHeader,
99+
observeStickyPin && isPinned && 'PINNED'
100+
)}
101+
ref={setTheadRef}
102+
{...props}
103+
>
104+
{children}
105+
</thead>
106+
);
107+
};
39108

40109
export const Thead = forwardRef((props: TheadProps, ref: React.Ref<HTMLTableSectionElement>) => (
41110
<TheadBase {...props} innerRef={ref} />

0 commit comments

Comments
 (0)