Skip to content

Commit 027e44a

Browse files
authored
feat(PageLayout): support a ref on PageLayout.Pane (#2324)
* feat(PageLayout): support a ref on PageLayout.Pane * docs(PageLayout): add `ref` to PageLayout.Pane
1 parent 1cee019 commit 027e44a

File tree

4 files changed

+125
-97
lines changed

4 files changed

+125
-97
lines changed

.changeset/smart-dolphins-live.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@primer/react': minor
3+
---
4+
5+
Update PageLayout.Pane to support a ref on the element wrapping children

docs/content/PageLayout.mdx

+6-6
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,6 @@ Using `aria-label` along with `PageLayout.Header`, `PageLayout.Content`, or `Pag
227227

228228
Using `aria-labelledby` along with `PageLayout.Header`, `PageLayout.Content`, or `PageLayout.Footer` creates a unique label for each landmark role by using the given `id` to associate the landmark with the content with the corresponding `id`. This is helpful when you have a visible item that visually communicates the type of content which you would like to associate to the landmark itself.
229229

230-
231230
```jsx live
232231
<PageLayout>
233232
<PageLayout.Header aria-labelledby="header-label">
@@ -249,11 +248,11 @@ Using `aria-labelledby` along with `PageLayout.Header`, `PageLayout.Content`, or
249248

250249
The `PageLayout` component uses [landmark roles](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/landmark_role) for `PageLayout.Header`, `PageLayout.Content`, and `PageLayout.Footer` in order to make it easier for screen reader users to navigate between sections of the page.
251250

252-
| Component | Landmark role |
253-
| :-------- | :------------ |
254-
| `PageLayout.Header` | [`banner`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/banner_role) |
255-
| `PageLayout.Content` | [`main`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/main_role) |
256-
| `PageLayout.Footer` | [`contentinfo`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/contentinfo_role) |
251+
| Component | Landmark role |
252+
| :------------------- | :------------------------------------------------------------------------------------------------------ |
253+
| `PageLayout.Header` | [`banner`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/banner_role) |
254+
| `PageLayout.Content` | [`main`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/main_role) |
255+
| `PageLayout.Footer` | [`contentinfo`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/contentinfo_role) |
257256

258257
Each component may be labeled through either `aria-label` or `aria-labelledby` in order to provide a unique label for the landmark. This can be helpful when there are multiple landmarks of the same type on the page.
259258

@@ -524,6 +523,7 @@ On macOS, you can open the VoiceOver rotor by pressing `VO-U`. You can navigate
524523
description="Whether the pane is hidden."
525524
/>
526525
<PropsTableSxRow />
526+
<PropsTableRefRow elementType="div" refType="HTMLDivElement" />
527527
</PropsTable>
528528

529529
### PageLayout.Footer

src/PageLayout/PageLayout.test.tsx

+16
Original file line numberDiff line numberDiff line change
@@ -156,4 +156,20 @@ describe('PageLayout', () => {
156156
expect(screen.getByRole('main')).toHaveAccessibleName('content')
157157
expect(screen.getByRole('contentinfo')).toHaveAccessibleName('footer')
158158
})
159+
160+
describe('PageLayout.Pane', () => {
161+
it('should support a ref on the element wrapping the contents of Pane', () => {
162+
const ref = jest.fn()
163+
render(
164+
<ThemeProvider>
165+
<PageLayout>
166+
<PageLayout.Pane ref={ref}>
167+
<div data-testid="content">Pane</div>
168+
</PageLayout.Pane>
169+
</PageLayout>
170+
</ThemeProvider>
171+
)
172+
expect(ref).toHaveBeenCalledWith(screen.getByTestId('content').parentNode)
173+
})
174+
})
159175
})

src/PageLayout/PageLayout.tsx

+98-91
Original file line numberDiff line numberDiff line change
@@ -403,99 +403,106 @@ const paneWidths = {
403403
large: ['100%', null, '256px', '320px', '336px']
404404
}
405405

