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