()
| 9 | * fits into the viewport even when the header or footer are visible. |
| 10 | */ |
| 11 | export function useStickyPaneHeight() { |
| 12 | const rootRef = React.useRef<HTMLDivElement>(null) |
| 13 | |
| 14 | // Default the height to the viewport height |
| 15 | const [height, setHeight] = React.useState(dvh(100)) |
| 16 | const [offsetHeader, setOffsetHeader] = React.useState<number | string>(0) |
| 17 | |
| 18 | // Create intersection observers to track the top and bottom of the content region |
| 19 | const [contentTopRef, contentTopInView, contentTopEntry] = useInView() |
| 20 | const [contentBottomRef, contentBottomInView] = useInView() |
| 21 | |
| 22 | // Calculate the height of the sticky pane based on the position of the |
| 23 | // top and bottom of the content region |
| 24 | const calculateHeight = React.useCallback(() => { |
| 25 | // Uncomment to debug |
| 26 | // console.log('Recalculating pane height...') |
| 27 | |
| 28 | let calculatedHeight = '' |
| 29 | |
| 30 | const scrollContainer = getScrollContainer(rootRef.current) |
| 31 | |
| 32 | const topRect = contentTopEntry?.target.getBoundingClientRect() |
| 33 | |
| 34 | // Custom sticky header's height with units |
| 35 | const offsetHeaderWithUnits = typeof offsetHeader === 'number' ? `${offsetHeader}px` : offsetHeader |
| 36 | |
| 37 | if (scrollContainer) { |
| 38 | const scrollRect = scrollContainer.getBoundingClientRect() |
| 39 | const topOffset = topRect ? Math.max(topRect.top - scrollRect.top, 0) : 0 |
| 40 | calculatedHeight = `calc(${scrollRect.height}px - (max(${topOffset}px, ${offsetHeaderWithUnits})))` |
| 41 | } else { |
| 42 | const topOffset = topRect ? Math.max(topRect.top, 0) : 0 |
| 43 | calculatedHeight = `calc(${dvh(100)} - (max(${topOffset}px, ${offsetHeaderWithUnits})))` |
| 44 | } |
| 45 | |
| 46 | setHeight(calculatedHeight) |
| 47 | }, [contentTopEntry, offsetHeader]) |
| 48 | |
| 49 | // We only want to add scroll and resize listeners if the pane is sticky. |
| 50 | // Since hooks can't be called conditionally, we need to use state to track |
| 51 | // if the pane is sticky. |
| 52 | const [isEnabled, setIsEnabled] = React.useState(false) |
| 53 | |
| 54 | useLayoutEffect(() => { |
| 55 | const scrollContainer = getScrollContainer(rootRef.current) |
| 56 | |
| 57 | if (isEnabled && (contentTopInView || contentBottomInView)) { |
| 58 | calculateHeight() |
| 59 | |
| 60 | // Start listeners if the top or the bottom edge of the content region is visible |
| 61 | |
| 62 | if (scrollContainer) { |
| 63 | // eslint-disable-next-line github/prefer-observers |
| 64 | scrollContainer.addEventListener('scroll', calculateHeight) |
| 65 | } else { |
| 66 | // eslint-disable-next-line github/prefer-observers |
| 67 | window.addEventListener('scroll', calculateHeight) |
| 68 | } |
no test coverage detected