From 73590e1a6a538c9f06053053a34f61c5c4db7e53 Mon Sep 17 00:00:00 2001 From: Katie McFaul Date: Thu, 2 Apr 2026 19:18:24 -0400 Subject: [PATCH] feat(Table): separate sticky and pinned styles --- .../src/components/Table/Table.tsx | 6 +- .../src/components/Table/Thead.tsx | 99 ++++++++++++++++--- 2 files changed, 88 insertions(+), 17 deletions(-) diff --git a/packages/react-table/src/components/Table/Table.tsx b/packages/react-table/src/components/Table/Table.tsx index 144fc0ccb24..6084c3cb708 100644 --- a/packages/react-table/src/components/Table/Table.tsx +++ b/packages/react-table/src/components/Table/Table.tsx @@ -82,12 +82,14 @@ interface TableContextProps { registerSelectableRow?: () => void; hasAnimations?: boolean; variant?: TableVariant | 'compact'; + isStickyHeader?: boolean; } export const TableContext = createContext({ registerSelectableRow: () => {}, hasAnimations: false, - variant: undefined + variant: undefined, + isStickyHeader: false }); const TableBase: React.FunctionComponent = ({ @@ -211,7 +213,7 @@ const TableBase: React.FunctionComponent = ({ }; return ( - + { + let parent = node.parentElement; + while (parent) { + const style = getComputedStyle(parent); + if (/(auto|scroll|overlay)/.test(style.overflowY) || /(auto|scroll|overlay)/.test(style.overflowX)) { + return parent; + } + parent = parent.parentElement; + } + return null; +}; + +const assignRef = (ref: React.Ref | undefined, value: T | null) => { + if (!ref) { + return; + } + if (typeof ref === 'function') { + ref(value); + } else { + (ref as React.MutableRefObject).current = value; + } +}; export interface TheadProps extends React.HTMLProps { /** Content rendered inside the row group */ @@ -22,20 +49,62 @@ const TheadBase: React.FunctionComponent = ({ innerRef, hasNestedHeader, ...props -}: TheadProps) => ( - - {children} - -); +}: TheadProps) => { + const { isStickyHeader } = useContext(TableContext); + const observeStickyPin = !!isStickyHeader; + const [isPinned, setIsPinned] = useState(false); + const theadElRef = useRef(null); + + const setTheadRef = useCallback( + (node: HTMLTableSectionElement | null) => { + theadElRef.current = node; + assignRef(innerRef, node); + }, + [innerRef] + ); + + useEffect(() => { + if (!observeStickyPin || typeof IntersectionObserver === 'undefined') { + setIsPinned(false); + return; + } + + const el = theadElRef.current; + if (!el) { + return; + } + + const scrollRoot = getOverflowScrollParent(el); + + // Requires sticky thead `inset-block-start: -1px` in CSS (see table.css). + const observer = new IntersectionObserver( + ([entry]) => { + // console.log(scrollRoot, entry, entry.intersectionRatio); + setIsPinned(entry.intersectionRatio < PINNED_INTERSECTION_RATIO); + }, + { threshold: [0, 1], root: scrollRoot } + ); + + observer.observe(el); + return () => observer.disconnect(); + }, [observeStickyPin]); + + return ( + + {children} + + ); +}; export const Thead = forwardRef((props: TheadProps, ref: React.Ref) => (