diff --git a/.changeset/fuzzy-windows-divide.md b/.changeset/fuzzy-windows-divide.md new file mode 100644 index 00000000000..90d2a132968 --- /dev/null +++ b/.changeset/fuzzy-windows-divide.md @@ -0,0 +1,5 @@ +--- +'@primer/react': minor +--- + +Add a `stickyTop` prop, the height of a sticky header, to the `PageLayout.Pane` to push the pane down for the sticky header when necessary. diff --git a/docs/content/PageLayout.mdx b/docs/content/PageLayout.mdx index 8867da7c38c..58b5195c262 100644 --- a/docs/content/PageLayout.mdx +++ b/docs/content/PageLayout.mdx @@ -168,6 +168,38 @@ See [storybook](https://primer.style/react/storybook?path=/story/layout-pagelayo ``` +### With a custom sticky header + +```jsx live + + + Custom sticky header + + + + + + + + + + + + + +``` + ## Props ### PageLayout @@ -337,6 +369,12 @@ See [storybook](https://primer.style/react/storybook?path=/story/layout-pagelayo defaultValue="false" description="Whether the pane should stick to the top of the screen while the content scrolls." /> + ``` +### With a custom sticky header + +```jsx live drafts + + + Custom sticky header + + + + + + + + + + + + + +``` + ## Props ### SplitPageLayout @@ -350,6 +382,12 @@ If you need a more flexible layout component, consider using the [PageLayout](/P defaultValue="true" description="Whether the pane should stick to the top of the screen while the content scrolls." /> + ( + // a box to create a sticky top element that will be on the consumer side and outside of the PageLayout component + + + Custom sticky header + + + + + {Array.from({length: args.numParagraphsInContent}).map((_, i) => ( + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam at enim id lorem tempus egestas a non ipsum. + Maecenas imperdiet ante quam, at varius lorem molestie vel. Sed at eros consequat, varius tellus et, + auctor felis. Donec pulvinar lacinia urna nec commodo. Phasellus at imperdiet risus. Donec sit amet massa + purus. Nunc sem lectus, bibendum a sapien nec, tristique tempus felis. Ut porttitor auctor tellus in + imperdiet. Ut blandit tincidunt augue, quis fringilla nunc tincidunt sed. Vestibulum auctor euismod nisi. + Nullam tincidunt est in mi tincidunt dictum. Sed consectetur aliquet velit ut ornare. + + ))} + + + + + {Array.from({length: args.numParagraphsInPane}).map((_, i) => ( + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam at enim id lorem tempus egestas a non ipsum. + Maecenas imperdiet ante quam, at varius lorem molestie vel. Sed at eros consequat, varius tellus et, + auctor felis. Donec pulvinar lacinia urna nec commodo. Phasellus at imperdiet risus. Donec sit amet massa + purus. + + ))} + + + + + + + +) + +CustomStickyHeader.argTypes = { + sticky: { + type: 'boolean', + defaultValue: true + }, + stickyTop: { + type: 'string', + defaultValue: '8rem' + }, + numParagraphsInPane: { + type: 'number', + defaultValue: 10 + }, + numParagraphsInContent: { + type: 'number', + defaultValue: 30 + } +} + export default meta diff --git a/src/PageLayout/PageLayout.tsx b/src/PageLayout/PageLayout.tsx index 0f9c81681b9..c2c2f8855dc 100644 --- a/src/PageLayout/PageLayout.tsx +++ b/src/PageLayout/PageLayout.tsx @@ -22,7 +22,7 @@ const PageLayoutContext = React.createContext<{ padding: keyof typeof SPACING_MAP rowGap: keyof typeof SPACING_MAP columnGap: keyof typeof SPACING_MAP - enableStickyPane?: () => void + enableStickyPane?: (top: number | string) => void disableStickyPane?: () => void contentTopRef?: (node?: Element | null | undefined) => void contentBottomRef?: (node?: Element | null | undefined) => void @@ -361,6 +361,7 @@ export type PageLayoutPaneProps = { */ dividerWhenNarrow?: 'inherit' | 'none' | 'line' | 'filled' sticky?: boolean + stickyTop?: string | number hidden?: boolean | ResponsiveValue } & SxProp @@ -383,6 +384,7 @@ const Pane: React.FC> = ({ divider: responsiveDivider = 'none', dividerWhenNarrow = 'inherit', sticky = false, + stickyTop = 0, hidden: responsiveHidden = false, children, sx = {} @@ -409,11 +411,11 @@ const Pane: React.FC> = ({ React.useEffect(() => { if (sticky) { - enableStickyPane?.() + enableStickyPane?.(stickyTop) } else { disableStickyPane?.() } - }, [sticky, enableStickyPane, disableStickyPane]) + }, [sticky, enableStickyPane, disableStickyPane, stickyTop]) return ( > = ({ ...(sticky ? { position: 'sticky', - top: 0, + // If stickyTop has value, it will stick the pane to the position where the sticky top ends + // else top will be 0 as the default value of stickyTop + top: stickyTop, overflow: 'hidden', maxHeight: 'var(--sticky-pane-height)' } diff --git a/src/PageLayout/useStickyPaneHeight.ts b/src/PageLayout/useStickyPaneHeight.ts index 2b5bb7e8430..40d50b4f6f7 100644 --- a/src/PageLayout/useStickyPaneHeight.ts +++ b/src/PageLayout/useStickyPaneHeight.ts @@ -5,12 +5,12 @@ import {useInView} from 'react-intersection-observer' * Calculates the height of the sticky pane such that it always * fits into the viewport even when the header or footer are visible. */ -// TODO: Handle sticky header export function useStickyPaneHeight() { const rootRef = React.useRef(null) // Default the height to the viewport height const [height, setHeight] = React.useState('100vh') + const [stickyTop, setStickyTop] = React.useState(0) // Create intersection observers to track the top and bottom of the content region const [contentTopRef, contentTopInView, contentTopEntry] = useInView() @@ -44,11 +44,13 @@ export function useStickyPaneHeight() { // We need to account for this when calculating the offset. const overflowScroll = Math.max(window.scrollY + window.innerHeight - document.body.scrollHeight, 0) - calculatedHeight = `calc(100vh - ${topOffset + bottomOffset - overflowScroll}px)` + const stickyTopWithUnits = typeof stickyTop === 'number' ? `${stickyTop}px` : stickyTop + + calculatedHeight = `calc(100vh - (max(${topOffset}px, ${stickyTopWithUnits}) + ${bottomOffset}px - ${overflowScroll}px))` } setHeight(calculatedHeight) - }, [contentTopEntry, contentBottomEntry]) + }, [contentTopEntry, contentBottomEntry, stickyTop]) // We only want to add scroll and resize listeners if the pane is sticky. // Since hooks can't be called conditionally, we need to use state to track @@ -88,10 +90,19 @@ export function useStickyPaneHeight() { } }, [isEnabled, contentTopInView, contentBottomInView, calculateHeight]) + function enableStickyPane(top: string | number) { + setIsEnabled(true) + setStickyTop(top) + } + + function disableStickyPane() { + setIsEnabled(false) + } + return { rootRef, - enableStickyPane: () => setIsEnabled(true), - disableStickyPane: () => setIsEnabled(false), + enableStickyPane, + disableStickyPane, contentTopRef, contentBottomRef, stickyPaneHeight: height