Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tiny-hats-applaud.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@primer/react": minor
---

Add `data-component` attributes for Details, Flash, FormControl (+ update InputValidation to forward from FormControl.Validation), Header, and Heading.
4 changes: 2 additions & 2 deletions packages/react/src/Details/Details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const Root = React.forwardRef<HTMLDetailsElement, DetailsProps>(
}, [])

return (
<details className={clsx(className, classes.Details)} {...rest} ref={ref}>
<details className={clsx(className, classes.Details)} {...rest} ref={ref} data-component="Details">
{children}
</details>
)
Expand All @@ -47,7 +47,7 @@ export type SummaryProps<As extends React.ElementType> = {
function Summary<As extends React.ElementType>({as, children, ...props}: SummaryProps<As>) {
const Component = as ?? 'summary'
return (
<Component as={Component === 'summary' ? null : 'summary'} {...props}>
<Component as={Component === 'summary' ? null : 'summary'} {...props} data-component="Details.Summary">
{children}
</Component>
)
Expand Down
13 changes: 13 additions & 0 deletions packages/react/src/Details/__tests__/Details.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,17 @@ describe('Details', () => {
expect(screen.getByText('test summary')).toHaveAttribute('data-testid', 'test')
})
})

describe('Details data-component attributes', () => {
it('renders data-component attributes', () => {
render(
<Details>
<Details.Summary>test summary</Details.Summary>
</Details>,
)

expect(screen.getByRole('group')).toHaveAttribute('data-component', 'Details')
expect(screen.getByText('test summary')).toHaveAttribute('data-component', 'Details.Summary')
})
})
})
1 change: 1 addition & 0 deletions packages/react/src/Flash/Flash.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const Flash = React.forwardRef(function Flash(
className={clsx(classes.Flash, className)}
data-full={full ? '' : undefined}
data-variant={variant}
data-component="Flash"
/>
)
}) as PolymorphicForwardRefComponent<'div', FlashProps>
Expand Down
5 changes: 5 additions & 0 deletions packages/react/src/Flash/__tests__/Flash.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,9 @@ describe('Flash', () => {
const {container} = render(<Flash data-testid="test" />)
expect(container.firstChild).toHaveAttribute('data-testid', 'test')
})