406-
const Pane: React.FC<React.PropsWithChildren<PageLayoutPaneProps>> = ({
407-
position: responsivePosition = 'end',
408-
positionWhenNarrow = 'inherit',
409-
width = 'medium',
410-
padding = 'none',
411-
divider: responsiveDivider = 'none',
412-
dividerWhenNarrow = 'inherit',
413-
sticky = false,
414-
offsetHeader = 0,
415-
hidden: responsiveHidden = false,
416-
children,
417-
sx = {}
418-
}) => {
419-
// Combine position and positionWhenNarrow for backwards compatibility
420-
const positionProp =
421-
!isResponsiveValue(responsivePosition) && positionWhenNarrow !== 'inherit'
422-
? {regular: responsivePosition, narrow: positionWhenNarrow}
423-
: responsivePosition
424-
425-
const position = useResponsiveValue(positionProp, 'end')
426-
427-
// Combine divider and dividerWhenNarrow for backwards compatibility
428-
const dividerProp =
429-
!isResponsiveValue(responsiveDivider) && dividerWhenNarrow !== 'inherit'
430-
? {regular: responsiveDivider, narrow: dividerWhenNarrow}
431-
: responsiveDivider
432-
433-
const dividerVariant = useResponsiveValue(dividerProp, 'none')
434-
435-
const isHidden = useResponsiveValue(responsiveHidden, false)
436-
437-
const {rowGap, columnGap, enableStickyPane, disableStickyPane} = React.useContext(PageLayoutContext)
438-
439-
React.useEffect(() => {
440-
if (sticky) {
441-
enableStickyPane?.(offsetHeader)
442-
} else {
443-
disableStickyPane?.()
444-
}
445-
}, [sticky, enableStickyPane, disableStickyPane, offsetHeader])
406+
const Pane = React.forwardRef<HTMLDivElement, React.PropsWithChildren<PageLayoutPaneProps>>(
407+
(
408+
{
409+
position: responsivePosition = 'end',
410+
positionWhenNarrow = 'inherit',
411+
width = 'medium',
412+
padding = 'none',
413+
divider: responsiveDivider = 'none',
414+
dividerWhenNarrow = 'inherit',
415+
sticky = false,
416+
offsetHeader = 0,
417+
hidden: responsiveHidden = false,
418+
children,
419+
sx = {}
420+
},
421+
forwardRef
422+
) => {
423+
// Combine position and positionWhenNarrow for backwards compatibility
424+
const positionProp =
425+
!isResponsiveValue(responsivePosition) && positionWhenNarrow !== 'inherit'
426+
? {regular: responsivePosition, narrow: positionWhenNarrow}
427+
: responsivePosition
428+
429+
const position = useResponsiveValue(positionProp, 'end')
430+
431+
// Combine divider and dividerWhenNarrow for backwards compatibility
432+
const dividerProp =
433+
!isResponsiveValue(responsiveDivider) && dividerWhenNarrow !== 'inherit'
434+
? {regular: responsiveDivider, narrow: dividerWhenNarrow}
435+
: responsiveDivider
436+
437+
const dividerVariant = useResponsiveValue(dividerProp, 'none')
438+
439+
const isHidden = useResponsiveValue(responsiveHidden, false)
440+
441+
const {rowGap, columnGap, enableStickyPane, disableStickyPane} = React.useContext(PageLayoutContext)
442+
443+
React.useEffect(() => {
444+
if (sticky) {
445+
enableStickyPane?.(offsetHeader)
446+
} else {
447+
disableStickyPane?.()
448+
}
449+
}, [sticky, enableStickyPane, disableStickyPane, offsetHeader])
446450

447-
return (
448-
<Box
449-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
450-
sx={(theme: any) =>
451-
merge<BetterSystemStyleObject>(
452-
{
453-
// Narrow viewports
454-
display: isHidden ? 'none' : 'flex',
455-
order: panePositions[position],
456-
width: '100%',
457-
marginX: 0,
458-
...(position === 'end'
459-
? {flexDirection: 'column', marginTop: SPACING_MAP[rowGap]}
460-
: {flexDirection: 'column-reverse', marginBottom: SPACING_MAP[rowGap]}),
461-
462-
// Regular and wide viewports
463-
[`@media screen and (min-width: ${theme.breakpoints[1]})`]: {
464-
width: 'auto',
465-
marginY: '0 !important',
466-
...(sticky
467-
? {
468-
position: 'sticky',
469-
// If offsetHeader has value, it will stick the pane to the position where the sticky top ends
470-
// else top will be 0 as the default value of offsetHeader
471-
top: typeof offsetHeader === 'number' ? `${offsetHeader}px` : offsetHeader,
472-
overflow: 'hidden',
473-
maxHeight: 'var(--sticky-pane-height)'
474-
}
475-
: {}),
451+
return (
452+
<Box
453+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
454+
sx={(theme: any) =>
455+
merge<BetterSystemStyleObject>(
456+
{
457+
// Narrow viewports
458+
display: isHidden ? 'none' : 'flex',
459+
order: panePositions[position],
460+
width: '100%',
461+
marginX: 0,
476462
...(position === 'end'
477-
? {flexDirection: 'row', marginLeft: SPACING_MAP[columnGap]}
478-
: {flexDirection: 'row-reverse', marginRight: SPACING_MAP[columnGap]})
479-
}
480-
},
481-
sx
482-
)
483-
}
484-
>
485-
{/* Show a horizontal divider when viewport is narrow. Otherwise, show a vertical divider. */}
486-
<HorizontalDivider
487-
variant={{narrow: dividerVariant, regular: 'none'}}
488-
sx={{[position === 'end' ? 'marginBottom' : 'marginTop']: SPACING_MAP[rowGap]}}
489-
/>
490-
<VerticalDivider
491-
variant={{narrow: 'none', regular: dividerVariant}}
492-
sx={{[position === 'end' ? 'marginRight' : 'marginLeft']: SPACING_MAP[columnGap]}}
493-
/>
494-
495-
<Box sx={{width: paneWidths[width], padding: SPACING_MAP[padding], overflow: 'auto'}}>{children}</Box>
496-
</Box>
497-
)
498-
}
463+
? {flexDirection: 'column', marginTop: SPACING_MAP[rowGap]}
464+
: {flexDirection: 'column-reverse', marginBottom: SPACING_MAP[rowGap]}),
465+
466+
// Regular and wide viewports
467+
[`@media screen and (min-width: ${theme.breakpoints[1]})`]: {
468+
width: 'auto',
469+
marginY: '0 !important',
470+
...(sticky
471+
? {
472+
position: 'sticky',
473+
// If offsetHeader has value, it will stick the pane to the position where the sticky top ends
474+
// else top will be 0 as the default value of offsetHeader
475+
top: typeof offsetHeader === 'number' ? `${offsetHeader}px` : offsetHeader,
476+
overflow: 'hidden',
477+
maxHeight: 'var(--sticky-pane-height)'
478+
}
479+
: {}),
480+
...(position === 'end'
481+
? {flexDirection: 'row', marginLeft: SPACING_MAP[columnGap]}
482+
: {flexDirection: 'row-reverse', marginRight: SPACING_MAP[columnGap]})
483+
}
484+
},
485+
sx
486+
)
487+
}
488+
>
489+
{/* Show a horizontal divider when viewport is narrow. Otherwise, show a vertical divider. */}
490+
<HorizontalDivider
491+
variant={{narrow: dividerVariant, regular: 'none'}}
492+
sx={{[position === 'end' ? 'marginBottom' : 'marginTop']: SPACING_MAP[rowGap]}}
493+
/>
494+
<VerticalDivider
495+
variant={{narrow: 'none', regular: dividerVariant}}
496+
sx={{[position === 'end' ? 'marginRight' : 'marginLeft']: SPACING_MAP[columnGap]}}
497+
/>
498+
499+
<Box ref={forwardRef} sx={{width: paneWidths[width], padding: SPACING_MAP[padding], overflow: 'auto'}}>
500+
{children}
501+
</Box>
502+
</Box>
503+
)
504+
}
505+
)
499506

500507
Pane.displayName = 'PageLayout.Pane'
501508

0 commit comments

Comments
 (0)