diff --git a/.changeset/silent-rivers-sell.md b/.changeset/silent-rivers-sell.md new file mode 100644 index 00000000000..addb854fb6f --- /dev/null +++ b/.changeset/silent-rivers-sell.md @@ -0,0 +1,5 @@ +--- +'@primer/react': patch +--- + +PageHeader: A11y eng review remediations diff --git a/docs/content/drafts/PageHeader.mdx b/docs/content/drafts/PageHeader.mdx index b16684f384f..6eaef75d158 100644 --- a/docs/content/drafts/PageHeader.mdx +++ b/docs/content/drafts/PageHeader.mdx @@ -121,6 +121,8 @@ import {PageHeader} from '@primer/react/drafts' ### With navigation slot +#### Utilizing a Navigation component + ```jsx live drafts @@ -145,6 +147,28 @@ import {PageHeader} from '@primer/react/drafts' ``` +#### Utilizing a custom navigation + +```jsx live drafts + + + Page Title + + + +
  • + + Item 1 + +
  • +
  • + Item 2 +
  • +
    +
    +
    +``` + ### With ContextArea #### With parent link and actions (only visible on mobile) diff --git a/generated/components.json b/generated/components.json index f05734a9b75..f0e59d2526a 100644 --- a/generated/components.json +++ b/generated/components.json @@ -3018,7 +3018,7 @@ ] }, { - "name": "PageHeader.ContextArea Children", + "name": "PageHeader.ParentLink", "props": [ { "name": "href", @@ -3039,7 +3039,7 @@ ] }, { - "name": "PageHeader.ContextArea Children", + "name": "PageHeader.ContextBar", "props": [ { "name": "hidden", @@ -3054,7 +3054,7 @@ ] }, { - "name": "PageHeader.ContextArea Children", + "name": "PageHeader.ContextAreaActions", "props": [ { "name": "hidden", @@ -3090,7 +3090,7 @@ ] }, { - "name": "PageHeader.TitleArea Children", + "name": "PageHeader.LeadingAction", "props": [ { "name": "hidden", @@ -3105,7 +3105,7 @@ ] }, { - "name": "PageHeader.TitleArea Children", + "name": "PageHeader.LeadingVisual", "props": [ { "name": "hidden", @@ -3120,7 +3120,7 @@ ] }, { - "name": "PageHeader.TitleArea Children", + "name": "PageHeader.Title", "props": [ { "name": "hidden", @@ -3140,7 +3140,7 @@ ] }, { - "name": "PageHeader.TitleArea Children", + "name": "PageHeader.TrailingVisual", "props": [ { "name": "hidden", @@ -3155,7 +3155,7 @@ ] }, { - "name": "PageHeader.TitleArea Children", + "name": "PageHeader.TrailingAction", "props": [ { "name": "hidden", @@ -3170,7 +3170,7 @@ ] }, { - "name": "PageHeader.TitleArea Children", + "name": "PageHeader.Actions", "props": [ { "name": "hidden", @@ -3202,6 +3202,22 @@ { "name": "PageHeader.Navigation", "props": [ + { + "name": "aria-label", + "type": "string", + "description": "The aria-label attribute for the navigaton component when it is rendered as a \"nav\" element." + }, + { + "name": "aria-labelledby", + "type": "string", + "description": "The aria-labelledby attribute for the navigaton component when it is rendered as a \"nav\" element." + }, + { + "name": "as", + "type": "div | nav", + "defaultValue": "\"div\"", + "description": "The HTML element used to render the navigation." + }, { "name": "hidden", "type": "| boolean | { narrow?: boolean regular?: boolean wide?: boolean }", diff --git a/src/PageHeader/PageHeader.docs.json b/src/PageHeader/PageHeader.docs.json index 582ab767934..cfc051f30f2 100644 --- a/src/PageHeader/PageHeader.docs.json +++ b/src/PageHeader/PageHeader.docs.json @@ -44,7 +44,7 @@ ] }, { - "name": "PageHeader.ContextArea Children", + "name": "PageHeader.ParentLink", "props": [ { "name": "href", @@ -65,7 +65,7 @@ ] }, { - "name": "PageHeader.ContextArea Children", + "name": "PageHeader.ContextBar", "props": [ { "name": "hidden", @@ -80,7 +80,7 @@ ] }, { - "name": "PageHeader.ContextArea Children", + "name": "PageHeader.ContextAreaActions", "props": [ { "name": "hidden", @@ -116,7 +116,7 @@ ] }, { - "name": "PageHeader.TitleArea Children", + "name": "PageHeader.LeadingAction", "props": [ { "name": "hidden", @@ -131,7 +131,7 @@ ] }, { - "name": "PageHeader.TitleArea Children", + "name": "PageHeader.LeadingVisual", "props": [ { "name": "hidden", @@ -146,7 +146,7 @@ ] }, { - "name": "PageHeader.TitleArea Children", + "name": "PageHeader.Title", "props": [ { "name": "hidden", @@ -166,7 +166,7 @@ ] }, { - "name": "PageHeader.TitleArea Children", + "name": "PageHeader.TrailingVisual", "props": [ { "name": "hidden", @@ -181,7 +181,7 @@ ] }, { - "name": "PageHeader.TitleArea Children", + "name": "PageHeader.TrailingAction", "props": [ { "name": "hidden", @@ -196,7 +196,7 @@ ] }, { - "name": "PageHeader.TitleArea Children", + "name": "PageHeader.Actions", "props": [ { "name": "hidden", @@ -228,6 +228,22 @@ { "name": "PageHeader.Navigation", "props": [ + { + "name": "aria-label", + "type": "string", + "description": "The aria-label attribute for the navigaton component when it is rendered as a \"nav\" element." + }, + { + "name": "aria-labelledby", + "type": "string", + "description": "The aria-labelledby attribute for the navigaton component when it is rendered as a \"nav\" element." + }, + { + "name": "as", + "type": "div | nav", + "defaultValue": "\"div\"", + "description": "The HTML element used to render the navigation." + }, { "name": "hidden", "type": "| boolean | { narrow?: boolean regular?: boolean wide?: boolean }", @@ -241,4 +257,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/src/PageHeader/PageHeader.test.tsx b/src/PageHeader/PageHeader.test.tsx index d2d00b5a908..f96fb9fd940 100644 --- a/src/PageHeader/PageHeader.test.tsx +++ b/src/PageHeader/PageHeader.test.tsx @@ -178,6 +178,36 @@ describe('PageHeader', () => { expect(getByText('Trailing Action')).toHaveStyle('height: 3rem') // add actions here }) + it('renders "aria-label" prop when Navigation is rendered as "nav" landmark', () => { + const {getByLabelText, getByText} = render( + + + Navigation + + , + ) + expect(getByLabelText('Custom')).toBeInTheDocument() + expect(getByText('Navigation')).toHaveAttribute('aria-label', 'Custom') + }) + it('does not renders "aria-label" prop when Navigation is rendered as "div"', () => { + const {getByText} = render( + + Navigation + , + ) + expect(getByText('Navigation')).not.toHaveAttribute('aria-label') + }) + it('logs a warning when the Navigation component is rendered as "nav" but no "aria-label" or "aria-labelledby" prop is provided', () => { + const consoleSpy = jest.spyOn(global.console, 'warn').mockImplementation() + render( + + Navigation + , + ) + expect(consoleSpy).toHaveBeenCalled() + + consoleSpy.mockRestore() + }) }) checkStoriesForAxeViolations('examples', '../PageHeader/') diff --git a/src/PageHeader/PageHeader.tsx b/src/PageHeader/PageHeader.tsx index 5bf2f304ccb..c876dd1a646 100644 --- a/src/PageHeader/PageHeader.tsx +++ b/src/PageHeader/PageHeader.tsx @@ -101,17 +101,7 @@ type LinkProps = Pick< export type ParentLinkProps = React.PropsWithChildren const ParentLink = React.forwardRef( - ( - { - children, - sx = {}, - href, - 'aria-label': ariaLabel = `Back to ${children}`, - as = 'a', - hidden = hiddenOnRegularAndWide, - }, - ref, - ) => { + ({children, sx = {}, href, 'aria-label': ariaLabel, as = 'a', hidden = hiddenOnRegularAndWide}, ref) => { return ( <> > = ({chil ) } +export type NavigationProps = { + as?: 'nav' | 'div' + 'aria-label'?: React.AriaAttributes['aria-label'] + 'aria-labelledby'?: React.AriaAttributes['aria-labelledby'] +} & ChildrenPropTypes + // PageHeader.Navigation: The local navigation area of the header. Visible on all viewports -const Navigation: React.FC> = ({children, sx = {}, hidden = false}) => { +const Navigation: React.FC> = ({ + children, + sx = {}, + hidden = false, + as, + 'aria-label': ariaLabel, + 'aria-labelledby': ariaLabelledBy, +}) => { + // TODO: use warning utility function when it is merged https://github.com/primer/react/pull/2901/ + if (__DEV__) { + if (as === 'nav' && !ariaLabel && !ariaLabelledBy) { + // eslint-disable-next-line no-console + console.warn( + 'Use `aria-label` or `aria-labelledby` prop to provide an accessible label to the `nav` landmark for assistive technology', + ) + } + } return ( ( { display: 'flex', diff --git a/src/PageHeader/features.stories.tsx b/src/PageHeader/features.stories.tsx index 260945d71c4..f64c4db5c1e 100644 --- a/src/PageHeader/features.stories.tsx +++ b/src/PageHeader/features.stories.tsx @@ -147,6 +147,28 @@ export const WithNavigationSlot = () => ( ) +export const WithCustomNavigation = () => ( + + + + Pull request title + + + +
  • + + Item 1 + +
  • +
  • + Item 2 +
  • +
    +
    +
    +
    +) + export const WithLeadingAndTrailingActions = () => (