Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
fff3f9d
update ActionMenu and add test
llastflowers May 21, 2026
dc1cc17
update AnchoredOverlay and add test
llastflowers May 21, 2026
d886f3c
update Autocomplete sub-components and add test
llastflowers May 22, 2026
55b406e
update NavList and add tests
llastflowers May 22, 2026
c6b08e9
tweak ActionMenu implementation to avoid downstream conflict with Act…
llastflowers May 22, 2026
3cd151f
add quick-ants-lead.md
llastflowers May 22, 2026
f3aa549
change override prevention logic in ActionMenu to prevent unwanted ov…
llastflowers May 22, 2026
55e192e
Merge branch 'main' into llastflowers/6497/data-component-ADR-part-5
llastflowers May 22, 2026
24026be
remove data-component from ActionMenu.Anchor
llastflowers May 22, 2026
bc3dc32
test fixes
llastflowers May 22, 2026
fd4e024
Merge branch 'main' into llastflowers/6497/data-component-ADR-part-5
llastflowers May 22, 2026
06c2480
Merge branch 'main' into llastflowers/6497/data-component-ADR-part-5
llastflowers May 28, 2026
de8cdc0
move data-component=ActionMenu.Overlay to AnchoredOverlay and allow p…
llastflowers Jun 1, 2026
9389cc7
add data-component to ActionMenu.Anchor
llastflowers Jun 1, 2026
7ab9e8b
update data-component=AnchoredOverlay.CloseButton implementation and …
llastflowers Jun 1, 2026
e1025dd
add data-component to AnchoredOverlay.Anchor and update tests
llastflowers Jun 1, 2026
bd14c4d
Autocomplete tweaks and fix for AnchoredOverlay.Anchor overriding Act…
llastflowers Jun 2, 2026
81832a2
update NavList tests
llastflowers Jun 2, 2026
0680071
update NavList.Item and test
llastflowers Jun 2, 2026
899c889
Merge branch 'main' into llastflowers/6497/data-component-ADR-part-5
llastflowers Jun 2, 2026
33f4e18
include data-component in TextInput props type
llastflowers Jun 2, 2026
51ab781
Merge branch 'main' into llastflowers/6497/data-component-ADR-part-5
llastflowers Jun 2, 2026
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/quick-ants-lead.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 ActionMenu, AnchoredOverlay, Autocomplete, and NavList
15 changes: 14 additions & 1 deletion packages/react/src/ActionMenu/ActionMenu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,19 @@ function ExampleWithSubmenus(): JSX.Element {
describe('ActionMenu', () => {
implementsClassName(ActionMenu.Button)

it('renders data-component attributes for ActionMenu parts', async () => {
const component = HTMLRender(<Example />)
const user = userEvent.setup()

const trigger = component.getByRole('button', {name: 'Toggle Menu'})
expect(trigger).toHaveAttribute('data-component', 'ActionMenu.Button')

await user.click(trigger)

expect(component.baseElement.querySelector('[data-component="ActionMenu.Overlay"]')).not.toBeNull()
expect(component.baseElement.querySelector('[data-component="AnchoredOverlay"]')).toBeNull()
})

it('should open Menu on MenuButton click', async () => {
const component = HTMLRender(<Example />)
const button = component.getByRole('button')
Expand Down Expand Up @@ -778,7 +791,7 @@ describe('ActionMenu', () => {
const initialAnchor = component.getByRole('button', {name: 'Open menu'})
await user.click(initialAnchor)

const overlay = component.baseElement.querySelector('[data-component="AnchoredOverlay"]') as HTMLElement
const overlay = component.baseElement.querySelector('[data-component="ActionMenu.Overlay"]') as HTMLElement
expect(overlay).not.toBeNull()

const initialAnchorName = initialAnchor.style.getPropertyValue('anchor-name')
Expand Down
9 changes: 7 additions & 2 deletions packages/react/src/ActionMenu/ActionMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ const Anchor: WithSlotMarker<
className: clsx(anchorProps.className, child.props.className),
onClick: onButtonClick,
onKeyDown: onButtonKeyDown,
'data-component': child.props['data-component'] ?? 'ActionMenu.Anchor',
})}
</ActionListContainerContext.Provider>
)
Expand All @@ -256,7 +257,7 @@ export type ActionMenuButtonProps = ButtonProps
const MenuButton = React.forwardRef(({...props}, anchorRef) => {
return (
<Anchor ref={anchorRef}>
<Button type="button" trailingAction={TriangleDownIcon} {...props} />
<Button data-component="ActionMenu.Button" type="button" trailingAction={TriangleDownIcon} {...props} />
</Anchor>
)
}) as PolymorphicForwardRefComponent<'button', ActionMenuButtonProps>
Expand Down Expand Up @@ -350,12 +351,16 @@ const Overlay: FCWithSlotMarker<React.PropsWithChildren<MenuOverlayProps>> = ({
anchorRef={anchorRef}
renderAnchor={renderAnchor}
anchorId={anchorId}
anchorProps={{'data-component': 'ActionMenu.Button'}}
open={open}
onOpen={onOpen}
onClose={handleClose}
align={align}
side={side ?? (isSubmenu ? 'outside-right' : 'outside-bottom')}
overlayProps={overlayProps}
overlayProps={{
...overlayProps,
'data-component': 'ActionMenu.Overlay',
}}
focusZoneSettings={isNarrowFullscreen ? {disabled: true} : {focusOutBehavior: 'wrap'}}
onPositionChange={onPositionChange}
variant={variant}
Expand Down
21 changes: 21 additions & 0 deletions packages/react/src/AnchoredOverlay/AnchoredOverlay.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,27 @@ describe.each([true, false])(
})
})

it('renders data-component attributes for AnchoredOverlay parts when shown', () => {
const {baseElement} = render(
<FeatureFlags flags={{primer_react_css_anchor_positioning: true}}>
<BaseStyles>
<AnchoredOverlay
open={true}
onOpen={() => {}}
onClose={() => {}}
renderAnchor={props => <Button {...props}>Anchor Button</Button>}
variant={{regular: 'anchored', narrow: 'fullscreen'}}
>
<div>content</div>
</AnchoredOverlay>
</BaseStyles>
</FeatureFlags>,
)

expect(baseElement.querySelector('[data-component="AnchoredOverlay.Anchor"]')).toBeInTheDocument()
expect(baseElement.querySelector('[data-component="AnchoredOverlay.CloseButton"]')).toBeInTheDocument()
})

it('should support a `ref` through `overlayProps` on the overlay element', () => {
const ref = createRef<HTMLDivElement>()

Expand Down
12 changes: 12 additions & 0 deletions packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ export type AnchoredOverlayWrapperAnchorProps =
| AnchoredOverlayPropsWithoutAnchor

interface AnchoredOverlayBaseProps extends Pick<OverlayProps, 'height' | 'width'> {
/**
* Props that will be merged into the props passed to `renderAnchor`.
* Useful for overriding the anchor `data-component` in composite components.
*/
anchorProps?: Omit<React.HTMLAttributes<HTMLElement>, 'aria-label' | 'aria-labelledby'> & {
'data-component'?: string
}

/**
* Determines whether the overlay portion of the component should be shown or not
*/
Expand Down Expand Up @@ -143,6 +151,7 @@ const defaultCloseButtonProps: Partial<IconButtonProps> = {}
*/
export const AnchoredOverlay: React.FC<React.PropsWithChildren<AnchoredOverlayProps>> = ({
renderAnchor,
anchorProps,
anchorRef: externalAnchorRef,
anchorId: externalAnchorId,
children,
Expand Down Expand Up @@ -361,13 +370,15 @@ export const AnchoredOverlay: React.FC<React.PropsWithChildren<AnchoredOverlayPr
{renderAnchor &&
// eslint-disable-next-line react-hooks/refs
renderAnchor({
...(anchorProps ?? {}),
ref: anchorRef,
id: anchorId,
'aria-haspopup': 'true',
'aria-expanded': open,
tabIndex: 0,
onClick: onAnchorClick,
onKeyDown: onAnchorKeyDown,
'data-component': anchorProps?.['data-component'] ?? 'AnchoredOverlay.Anchor',
...(shouldRenderAsPopover ? {popoverTarget: popoverId} : {}),
})}
{open ? (
Expand Down Expand Up @@ -405,6 +416,7 @@ export const AnchoredOverlay: React.FC<React.PropsWithChildren<AnchoredOverlayPr
<div className={classes.ResponsiveCloseButtonContainer}>
<IconButton
{...(closeButtonProps as IconButtonProps)}
data-component="AnchoredOverlay.CloseButton"
type="button"
variant="invisible"
icon={XIcon}
Expand Down
18 changes: 18 additions & 0 deletions packages/react/src/Autocomplete/Autocomplete.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,24 @@ describe('Autocomplete', () => {
<Autocomplete.Input {...props} />
</Autocomplete>
))

it('renders data-component attributes for Autocomplete parts when menu is shown', async () => {
const user = userEvent.setup()
const {container} = render(
<LabelledAutocomplete
menuProps={{items: mockItems, selectedItemIds: [], ['aria-labelledby']: 'autocompleteLabel'}}
/>,
)

const input = container.querySelector('#autocompleteInput') as HTMLInputElement
expect(input).toHaveAttribute('data-component', 'Autocomplete.Input')

await user.type(input, 'z')

expect(container.querySelector('[data-component="Autocomplete.Overlay"]')).toBeInTheDocument()
expect(container.querySelector('[data-component="Autocomplete.Menu"]')).toBeInTheDocument()
})
Comment thread
llastflowers marked this conversation as resolved.

it('calls onChange', async () => {
const user = userEvent.setup()
const onChangeMock = vi.fn()
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/Autocomplete/AutocompleteInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ const AutocompleteInput = React.forwardRef(
autoComplete="off"
id={id}
{...props}
data-component="Autocomplete.Input"
/>
)
},
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/Autocomplete/AutocompleteMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,7 @@ function AutocompleteMenu<T extends AutocompleteItemProps>(props: AutocompleteMe
<div ref={listContainerRef}>
{allItemsToRender.length ? (
<ActionList
data-component="Autocomplete.Menu"
selectionVariant={selectionVariant} // TODO: make this configurable
role="listbox"
id={`${id}-listbox`}
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/Autocomplete/AutocompleteOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ function AutocompleteOverlay({
left={position?.left}
className={clsx(classes.Overlay, className)}
{...overlayProps}
data-component="Autocomplete.Overlay"
>
{children}
</Overlay>
Expand Down
36 changes: 36 additions & 0 deletions packages/react/src/NavList/NavList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,42 @@ const NextJSLikeLink = React.forwardRef<HTMLAnchorElement, NextJSLinkProps>(
describe('NavList', () => {
implementsClassName(NavList)

it('renders data-component attributes for NavList, NavList.Item, and NavList.SubNav', () => {
const {container, getByRole} = render(
<NavList>
<NavList.Item href="#">Item 1</NavList.Item>
<NavList.Item>
Item 2
<NavList.SubNav>
<NavList.Item href="#">Sub Item 1</NavList.Item>
</NavList.SubNav>
</NavList.Item>
</NavList>,
)

const nav = container.querySelector('nav')
expect(nav).toBeInTheDocument()
expect(nav).toHaveAttribute('data-component', 'NavList')

const item1Link = getByRole('link', {name: 'Item 1'})
expect(item1Link).toBeInTheDocument()
expect(item1Link).toHaveAttribute('data-component', 'NavList.Item')

const item2Button = getByRole('button', {name: 'Item 2'})
expect(item2Button).toBeInTheDocument()
expect(item2Button).toHaveAttribute('data-component', 'NavList.Item')

const subNav = container.querySelector('[data-component="NavList.SubNav"]')
expect(subNav).toBeInTheDocument()
Comment thread
llastflowers marked this conversation as resolved.

// Expand so nested links are in the accessible tree
fireEvent.click(item2Button)

const subItem1Link = getByRole('link', {name: 'Sub Item 1'})
expect(subItem1Link).toBeInTheDocument()
expect(subItem1Link).toHaveAttribute('data-component', 'NavList.Item')
})

it('supports TrailingAction', async () => {
const {getByRole} = render(
<NavList>
Expand Down
12 changes: 10 additions & 2 deletions packages/react/src/NavList/NavList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export type NavListProps = {

const Root = React.forwardRef<HTMLElement, NavListProps>(({children, ...props}, ref) => {
return (
<nav {...props} ref={ref}>
<nav {...props} ref={ref} data-component="NavList">
<ActionListContainerContext.Provider
value={{
container: 'NavList',
Expand Down Expand Up @@ -102,6 +102,7 @@ const ItemComponent = fixedForwardRef(
aria-current={ariaCurrent}
active={Boolean(ariaCurrent) && ariaCurrent !== 'false'}
style={{'--subitem-depth': depth} as React.CSSProperties}
data-component="NavList.Item"
{...props}
>
{children}
Expand Down Expand Up @@ -184,6 +185,7 @@ function ItemWithSubNav({children, subNav, depth: _depth, defaultOpen, style}: I
active={!isOpen && containsCurrentItem}
onSelect={() => setIsOpen(open => !open)}
style={style}
data-component="NavList.Item"
>
{children}
{/* What happens if the user provides a TrailingVisual? */}
Expand Down Expand Up @@ -224,7 +226,13 @@ const SubNav = React.forwardRef<HTMLUListElement, NavListSubNavProps>(({children

return (
<SubNavContext.Provider value={{depth: depth + 1}}>
<ul className={classes.SubGroup} id={subNavId} aria-labelledby={buttonId} ref={forwardedRef}>
<ul
className={classes.SubGroup}
id={subNavId}
aria-labelledby={buttonId}
ref={forwardedRef}
data-component="NavList.SubNav"
>
{children}
</ul>
</SubNavContext.Provider>
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/Overlay/Overlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ function getSlideAnimationStartingVector(anchorSide?: AnchorSide): {x: number; y
type BaseOverlayProps = {
visibility?: 'visible' | 'hidden'
'data-test-id'?: unknown
'data-component'?: string
position?: React.CSSProperties['position']
top?: React.CSSProperties['top']
left?: React.CSSProperties['left']
Expand Down
9 changes: 8 additions & 1 deletion packages/react/src/TextInput/TextInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ export type TextInputNonPassthroughProps = {
* When the limit is exceeded, validation styling will be applied.
*/
characterLimit?: number
/**
* Stable identifier for the underlying input element.
*
* TODO: next-major: Remove in favor of data-component="TextInput.Input"
*/
'data-component'?: string
} & Partial<
Pick<
StyledWrapperProps,
Expand Down Expand Up @@ -98,6 +104,7 @@ const TextInput = React.forwardRef<HTMLInputElement, TextInputProps>(
onChange,
value,
defaultValue,
'data-component': dataComponent,
...inputProps
},
ref,
Expand Down Expand Up @@ -276,7 +283,7 @@ const TextInput = React.forwardRef<HTMLInputElement, TextInputProps>(
: inputDescribedBy
}
// TODO: next-major: Remove in favor of data-component="TextInput.Input"
data-component="input"
data-component={dataComponent ?? 'input'}
/>
{loading && <VisuallyHidden id={loadingId}>{loaderText}</VisuallyHidden>}
<TextInputInnerVisualSlot
Expand Down
Loading