Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add stickyTop prop to PageLayout.Pane #2232

Merged
merged 13 commits into from
Aug 18, 2022
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.
38 changes: 38 additions & 0 deletions docs/content/PageLayout.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,38 @@ See [storybook](https://primer.style/react/storybook?path=/story/layout-pagelayo
</Box>
```

### With a custom sticky header

```jsx live
<Box sx={{height: 320, overflowY: 'auto', border: '1px solid', borderColor: 'border.default'}}>
<Box
sx={{
position: 'sticky',
top: 0,
height: 64,
display: 'grid',
placeItems: 'center',
backgroundColor: 'canvas.subtle',
borderBottom: '1px solid',
borderColor: 'border.default'
}}
>
Custom sticky header
</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 +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."
/>
<PropsTableRow
name="stickyTop"
type="number | string"
defaultValue="0"
description="Use stickyTop to push the sticky pane down to make room for a sticky header if necessary. Use the type `string` to specify the height with a unit i.e. 5rem; otherwise the type `number` will be taken as px."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you copy this prop documentation and With custom sticky header example into the SplitPageLayout.mdx page as well?

/>
<PropsTableRow
name="padding"
type={`| 'none'
Expand Down
70 changes: 70 additions & 0 deletions src/PageLayout/PageLayout.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -587,4 +587,74 @@ NestedScrollContainer.argTypes = {
}
}

export const CustomStickyHeader: 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
sx={{
position: 'sticky',
top: 0,
height: args.stickyTop,
display: 'grid',
placeItems: 'center',
backgroundColor: 'canvas.subtle',
borderBottom: '1px solid',
borderColor: 'border.default'
}}
>
Custom sticky header
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah this is very explanatory. Thanks for updating it!

</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" sticky stickyTop={args.stickyTop}>
<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>
)

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
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?: (top: number | string) => void
disableStickyPane?: () => void
contentTopRef?: (node?: Element | null | undefined) => void
contentBottomRef?: (node?: Element | null | undefined) => void
Expand Down Expand Up @@ -361,6 +361,7 @@ export type PageLayoutPaneProps = {
*/
dividerWhenNarrow?: 'inherit' | 'none' | 'line' | 'filled'
sticky?: boolean
stickyTop?: string | number
hidden?: boolean | ResponsiveValue<boolean>
} & SxProp

Expand All @@ -383,6 +384,7 @@ const Pane: React.FC<React.PropsWithChildren<PageLayoutPaneProps>> = ({
divider: responsiveDivider = 'none',
dividerWhenNarrow = 'inherit',
sticky = false,
stickyTop = 0,
hidden: responsiveHidden = false,
children,
sx = {}
Expand All @@ -409,11 +411,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 @@ -438,7 +440,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
21 changes: 16 additions & 5 deletions src/PageLayout/useStickyPaneHeight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLDivElement>(null)

// Default the height to the viewport height
const [height, setHeight] = React.useState('100vh')
const [stickyTop, setStickyTop] = React.useState<number | string>(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 +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))`
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love that! 🙌🏼

}

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
Expand Down Expand Up @@ -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
Expand Down