Skip to content

Commit

Permalink
Add stickyTop prop to PageLayout.Pane (#2232)
Browse files Browse the repository at this point in the history
* Add stickyTop prop to PageLayout.Pane

* update docs

* add changeset

* Update stickyTop story

* Allow stickyTop to accept a string

* Update docs/content/PageLayout.mdx

Co-authored-by: Cole Bemis <colebemis@github.com>

* Update docs/content/PageLayout.mdx

Co-authored-by: Cole Bemis <colebemis@github.com>

* Update prop description on the docs

* Add stickyTop example and prop docs

* Update SplitPageLayout example

* Fix code fence

Co-authored-by: Cole Bemis <colebemis@github.com>
  • Loading branch information
broccolinisoup and colebemis committed Aug 18, 2022
1 parent 041d6d0 commit a0fcce6
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 9 deletions.
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."
/>
<PropsTableRow
name="padding"
type={`| 'none'
Expand Down
38 changes: 38 additions & 0 deletions docs/content/SplitPageLayout.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,38 @@ If you need a more flexible layout component, consider using the [PageLayout](/P
</Box>
```

### With a custom sticky header

```jsx live drafts
<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>
<SplitPageLayout>
<SplitPageLayout.Content>
<Placeholder label="Content" height={320} />
</SplitPageLayout.Content>
<SplitPageLayout.Pane stickyTop={64}>
<Placeholder label="Pane" height={120} />
</SplitPageLayout.Pane>
<SplitPageLayout.Footer>
<Placeholder label="Footer" height={64} />
</SplitPageLayout.Footer>
</SplitPageLayout>
</Box>
```

## Props

### SplitPageLayout
Expand Down Expand Up @@ -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."
/>
<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."
/>
<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
</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))`
}

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

0 comments on commit a0fcce6

Please sign in to comment.