diff --git a/.changeset/tiny-hats-applaud.md b/.changeset/tiny-hats-applaud.md new file mode 100644 index 00000000000..8c563308bfc --- /dev/null +++ b/.changeset/tiny-hats-applaud.md @@ -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. diff --git a/packages/react/src/Details/Details.tsx b/packages/react/src/Details/Details.tsx index 2d512c41dfe..a28899accbe 100644 --- a/packages/react/src/Details/Details.tsx +++ b/packages/react/src/Details/Details.tsx @@ -27,7 +27,7 @@ const Root = React.forwardRef( }, []) return ( -
+
{children}
) @@ -47,7 +47,7 @@ export type SummaryProps = { function Summary({as, children, ...props}: SummaryProps) { const Component = as ?? 'summary' return ( - + {children} ) diff --git a/packages/react/src/Details/__tests__/Details.test.tsx b/packages/react/src/Details/__tests__/Details.test.tsx index ea1c7bac6cc..61afbccec04 100644 --- a/packages/react/src/Details/__tests__/Details.test.tsx +++ b/packages/react/src/Details/__tests__/Details.test.tsx @@ -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( +
+ test summary +
, + ) + + expect(screen.getByRole('group')).toHaveAttribute('data-component', 'Details') + expect(screen.getByText('test summary')).toHaveAttribute('data-component', 'Details.Summary') + }) + }) }) diff --git a/packages/react/src/Flash/Flash.tsx b/packages/react/src/Flash/Flash.tsx index 6f95ca311c1..b1b67db5f79 100644 --- a/packages/react/src/Flash/Flash.tsx +++ b/packages/react/src/Flash/Flash.tsx @@ -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> diff --git a/packages/react/src/Flash/__tests__/Flash.test.tsx b/packages/react/src/Flash/__tests__/Flash.test.tsx index 70dbbdea1f2..85c01a297d3 100644 --- a/packages/react/src/Flash/__tests__/Flash.test.tsx +++ b/packages/react/src/Flash/__tests__/Flash.test.tsx @@ -37,4 +37,9 @@ describe('Flash', () => { const {container} = render() expect(container.firstChild).toHaveAttribute('data-testid', 'test') }) + + it('renders data-component attribute', () => { + render() + expect(screen.getByTestId('flash')).toHaveAttribute('data-component', 'Flash') + }) }) diff --git a/packages/react/src/FormControl/FormControl.tsx b/packages/react/src/FormControl/FormControl.tsx index c8300bcfac1..cfbf3c44962 100644 --- a/packages/react/src/FormControl/FormControl.tsx +++ b/packages/react/src/FormControl/FormControl.tsx @@ -192,6 +192,7 @@ const FormControl = React.forwardRef( data-has-leading-visual={slots.leadingVisual ? '' : undefined} className={clsx(className, classes.ControlHorizontalLayout)} style={style} + data-component="FormControl" > {InputChildren} @@ -201,6 +202,7 @@ const FormControl = React.forwardRef( data-has-label={!isLabelHidden ? '' : undefined} className={clsx(className, classes.ControlVerticalLayout)} style={style} + data-component="FormControl" > {slots.label} {React.isValidElement(InputComponent) && diff --git a/packages/react/src/FormControl/FormControlCaption.tsx b/packages/react/src/FormControl/FormControlCaption.tsx index a4f402a7213..c8c30eba9ef 100644 --- a/packages/react/src/FormControl/FormControlCaption.tsx +++ b/packages/react/src/FormControl/FormControlCaption.tsx @@ -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} diff --git a/packages/react/src/FormControl/FormControlLabel.tsx b/packages/react/src/FormControl/FormControlLabel.tsx index b1d7f86a9a9..e0a6e829cb8 100644 --- a/packages/react/src/FormControl/FormControlLabel.tsx +++ b/packages/react/src/FormControl/FormControlLabel.tsx @@ -50,7 +50,11 @@ const FormControlLabel: FCWithSlotMarker< ...props, } - return {children} + return ( + + {children} + + ) } FormControlLabel.__SLOT__ = Symbol('FormControl.Label') diff --git a/packages/react/src/FormControl/FormControlLeadingVisual.tsx b/packages/react/src/FormControl/FormControlLeadingVisual.tsx index 5063c690df7..c038c1705be 100644 --- a/packages/react/src/FormControl/FormControlLeadingVisual.tsx +++ b/packages/react/src/FormControl/FormControlLeadingVisual.tsx @@ -14,6 +14,7 @@ const FormControlLeadingVisual: FCWithSlotMarker {children} diff --git a/packages/react/src/FormControl/_FormControlValidation.tsx b/packages/react/src/FormControl/_FormControlValidation.tsx index 6cde64ba242..f83322aad7c 100644 --- a/packages/react/src/FormControl/_FormControlValidation.tsx +++ b/packages/react/src/FormControl/_FormControlValidation.tsx @@ -25,6 +25,7 @@ const FormControlValidation: FCWithSlotMarker {children} diff --git a/packages/react/src/FormControl/__tests__/FormControl.test.tsx b/packages/react/src/FormControl/__tests__/FormControl.test.tsx index ae060144c5e..bd3141abdd6 100644 --- a/packages/react/src/FormControl/__tests__/FormControl.test.tsx +++ b/packages/react/src/FormControl/__tests__/FormControl.test.tsx @@ -63,6 +63,43 @@ describe('FormControl', () => { implementsClassName(props => , 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( + + {LABEL_TEXT} + + {CAPTION_TEXT} + {ERROR_TEXT} + , + ) + + 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( + + {LABEL_TEXT} + + + + + , + ) + + expect(container.firstElementChild).toHaveAttribute('data-component', 'FormControl') + expect(getByText(LABEL_TEXT)).toHaveAttribute('data-component', 'FormControl.Label') + + const leadingVisual = container.querySelector('[data-component="FormControl.LeadingVisual"]') + expect(leadingVisual).not.toBeNull() + }) + describe('vertically stacked layout (default)', () => { describe('rendering', () => { it('renders with a hidden label', () => { diff --git a/packages/react/src/Header/Header.test.tsx b/packages/react/src/Header/Header.test.tsx index 9266edd110f..ce6ce3e4731 100644 --- a/packages/react/src/Header/Header.test.tsx +++ b/packages/react/src/Header/Header.test.tsx @@ -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(
) + expect(headerContainer.firstChild).toHaveAttribute('data-component', 'Header') + + const {container: itemContainer} = render() + expect(itemContainer.firstChild).toHaveAttribute('data-component', 'Header.Item') + + const {container: linkContainer} = render() + expect(linkContainer.firstChild).toHaveAttribute('data-component', 'Header.Link') + }) describe('Header.Item', () => { implementsClassName(Header.Item, classes.HeaderItem) it('accepts and applies className', () => { diff --git a/packages/react/src/Header/Header.tsx b/packages/react/src/Header/Header.tsx index 531938db66d..69cd4fc175d 100644 --- a/packages/react/src/Header/Header.tsx +++ b/packages/react/src/Header/Header.tsx @@ -13,7 +13,7 @@ const Header = React.forwardRef(function Header( forwardRef, ) { return ( - + {children} ) @@ -26,7 +26,13 @@ const HeaderItem = React.forwardRef(function He forwardRef, ) { return ( -
+
{children}
) @@ -39,7 +45,12 @@ const HeaderLink = React.forwardRef(function forwardRef, ) { return ( - + {children} ) diff --git a/packages/react/src/Heading/Heading.tsx b/packages/react/src/Heading/Heading.tsx index 474a49ede24..ba2e517fd21 100644 --- a/packages/react/src/Heading/Heading.tsx +++ b/packages/react/src/Heading/Heading.tsx @@ -32,7 +32,15 @@ const Heading = forwardRef(({as: Component = 'h2', className, variant, ...props} }, [innerRef]) } - return + return ( + + ) }) as PolymorphicForwardRefComponent Heading.displayName = 'Heading' diff --git a/packages/react/src/Heading/__tests__/Heading.test.tsx b/packages/react/src/Heading/__tests__/Heading.test.tsx index f62fb42d03f..a3b7549fac0 100644 --- a/packages/react/src/Heading/__tests__/Heading.test.tsx +++ b/packages/react/src/Heading/__tests__/Heading.test.tsx @@ -7,6 +7,11 @@ import {implementsClassName} from '../../utils/testing' describe('Heading', () => { implementsClassName(Heading, classes.Heading) + it('renders data-component attribute', () => { + const {container} = render() + expect(container.firstChild).toHaveAttribute('data-component', 'Heading') + }) + it('renders

by default', () => { const {container} = render() const heading = container.firstChild as HTMLElement diff --git a/packages/react/src/internal/components/InputValidation.tsx b/packages/react/src/internal/components/InputValidation.tsx index e013cab6948..9aa592099b9 100644 --- a/packages/react/src/internal/components/InputValidation.tsx +++ b/packages/react/src/internal/components/InputValidation.tsx @@ -11,6 +11,7 @@ type Props = { id: string validationStatus?: FormValidationStatus style?: React.CSSProperties + 'data-component'?: string } const validationIconMap: Record< @@ -27,6 +28,7 @@ const InputValidation: React.FC> = ({ id, validationStatus, style, + 'data-component': dataComponent, }) => { const IconComponent = validationStatus ? validationIconMap[validationStatus] : undefined @@ -37,7 +39,12 @@ const InputValidation: React.FC> = ({ const iconBoxMinHeight = iconSize * captionLineHeight return ( - + {IconComponent ? (