Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fuzzy-windows-divide.md
Original file line number Diff line number Diff line change
@@ -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.
31 changes: 31 additions & 0 deletions docs/content/PageLayout.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,36 @@ See [storybook](https://primer.style/react/storybook?path=/story/layout-pagelayo
</Box>
```

### With a stickyTop sticky pane

```jsx live
<Box sx={{height: 320, overflowY: 'auto', border: '1px solid', borderColor: 'border.default'}}>
<Box
height="64px"
width="100%"
borderBottom="1px solid"
borderColor="border.default"
sx={{position: 'sticky', top: '0', backgroundColor: 'canvas.subtle'}}
display="flex"
alignItems="center"
justifyContent="center"
>
<Heading>Sticky top content</Heading>
</Box>
<PageLayout>
<PageLayout.Content>
<Placeholder label="Content" height={320} />
</PageLayout.Content>
<PageLayout.Pane position="start" stickyTop={64} sticky>
<Placeholder label="Pane" height={120} />
</PageLayout.Pane>
<PageLayout.Footer>
<Placeholder label="Footer" height={64} />
</PageLayout.Footer>
</PageLayout>
</Box>
```

## Props

### PageLayout
Expand Down Expand Up @@ -337,6 +367,7 @@ 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."
/>
<PropsTableRow name="stickyTop" type="number" defaultValue="0" description="The height of the sticky top element" />
<PropsTableRow
name="padding"
type={`| 'none'
Expand Down
68 changes: 68 additions & 0 deletions src/PageLayout/PageLayout.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -659,4 +659,72 @@ NestedScrollContainer.argTypes = {
}
}

export const StickyPaneWithStickyTop: Story = args => (
// a box to create a sticky top element that will be on the consumer side and outside of the PageLayout component
<Box>
<Box
height={args.stickyTop}
width="100%"
border="1px solid"
borderColor="border.default"
sx={{position: 'sticky', top: '0', backgroundColor: 'canvas.subtle'}}
display="flex"
alignItems="center"
justifyContent="center"
>
<Heading>Sticky top content</Heading>
</Box>
<PageLayout rowGap="none" columnGap="none" padding="none" containerWidth="full">
<PageLayout.Content padding="normal" width="large">
<Box sx={{display: 'grid', gap: 3}}>
{Array.from({length: args.numParagraphsInContent}).map((_, i) => (
<Box key={i} as="p" sx={{margin: 0}}>
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.
</Box>
))}
</Box>
</PageLayout.Content>
<PageLayout.Pane position="start" padding="normal" divider="line" stickyTop={100} sticky>
<Box sx={{display: 'grid', gap: 3}}>
{Array.from({length: args.numParagraphsInPane}).map((_, i) => (
<Box key={i} as="p" sx={{margin: 0}}>
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.
</Box>
))}
</Box>
</PageLayout.Pane>
<PageLayout.Footer padding="normal" divider="line">
<Placeholder label="Footer" height={64} />
</PageLayout.Footer>
</PageLayout>
</Box>
)

StickyPaneWithStickyTop.argTypes = {
sticky: {
type: 'boolean',
defaultValue: true
},
stickyTop: {
type: 'number',
defaultValue: 100
},
numParagraphsInPane: {
type: 'number',
defaultValue: 10
},
numParagraphsInContent: {
type: 'number',
defaultValue: 30
}
}

export default meta
12 changes: 8 additions & 4 deletions src/PageLayout/PageLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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?: (stickyTopHeight: number) => void
disableStickyPane?: () => void
contentTopRef?: (node?: Element | null | undefined) => void
contentBottomRef?: (node?: Element | null | undefined) => void
Expand Down Expand Up @@ -362,6 +362,7 @@ export type PageLayoutPaneProps = {
*/
dividerWhenNarrow?: 'inherit' | 'none' | 'line' | 'filled'
sticky?: boolean
stickyTop?: number // the height of the sticky top element
hidden?: boolean | ResponsiveValue<boolean>
} & SxProp

Expand All @@ -384,6 +385,7 @@ const Pane: React.FC<React.PropsWithChildren<PageLayoutPaneProps>> = ({
divider: responsiveDivider = 'none',
dividerWhenNarrow = 'inherit',
sticky = false,
stickyTop = 0,
hidden: responsiveHidden = false,
children,
sx = {}
Expand All @@ -410,11 +412,11 @@ const Pane: React.FC<React.PropsWithChildren<PageLayoutPaneProps>> = ({

React.useEffect(() => {
if (sticky) {
enableStickyPane?.()
enableStickyPane?.(stickyTop)
} else {
disableStickyPane?.()
}
}, [sticky, enableStickyPane, disableStickyPane])
}, [sticky, enableStickyPane, disableStickyPane, stickyTop])

return (
<Box
Expand All @@ -439,7 +441,9 @@ const Pane: React.FC<React.PropsWithChildren<PageLayoutPaneProps>> = ({
...(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)'
}
Expand Down
17 changes: 13 additions & 4 deletions src/PageLayout/useStickyPaneHeight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export function useStickyPaneHeight() {

// Default the height to the viewport height
const [height, setHeight] = React.useState('100vh')
// stickyTop state
const [stickyTopHeight, setStickyTopHeight] = React.useState(0)

// Create intersection observers to track the top and bottom of the content region
const [contentTopRef, contentTopInView, contentTopEntry] = useInView()
Expand Down Expand Up @@ -44,11 +46,11 @@ 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)`
calculatedHeight = `calc(100vh - ${Math.max(topOffset, stickyTopHeight) + bottomOffset - overflowScroll}px)`
}

setHeight(calculatedHeight)
}, [contentTopEntry, contentBottomEntry])
}, [contentTopEntry, contentBottomEntry, stickyTopHeight])

// 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
Expand Down Expand Up @@ -88,10 +90,17 @@ export function useStickyPaneHeight() {
}
}, [isEnabled, contentTopInView, contentBottomInView, calculateHeight])

const enableStickyPane = (stickyTop: number) => {
setIsEnabled(true)
setStickyTopHeight(stickyTop)
}

const disableStickyPane = () => setIsEnabled(false)

return {
rootRef,
enableStickyPane: () => setIsEnabled(true),
disableStickyPane: () => setIsEnabled(false),
enableStickyPane,
disableStickyPane,
contentTopRef,
contentBottomRef,
stickyPaneHeight: height
Expand Down