diff --git a/.changeset/smart-dolphins-live.md b/.changeset/smart-dolphins-live.md new file mode 100644 index 00000000000..939d2012af0 --- /dev/null +++ b/.changeset/smart-dolphins-live.md @@ -0,0 +1,5 @@ +--- +'@primer/react': minor +--- + +Update PageLayout.Pane to support a ref on the element wrapping children diff --git a/docs/content/PageLayout.mdx b/docs/content/PageLayout.mdx index 53958e9615c..f556a6f34ba 100644 --- a/docs/content/PageLayout.mdx +++ b/docs/content/PageLayout.mdx @@ -227,7 +227,6 @@ Using `aria-label` along with `PageLayout.Header`, `PageLayout.Content`, or `Pag 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. - ```jsx live @@ -249,11 +248,11 @@ Using `aria-labelledby` along with `PageLayout.Header`, `PageLayout.Content`, or 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. -| Component | Landmark role | -| :-------- | :------------ | -| `PageLayout.Header` | [`banner`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/banner_role) | -| `PageLayout.Content` | [`main`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/main_role) | -| `PageLayout.Footer` | [`contentinfo`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/contentinfo_role) | +| Component | Landmark role | +| :------------------- | :------------------------------------------------------------------------------------------------------ | +| `PageLayout.Header` | [`banner`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/banner_role) | +| `PageLayout.Content` | [`main`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/main_role) | +| `PageLayout.Footer` | [`contentinfo`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/contentinfo_role) | 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. @@ -524,6 +523,7 @@ On macOS, you can open the VoiceOver rotor by pressing `VO-U`. You can navigate description="Whether the pane is hidden." /> + ### PageLayout.Footer diff --git a/src/PageLayout/PageLayout.test.tsx b/src/PageLayout/PageLayout.test.tsx index d4ae9f8380b..905e152f9b2 100644 --- a/src/PageLayout/PageLayout.test.tsx +++ b/src/PageLayout/PageLayout.test.tsx @@ -156,4 +156,20 @@ describe('PageLayout', () => { expect(screen.getByRole('main')).toHaveAccessibleName('content') expect(screen.getByRole('contentinfo')).toHaveAccessibleName('footer') }) + + describe('PageLayout.Pane', () => { + it('should support a ref on the element wrapping the contents of Pane', () => { + const ref = jest.fn() + render( + + + +
Pane
+
+
+
+ ) + expect(ref).toHaveBeenCalledWith(screen.getByTestId('content').parentNode) + }) + }) }) diff --git a/src/PageLayout/PageLayout.tsx b/src/PageLayout/PageLayout.tsx index c03ec999505..7f0ca096576 100644 --- a/src/PageLayout/PageLayout.tsx +++ b/src/PageLayout/PageLayout.tsx @@ -403,99 +403,106 @@ const paneWidths = { large: ['100%', null, '256px', '320px', '336px'] } -const Pane: React.FC> = ({ - position: responsivePosition = 'end', - positionWhenNarrow = 'inherit', - width = 'medium', - padding = 'none', - divider: responsiveDivider = 'none', - dividerWhenNarrow = 'inherit', - sticky = false, - offsetHeader = 0, - hidden: responsiveHidden = false, - children, - sx = {} -}) => { - // Combine position and positionWhenNarrow for backwards compatibility - const positionProp = - !isResponsiveValue(responsivePosition) && positionWhenNarrow !== 'inherit' - ? {regular: responsivePosition, narrow: positionWhenNarrow} - : responsivePosition - - const position = useResponsiveValue(positionProp, 'end') - - // Combine divider and dividerWhenNarrow for backwards compatibility - const dividerProp = - !isResponsiveValue(responsiveDivider) && dividerWhenNarrow !== 'inherit' - ? {regular: responsiveDivider, narrow: dividerWhenNarrow} - : responsiveDivider - - const dividerVariant = useResponsiveValue(dividerProp, 'none') - - const isHidden = useResponsiveValue(responsiveHidden, false) - - const {rowGap, columnGap, enableStickyPane, disableStickyPane} = React.useContext(PageLayoutContext) - - React.useEffect(() => { - if (sticky) { - enableStickyPane?.(offsetHeader) - } else { - disableStickyPane?.() - } - }, [sticky, enableStickyPane, disableStickyPane, offsetHeader]) +const Pane = React.forwardRef>( + ( + { + position: responsivePosition = 'end', + positionWhenNarrow = 'inherit', + width = 'medium', + padding = 'none', + divider: responsiveDivider = 'none', + dividerWhenNarrow = 'inherit', + sticky = false, + offsetHeader = 0, + hidden: responsiveHidden = false, + children, + sx = {} + }, + forwardRef + ) => { + // Combine position and positionWhenNarrow for backwards compatibility + const positionProp = + !isResponsiveValue(responsivePosition) && positionWhenNarrow !== 'inherit' + ? {regular: responsivePosition, narrow: positionWhenNarrow} + : responsivePosition + + const position = useResponsiveValue(positionProp, 'end') + + // Combine divider and dividerWhenNarrow for backwards compatibility + const dividerProp = + !isResponsiveValue(responsiveDivider) && dividerWhenNarrow !== 'inherit' + ? {regular: responsiveDivider, narrow: dividerWhenNarrow} + : responsiveDivider + + const dividerVariant = useResponsiveValue(dividerProp, 'none') + + const isHidden = useResponsiveValue(responsiveHidden, false) + + const {rowGap, columnGap, enableStickyPane, disableStickyPane} = React.useContext(PageLayoutContext) + + React.useEffect(() => { + if (sticky) { + enableStickyPane?.(offsetHeader) + } else { + disableStickyPane?.() + } + }, [sticky, enableStickyPane, disableStickyPane, offsetHeader]) - return ( - - merge( - { - // Narrow viewports - display: isHidden ? 'none' : 'flex', - order: panePositions[position], - width: '100%', - marginX: 0, - ...(position === 'end' - ? {flexDirection: 'column', marginTop: SPACING_MAP[rowGap]} - : {flexDirection: 'column-reverse', marginBottom: SPACING_MAP[rowGap]}), - - // Regular and wide viewports - [`@media screen and (min-width: ${theme.breakpoints[1]})`]: { - width: 'auto', - marginY: '0 !important', - ...(sticky - ? { - position: 'sticky', - // If offsetHeader 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 offsetHeader - top: typeof offsetHeader === 'number' ? `${offsetHeader}px` : offsetHeader, - overflow: 'hidden', - maxHeight: 'var(--sticky-pane-height)' - } - : {}), + return ( + + merge( + { + // Narrow viewports + display: isHidden ? 'none' : 'flex', + order: panePositions[position], + width: '100%', + marginX: 0, ...(position === 'end' - ? {flexDirection: 'row', marginLeft: SPACING_MAP[columnGap]} - : {flexDirection: 'row-reverse', marginRight: SPACING_MAP[columnGap]}) - } - }, - sx - ) - } - > - {/* Show a horizontal divider when viewport is narrow. Otherwise, show a vertical divider. */} - - - - {children} - - ) -} + ? {flexDirection: 'column', marginTop: SPACING_MAP[rowGap]} + : {flexDirection: 'column-reverse', marginBottom: SPACING_MAP[rowGap]}), + + // Regular and wide viewports + [`@media screen and (min-width: ${theme.breakpoints[1]})`]: { + width: 'auto', + marginY: '0 !important', + ...(sticky + ? { + position: 'sticky', + // If offsetHeader 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 offsetHeader + top: typeof offsetHeader === 'number' ? `${offsetHeader}px` : offsetHeader, + overflow: 'hidden', + maxHeight: 'var(--sticky-pane-height)' + } + : {}), + ...(position === 'end' + ? {flexDirection: 'row', marginLeft: SPACING_MAP[columnGap]} + : {flexDirection: 'row-reverse', marginRight: SPACING_MAP[columnGap]}) + } + }, + sx + ) + } + > + {/* Show a horizontal divider when viewport is narrow. Otherwise, show a vertical divider. */} + + + + + {children} + + + ) + } +) Pane.displayName = 'PageLayout.Pane'