it('renders data-component attribute', () => {
render(<Flash data-testid="flash" />)
expect(screen.getByTestId('flash')).toHaveAttribute('data-component', 'Flash')
})
})
2 changes: 2 additions & 0 deletions packages/react/src/FormControl/FormControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ const FormControl = React.forwardRef<HTMLDivElement, FormControlProps>(
data-has-leading-visual={slots.leadingVisual ? '' : undefined}
className={clsx(className, classes.ControlHorizontalLayout)}
style={style}
data-component="FormControl"
>
{InputChildren}
</div>
Expand All @@ -201,6 +202,7 @@ const FormControl = React.forwardRef<HTMLDivElement, FormControlProps>(
data-has-label={!isLabelHidden ? '' : undefined}
className={clsx(className, classes.ControlVerticalLayout)}
style={style}
data-component="FormControl"
>
{slots.label}
{React.isValidElement(InputComponent) &&
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/FormControl/FormControlCaption.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ function FormControlCaption({id, children, className, style}: FormControlCaption
className={clsx(className, classes.Caption)}
data-control-disabled={disabled ? '' : undefined}
style={style}
data-component="FormControl.Caption"
>
{children}
</Text>
Expand Down
6 changes: 5 additions & 1 deletion packages/react/src/FormControl/FormControlLabel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@ const FormControlLabel: FCWithSlotMarker<
...props,
}

return <InputLabel {...labelProps}>{children}</InputLabel>
return (
<InputLabel {...labelProps} data-component="FormControl.Label">
{children}
</InputLabel>
)
}

FormControlLabel.__SLOT__ = Symbol('FormControl.Label')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const FormControlLeadingVisual: FCWithSlotMarker<React.PropsWithChildren<{style?
data-control-disabled={disabled ? '' : undefined}
style={style}
data-has-caption={captionId ? '' : undefined}
data-component="FormControl.LeadingVisual"
>
{children}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const FormControlValidation: FCWithSlotMarker<React.PropsWithChildren<FormContro
validationStatus={variant}
id={id || validationMessageId || ''}
style={style}
data-component="FormControl.Validation"
>
{children}
</InputValidation>
Expand Down
36 changes: 36 additions & 0 deletions packages/react/src/FormControl/__tests__/FormControl.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,42 @@
implementsClassName(props => <FormControl {...props} layout="horizontal" />, classes.ControlHorizontalLayout)
implementsClassName(FormControl.Caption, captionClasses.Caption)
implementsClassName(FormControl.Label, inputClasses.Label)

it('renders data-component attributes (vertical, non-choice input)', () => {
const {container, getByText} = render(
<FormControl id="test-id">
<FormControl.Label>{LABEL_TEXT}</FormControl.Label>
<TextInput />
<FormControl.Caption>{CAPTION_TEXT}</FormControl.Caption>
<FormControl.Validation variant="error">{ERROR_TEXT}</FormControl.Validation>
</FormControl>,
)

expect(container.firstElementChild).toHaveAttribute('data-component', 'FormControl')
expect(getByText(LABEL_TEXT)).toHaveAttribute('data-component', 'FormControl.Label')
expect(getByText(CAPTION_TEXT)).toHaveAttribute('data-component', 'FormControl.Caption')

const validation = container.querySelector('[data-component="FormControl.Validation"]')
expect(validation).toHaveTextContent(ERROR_TEXT)
})

it('renders data-component attributes (choice input)', () => {
const {container, getByText} = render(
<FormControl id="test-id-choice">
<FormControl.Label>{LABEL_TEXT}</FormControl.Label>
<Checkbox />
<FormControl.LeadingVisual>
<MarkGithubIcon aria-label="Icon label" />
</FormControl.LeadingVisual>
</FormControl>,
)

expect(container.firstElementChild).toHaveAttribute('data-component', 'FormControl')
expect(getByText(LABEL_TEXT)).toHaveAttribute('data-component', 'FormControl.Label')

const leadingVisual = container.querySelector('[data-component="FormControl.LeadingVisual"]')

Check failure on line 99 in packages/react/src/FormControl/__tests__/FormControl.test.tsx

View workflow job for this annotation

GitHub Actions / lint

'leadingVisual' is assigned a value but never used
Comment thread
llastflowers marked this conversation as resolved.
})

describe('vertically stacked layout (default)', () => {
describe('rendering', () => {
it('renders with a hidden label', () => {
Expand Down
10 changes: 10 additions & 0 deletions packages/react/src/Header/Header.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ import classes from './Header.module.css'

describe('Header', () => {
implementsClassName(Header, classes.Header)
it('renders data-component attributes', () => {
const {container: headerContainer} = render(<Header />)
expect(headerContainer.firstChild).toHaveAttribute('data-component', 'Header')

const {container: itemContainer} = render(<Header.Item />)
expect(itemContainer.firstChild).toHaveAttribute('data-component', 'Header.Item')

const {container: linkContainer} = render(<Header.Link />)
expect(linkContainer.firstChild).toHaveAttribute('data-component', 'Header.Link')
})
describe('Header.Item', () => {
implementsClassName(Header.Item, classes.HeaderItem)
it('accepts and applies className', () => {
Expand Down
17 changes: 14 additions & 3 deletions packages/react/src/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const Header = React.forwardRef<HTMLElement, HeaderProps>(function Header(
forwardRef,
) {
return (
<BaseComponent ref={forwardRef} className={clsx(className, classes.Header)} {...rest}>
<BaseComponent ref={forwardRef} className={clsx(className, classes.Header)} {...rest} data-component="Header">
{children}
</BaseComponent>
)
Expand All @@ -26,7 +26,13 @@ const HeaderItem = React.forwardRef<HTMLDivElement, HeaderItemProps>(function He
forwardRef,
) {
return (
<div ref={forwardRef} className={clsx(className, classes.HeaderItem)} data-full={full} {...rest}>
<div
ref={forwardRef}
className={clsx(className, classes.HeaderItem)}
data-full={full}
{...rest}
data-component="Header.Item"
>
{children}
</div>
)
Expand All @@ -39,7 +45,12 @@ const HeaderLink = React.forwardRef<HTMLAnchorElement, HeaderLinkProps>(function
forwardRef,
) {
return (
<BaseComponent ref={forwardRef} className={clsx(className, classes.HeaderLink)} {...rest}>
<BaseComponent
ref={forwardRef}
className={clsx(className, classes.HeaderLink)}
{...rest}
data-component="Header.Link"
>
{children}
</BaseComponent>
)
Expand Down
10 changes: 9 additions & 1 deletion packages/react/src/Heading/Heading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,15 @@ const Heading = forwardRef(({as: Component = 'h2', className, variant, ...props}
}, [innerRef])
}

return <Component className={clsx(className, classes.Heading)} data-variant={variant} {...props} ref={innerRef} />
return (
<Component
className={clsx(className, classes.Heading)}
data-variant={variant}
{...props}
data-component="Heading"
ref={innerRef}
/>
)
}) as PolymorphicForwardRefComponent<HeadingLevels, StyledHeadingProps>

Heading.displayName = 'Heading'
Expand Down
5 changes: 5 additions & 0 deletions packages/react/src/Heading/__tests__/Heading.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ import {implementsClassName} from '../../utils/testing'
describe('Heading', () => {
implementsClassName(Heading, classes.Heading)

it('renders data-component attribute', () => {
const {container} = render(<Heading />)
expect(container.firstChild).toHaveAttribute('data-component', 'Heading')
})

it('renders <h2> by default', () => {
const {container} = render(<Heading />)
const heading = container.firstChild as HTMLElement
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type Props = {
id: string
validationStatus?: FormValidationStatus
style?: React.CSSProperties
'data-component'?: string
}

const validationIconMap: Record<
Expand All @@ -27,6 +28,7 @@ const InputValidation: React.FC<React.PropsWithChildren<Props>> = ({
id,
validationStatus,
style,
'data-component': dataComponent,
}) => {
const IconComponent = validationStatus ? validationIconMap[validationStatus] : undefined

Expand All @@ -37,7 +39,12 @@ const InputValidation: React.FC<React.PropsWithChildren<Props>> = ({
const iconBoxMinHeight = iconSize * captionLineHeight

return (
<Text className={clsx(className, classes.InputValidation)} data-validation-status={validationStatus} style={style}>
<Text
className={clsx(className, classes.InputValidation)}
data-validation-status={validationStatus}
style={style}
data-component={dataComponent}
>
{IconComponent ? (
<span
aria-hidden="true"
Expand Down
Loading