Skip to content
5 changes: 5 additions & 0 deletions .changeset/pretty-coats-sell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@primer/react": minor
---

Add data-component attributes and associated tests for PageHeader, PageLayout, Pagehead, Popover, Portal, and ProgressBar
44 changes: 22 additions & 22 deletions packages/react/src/PageHeader/PageHeader.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -35,23 +35,23 @@
We don't want these values to be overridden but still want to allow consumers to override them if needed.
*/
/* stylelint-disable selector-pseudo-class-disallowed-list -- :has() scoped to CSS Module, audited (github/github-ui#17224) */
&:has([data-component='TitleArea'][data-size-variant='large']) {
&:has([data-component='PageHeader.TitleArea'][data-size-variant='large']) {
font-size: var(--custom-font-size, var(--text-title-size-large, 2rem));
font-weight: var(--custom-font-weight, var(--base-text-weight-normal, 400));
line-height: var(--custom-line-height, var(--text-title-lineHeight-large, 1.5)); /* calc(48/32) */

--title-line-height: var(--custom-line-height, var(--text-title-lineHeight-large, 1.5));
}

&:has([data-component='TitleArea'][data-size-variant='medium']) {
&:has([data-component='PageHeader.TitleArea'][data-size-variant='medium']) {
font-size: var(--custom-font-size, var(--text-title-size-medium, 1.25rem));
font-weight: var(--custom-font-weight, var(--base-text-weight-semibold, 600));
line-height: var(--custom-line-height, var(--text-title-lineHeight-medium, 1.6)); /* calc(32/20) */

--title-line-height: var(--custom-line-height, var(--text-title-lineHeight-medium, 1.6));
}

&:has([data-component='TitleArea'][data-size-variant='subtitle']) {
&:has([data-component='PageHeader.TitleArea'][data-size-variant='subtitle']) {
font-size: var(--custom-font-size, var(--text-title-size-medium, 1.25rem));
font-weight: var(--custom-font-weight, var(--base-text-weight-normal, 400));
line-height: var(--custom-line-height, var(--text-title-lineHeight-medium, 1.6)); /* calc(32/20) */
Expand All @@ -61,23 +61,23 @@

/* Responsive size variants */
@media (--viewportRange-narrow) {
&:has([data-component='TitleArea'][data-size-variant-narrow='large']) {
&:has([data-component='PageHeader.TitleArea'][data-size-variant-narrow='large']) {
font-size: var(--custom-font-size, var(--text-title-size-large, 2rem));
font-weight: var(--custom-font-weight, var(--base-text-weight-normal, 400));
line-height: var(--custom-line-height, var(--text-title-lineHeight-large, 1.5));

--title-line-height: var(--custom-line-height, var(--text-title-lineHeight-large, 1.5));
}

&:has([data-component='TitleArea'][data-size-variant-narrow='medium']) {
&:has([data-component='PageHeader.TitleArea'][data-size-variant-narrow='medium']) {
font-size: var(--custom-font-size, var(--text-title-size-medium, 1.25rem));
font-weight: var(--custom-font-weight, var(--base-text-weight-semibold, 600));
line-height: var(--custom-line-height, var(--text-title-lineHeight-medium, 1.6));

--title-line-height: var(--custom-line-height, var(--text-title-lineHeight-medium, 1.6));
}

&:has([data-component='TitleArea'][data-size-variant-narrow='subtitle']) {
&:has([data-component='PageHeader.TitleArea'][data-size-variant-narrow='subtitle']) {
font-size: var(--custom-font-size, var(--text-title-size-medium, 1.25rem));
font-weight: var(--custom-font-weight, var(--base-text-weight-normal, 400));
line-height: var(--custom-line-height, var(--text-title-lineHeight-medium, 1.6));
Expand All @@ -87,23 +87,23 @@
}

@media (--viewportRange-regular) {
&:has([data-component='TitleArea'][data-size-variant-regular='large']) {
&:has([data-component='PageHeader.TitleArea'][data-size-variant-regular='large']) {
font-size: var(--custom-font-size, var(--text-title-size-large, 2rem));
font-weight: var(--custom-font-weight, var(--base-text-weight-normal, 400));
line-height: var(--custom-line-height, var(--text-title-lineHeight-large, 1.5));

--title-line-height: var(--custom-line-height, var(--text-title-lineHeight-large, 1.5));
}

&:has([data-component='TitleArea'][data-size-variant-regular='medium']) {
&:has([data-component='PageHeader.TitleArea'][data-size-variant-regular='medium']) {
font-size: var(--custom-font-size, var(--text-title-size-medium, 1.25rem));
font-weight: var(--custom-font-weight, var(--base-text-weight-semibold, 600));
line-height: var(--custom-line-height, var(--text-title-lineHeight-medium, 1.6));

--title-line-height: var(--custom-line-height, var(--text-title-lineHeight-medium, 1.6));
}

&:has([data-component='TitleArea'][data-size-variant-regular='subtitle']) {
&:has([data-component='PageHeader.TitleArea'][data-size-variant-regular='subtitle']) {
font-size: var(--custom-font-size, var(--text-title-size-medium, 1.25rem));
font-weight: var(--custom-font-weight, var(--base-text-weight-normal, 400));
line-height: var(--custom-line-height, var(--text-title-lineHeight-medium, 1.6));
Expand All @@ -113,23 +113,23 @@
}

@media (--viewportRange-wide) {
&:has([data-component='TitleArea'][data-size-variant-wide='large']) {
&:has([data-component='PageHeader.TitleArea'][data-size-variant-wide='large']) {
font-size: var(--custom-font-size, var(--text-title-size-large, 2rem));
font-weight: var(--custom-font-weight, var(--base-text-weight-normal, 400));
line-height: var(--custom-line-height, var(--text-title-lineHeight-large, 1.5));

--title-line-height: var(--custom-line-height, var(--text-title-lineHeight-large, 1.5));
}

&:has([data-component='TitleArea'][data-size-variant-wide='medium']) {
&:has([data-component='PageHeader.TitleArea'][data-size-variant-wide='medium']) {
font-size: var(--custom-font-size, var(--text-title-size-medium, 1.25rem));
font-weight: var(--custom-font-weight, var(--base-text-weight-semibold, 600));
line-height: var(--custom-line-height, var(--text-title-lineHeight-medium, 1.6));

--title-line-height: var(--custom-line-height, var(--text-title-lineHeight-medium, 1.6));
}

&:has([data-component='TitleArea'][data-size-variant-wide='subtitle']) {
&:has([data-component='PageHeader.TitleArea'][data-size-variant-wide='subtitle']) {
font-size: var(--custom-font-size, var(--text-title-size-medium, 1.25rem));
font-weight: var(--custom-font-weight, var(--base-text-weight-normal, 400));
line-height: var(--custom-line-height, var(--text-title-lineHeight-medium, 1.6));
Expand All @@ -138,39 +138,39 @@
}
}

&[data-has-border='true']:has([data-component='PH_Navigation'][data-hidden-all]),
&[data-has-border='true']:not(:has([data-component='PH_Navigation'])) {
&[data-has-border='true']:has([data-component='PageHeader.Navigation'][data-hidden-all]),
&[data-has-border='true']:not(:has([data-component='PageHeader.Navigation'])) {
border-block-end: var(--borderWidth-thin) solid var(--borderColor-default);
padding-block-end: var(--base-size-8);
}

@media (--viewportRange-narrow) {
&[data-has-border='true']:has([data-component='PH_Navigation'][data-hidden-narrow]) {
&[data-has-border='true']:has([data-component='PageHeader.Navigation'][data-hidden-narrow]) {
border-block-end: var(--borderWidth-thin) solid var(--borderColor-default);
padding-block-end: var(--base-size-8);
}
}

@media (--viewportRange-regular) {
&[data-has-border='true']:has([data-component='PH_Navigation'][data-hidden-regular]) {
&[data-has-border='true']:has([data-component='PageHeader.Navigation'][data-hidden-regular]) {
border-block-end: var(--borderWidth-thin) solid var(--borderColor-default);
padding-block-end: var(--base-size-8);
}
}

@media (--viewportRange-wide) {
&[data-has-border='true']:has([data-component='PH_Navigation'][data-hidden-wide]) {
&[data-has-border='true']:has([data-component='PageHeader.Navigation'][data-hidden-wide]) {
border-block-end: var(--borderWidth-thin) solid var(--borderColor-default);
padding-block-end: var(--base-size-8);
}
}
/* stylelint-enable selector-pseudo-class-disallowed-list */

& [data-component='PH_LeadingAction'],
& [data-component='PH_TrailingAction'],
& [data-component='PH_Actions'],
& [data-component='PH_LeadingVisual'],
& [data-component='PH_TrailingVisual'] {
& [data-component='PageHeader.LeadingAction'],
& [data-component='PageHeader.TrailingAction'],
& [data-component='PageHeader.Actions'],
& [data-component='PageHeader.LeadingVisual'],
& [data-component='PageHeader.TrailingVisual'] {
height: calc(var(--title-line-height) * 1em);
}

Expand Down
45 changes: 45 additions & 0 deletions packages/react/src/PageHeader/PageHeader.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,45 @@ describe('PageHeader', () => {
implementsClassName(PageHeader.Actions, classes.Actions)
implementsClassName(PageHeader.Description, classes.Description)
implementsClassName(PageHeader.Navigation, classes.Navigation)

it('renders data-component attributes for PageHeader and exported subcomponents', () => {
const {container} = render(
<PageHeader aria-label="Title" role="banner">
<PageHeader.ContextArea>ContextArea</PageHeader.ContextArea>
<PageHeader.ParentLink href="#">ParentLink</PageHeader.ParentLink>
<PageHeader.ContextBar>ContextBar</PageHeader.ContextBar>
<PageHeader.TitleArea>
<PageHeader.LeadingAction>LeadingAction</PageHeader.LeadingAction>
<PageHeader.Breadcrumbs>Breadcrumbs</PageHeader.Breadcrumbs>
<PageHeader.LeadingVisual>LeadingVisual</PageHeader.LeadingVisual>
<PageHeader.Title>Title</PageHeader.Title>
<PageHeader.TrailingVisual>TrailingVisual</PageHeader.TrailingVisual>
<PageHeader.TrailingAction>TrailingAction</PageHeader.TrailingAction>
<PageHeader.Actions>Actions</PageHeader.Actions>
</PageHeader.TitleArea>
<PageHeader.ContextAreaActions>ContextAreaActions</PageHeader.ContextAreaActions>
<PageHeader.Description>Description</PageHeader.Description>
<PageHeader.Navigation>Navigation</PageHeader.Navigation>
</PageHeader>,
)

expect(container.firstChild).toHaveAttribute('data-component', 'PageHeader')
expect(container.querySelector('[data-component="PageHeader.ContextArea"]')).toBeInTheDocument()
expect(container.querySelector('[data-component="PageHeader.ParentLink"]')).toBeInTheDocument()
expect(container.querySelector('[data-component="PageHeader.ContextBar"]')).toBeInTheDocument()
expect(container.querySelector('[data-component="PageHeader.TitleArea"]')).toBeInTheDocument()
expect(container.querySelector('[data-component="PageHeader.ContextAreaActions"]')).toBeInTheDocument()
expect(container.querySelector('[data-component="PageHeader.LeadingAction"]')).toBeInTheDocument()
expect(container.querySelector('[data-component="PageHeader.Breadcrumbs"]')).toBeInTheDocument()
expect(container.querySelector('[data-component="PageHeader.LeadingVisual"]')).toBeInTheDocument()
expect(container.querySelector('[data-component="PageHeader.Title"]')).toBeInTheDocument()
expect(container.querySelector('[data-component="PageHeader.TrailingVisual"]')).toBeInTheDocument()
expect(container.querySelector('[data-component="PageHeader.TrailingAction"]')).toBeInTheDocument()
expect(container.querySelector('[data-component="PageHeader.Actions"]')).toBeInTheDocument()
expect(container.querySelector('[data-component="PageHeader.Description"]')).toBeInTheDocument()
expect(container.querySelector('[data-component="PageHeader.Navigation"]')).toBeInTheDocument()
})

it('respects the title variant prop', () => {
const {getByText} = render(
<PageHeader role="banner" aria-label="Title">
Expand All @@ -31,6 +70,7 @@ describe('PageHeader', () => {
)
expect(getByText('Title')).toHaveStyle('font-size: 32px')
})

it('renders "aria-label" prop when Navigation is rendered as "nav" landmark', () => {
const {getByLabelText, getByText} = render(
<PageHeader role="banner" aria-label="Title">
Expand All @@ -45,6 +85,7 @@ describe('PageHeader', () => {
expect(getByLabelText('Custom')).toBeInTheDocument()
expect(getByText('Navigation')).toHaveAttribute('aria-label', 'Custom')
})

it('does not render "aria-label" prop when Navigation is rendered as "div"', () => {
const {getByText} = render(
<PageHeader role="banner" aria-label="Title">
Expand All @@ -71,6 +112,7 @@ describe('PageHeader', () => {

consoleSpy.mockRestore()
})

it('does not render "role" attribute when not explicitly specified', () => {
const {container} = render(
<PageHeader>
Expand All @@ -81,6 +123,7 @@ describe('PageHeader', () => {
)
expect(container.firstChild).not.toHaveAttribute('role')
})

it('renders "role" attribute when explicitly specified', () => {
const {container} = render(
<PageHeader role="banner">
Expand All @@ -91,6 +134,7 @@ describe('PageHeader', () => {
)
expect(container.firstChild).toHaveAttribute('role', 'banner')
})

it('does not render "aria-label" attribute when not explicitly specified', () => {
const {container} = render(
<PageHeader role="banner">
Expand All @@ -101,6 +145,7 @@ describe('PageHeader', () => {
)
expect(container.firstChild).not.toHaveAttribute('aria-label')
})

it('renders custom "aria-label" attribute when explicitly specified', () => {
const {container} = render(
<PageHeader aria-label="Custom aria-label" role="banner">
Expand Down
Loading
Loading