Skip to content
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
abfe9c4
feat(ActionBar): add data-component attributes for better accessibility
francinelucca Apr 2, 2026
6381280
fix test
francinelucca Apr 2, 2026
091de76
Merge branch 'main' of github.com:primer/react into chore/implement-a…
francinelucca Apr 7, 2026
32d4062
fix(ActionBar): update data-component attributes for IconButton and Menu
francinelucca Apr 7, 2026
c7c0da7
Add data-component for ActionList
francinelucca Apr 8, 2026
4cf2494
fix(ActionList): add data-component attributes for Item and LinkItem
francinelucca Apr 8, 2026
2962363
test(Tooltip): add tests for data-component attributes in Tooltip and…
francinelucca Apr 8, 2026
3f92c21
Merge branch 'main' of github.com:primer/react into chore/implement-a…
francinelucca Apr 9, 2026
273f681
fix: update data-component attribute for GroupHeading to GroupHeading…
francinelucca Apr 9, 2026
8c38035
fix comment
francinelucca Apr 9, 2026
8c3d244
Merge branch 'main' into chore/implement-adr-023
francinelucca Apr 13, 2026
8ca6eac
add data-component attribute to Pagination
francinelucca Apr 13, 2026
ddca738
Merge branch 'chore/implement-adr-023' of github.com:primer/react int…
francinelucca Apr 13, 2026
6f96ebd
Add data-component attributes to TextInput components for improved ac…
francinelucca Apr 13, 2026
17f2bf0
test: add nested ActionList data-component attribute tests for Filter…
francinelucca Apr 13, 2026
ff94d66
Merge branch 'main' into chore/implement-adr-023
francinelucca Apr 14, 2026
79e42d4
Merge branch 'chore/implement-adr-023' of github.com:primer/react int…
francinelucca Apr 14, 2026
b05df83
test: enhance data-component attribute tests for FilteredActionList a…
francinelucca Apr 14, 2026
db67cc2
test: add data-component attribute tests for Table and Pagination com…
francinelucca Apr 14, 2026
e71ce27
test: add data-component attribute tests for Table.Header and Table.S…
francinelucca Apr 14, 2026
f2b0a1f
test: add data-component attribute to Button and ButtonReset components
francinelucca Apr 14, 2026
e94fe52
test: add data-component attribute for LinkButton and its tests
francinelucca Apr 14, 2026
8c886da
test: add data-component attribute to Link component and its tests
francinelucca Apr 14, 2026
76bb50d
test: add data-component attributes for SelectPanel.CloseButton and S…
francinelucca Apr 14, 2026
bba65c1
test: add stable data-component selectors to DataTable, Button, Link,…
francinelucca Apr 14, 2026
9aaead8
rearrange stuff
francinelucca Apr 14, 2026
8429e82
chore: auto-fix lint and formatting issues
francinelucca Apr 14, 2026
a874ad0
Merge branch 'main' of github.com:primer/react into chore/implement-a…
francinelucca Apr 16, 2026
34ac550
chore: update octicon
francinelucca Apr 16, 2026
6e784d9
Merge branch 'chore/implement-adr-023' of github.com:primer/react int…
francinelucca Apr 16, 2026
8e7bf4c
Merge branch 'main' into chore/implement-adr-023
francinelucca Apr 20, 2026
e20e7a9
test(vrt): update snapshots
francinelucca Apr 20, 2026
5f52ac4
Merge branch 'main' into chore/implement-adr-023
francinelucca Apr 22, 2026
b6f32bf
test: add implementsClassName for FilteredActionList component
francinelucca Apr 22, 2026
21cbd6d
test(vrt): update snapshots
francinelucca Apr 22, 2026
1e18968
Revert "test(vrt): update snapshots"
francinelucca Apr 22, 2026
1cbc315
Revert "test(vrt): update snapshots"
francinelucca Apr 22, 2026
2d13445
Merge branch 'main' into chore/implement-adr-023
francinelucca Apr 22, 2026
2eb3867
remove unnecessary ActionList.Item.Wrapper
francinelucca Apr 22, 2026
b4a9532
Merge branch 'chore/implement-adr-023' of github.com:primer/react int…
francinelucca Apr 22, 2026
5a607d3
data-component fix for ButtonReset
francinelucca Apr 22, 2026
7d57382
Update packages/react/src/ActionBar/ActionBar.tsx
francinelucca Apr 22, 2026
f7829b8
Update .changeset/datatable-stable-selectors.md
francinelucca Apr 22, 2026
f44ccfb
Merge branch 'main' into chore/implement-adr-023
francinelucca Apr 22, 2026
a3e4ba9
Merge branch 'main' into chore/implement-adr-023
francinelucca Apr 22, 2026
60aa35f
Merge branch 'main' into chore/implement-adr-023
francinelucca Apr 22, 2026
a0b9c60
Merge branch 'main' into chore/implement-adr-023
francinelucca Apr 22, 2026
b8dc78b
test(vrt): update snapshots
francinelucca Apr 22, 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
20 changes: 20 additions & 0 deletions .changeset/datatable-stable-selectors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
'@primer/react': minor
---

Add stable `data-component` selectors to multiple components following ADR-023:

- **ActionBar**
- **ActionList** and friends
- **Button**
- **FilteredActionList** and friends
- **Link**
- **LinkButton**
- **Pagination**
- **SelectPanel** and friends
- **Table** and friends
- **TextInput**
- **TextInputwithTokens**
Comment thread
francinelucca marked this conversation as resolved.
Outdated
- **TooltipV2**

This enables consumers to query and test components using stable selectors like `[data-component="Table.Row"]`.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

General comment, that's why I'm leaving it on the changelog.

How i understand it, the problem that we're trying to solve with data-component is to expose stable selectors which otherwise would be hidden or tucked away.

Some of these components are top level components that support className and are not composed in an invisible manner inside another component (like selectpanel items), should we still add data-component to them?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and are not composed in an invisible manner inside another component

I think this is the key here. Which one of these falls into that category? 🤔 The way I see it these all are either:

  • parts of bigger components (ex: Button inside SelectPanel, Button inside table, Tooltip inside everywhere), so we want the data-component to be able to drill down:
image
  • Big components with smaller parts (SelectPanel has ActionList, Table has Button, ActionBar has Button...), so we need the top-level data-component to "inherit from":
image

62 changes: 62 additions & 0 deletions packages/react/src/ActionBar/ActionBar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -417,3 +417,65 @@ describe('ActionBar.Menu returnFocusRef', () => {
expect(document.activeElement).toEqual(menuButton)
})
})

describe('ActionBar data-component attributes', () => {
it('renders ActionBar with data-component attribute', () => {
const {container} = render(
<ActionBar aria-label="Toolbar">
<ActionBar.IconButton icon={BoldIcon} aria-label="Bold" />
</ActionBar>,
)

const actionBar = container.querySelector('[data-component="ActionBar"]')
expect(actionBar).toBeInTheDocument()
})

it('renders ActionBar.IconButton with data-component attribute', () => {
const {container} = render(
<ActionBar aria-label="Toolbar">
<ActionBar.IconButton icon={BoldIcon} aria-label="Bold" />
</ActionBar>,
)

const iconButton = container.querySelector('[data-component="ActionBar"] [data-component="IconButton"]')
expect(iconButton).toBeInTheDocument()
})

it('renders ActionBar.VerticalDivider with data-component attribute', () => {
const {container} = render(
<ActionBar aria-label="Toolbar">
<ActionBar.IconButton icon={BoldIcon} aria-label="Bold" />
<ActionBar.Divider />
<ActionBar.IconButton icon={ItalicIcon} aria-label="Italic" />
</ActionBar>,
)

const divider = container.querySelector('[data-component="ActionBar.VerticalDivider"]')
expect(divider).toBeInTheDocument()
})

it('renders ActionBar.Group with data-component attribute', () => {
const {container} = render(
<ActionBar aria-label="Toolbar">
<ActionBar.Group>
<ActionBar.IconButton icon={BoldIcon} aria-label="Bold" />
<ActionBar.IconButton icon={ItalicIcon} aria-label="Italic" />
</ActionBar.Group>
</ActionBar>,
)

const group = container.querySelector('[data-component="ActionBar.Group"]')
expect(group).toBeInTheDocument()
})

it('renders ActionBar.Menu.IconButton with data-component attribute', () => {
render(
<ActionBar aria-label="Toolbar">
<ActionBar.Menu aria-label="More options" icon={BoldIcon} items={[{label: 'Option 1', onClick: vi.fn()}]} />
</ActionBar>,
)

const menuButton = screen.getByRole('button', {name: 'More options'})
expect(menuButton).toHaveAttribute('data-component', 'ActionBar.Menu.IconButton')
})
})
13 changes: 10 additions & 3 deletions packages/react/src/ActionBar/ActionBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ export const ActionBar: React.FC<React.PropsWithChildren<ActionBarProps>> = prop

return (
<ActionBarContext.Provider value={{size, isVisibleChild}}>
<div ref={navRef} className={clsx(className, styles.Nav)} data-flush={flush}>
<div ref={navRef} className={clsx(className, styles.Nav)} data-component="ActionBar" data-flush={flush}>
<div
ref={listRef}
role="toolbar"
Expand Down Expand Up @@ -532,7 +532,7 @@ export const ActionBarGroup = forwardRef(({children}: React.PropsWithChildren, f

return (
<ActionBarGroupContext.Provider value={{groupId: id}}>
<div className={styles.Group} ref={ref}>
<div className={styles.Group} data-component="ActionBar.Group" ref={ref}>
{children}
</div>
</ActionBarGroupContext.Provider>
Expand Down Expand Up @@ -571,7 +571,14 @@ export const ActionBarMenu = forwardRef(
return (
<ActionMenu anchorRef={ref} open={menuOpen} onOpenChange={setMenuOpen}>
<ActionMenu.Anchor>
<IconButton variant="invisible" aria-label={ariaLabel} icon={icon} {...props} />
<IconButton
variant="invisible"
aria-label={ariaLabel}
icon={icon}
{...props}
// overriding IconButton's data-component so that the ActionBar's "More Menu" Icon can be targetted specifically
Comment thread
francinelucca marked this conversation as resolved.
Outdated
data-component="ActionBar.Menu.IconButton"
Comment thread
francinelucca marked this conversation as resolved.
/>
</ActionMenu.Anchor>
<ActionMenu.Overlay {...(returnFocusRef && {returnFocusRef})}>
<ActionList>{items.map((item, index) => renderMenuItem(item, index))}</ActionList>
Expand Down
177 changes: 177 additions & 0 deletions packages/react/src/ActionList/ActionList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -258,3 +258,180 @@ describe('ActionList', () => {
expect(linkElements[2]).toHaveAttribute('data-size', 'medium') // default should be medium
})
})

describe('ActionList data-component attributes', () => {
it('renders ActionList with data-component attribute', () => {
const {container} = HTMLRender(
<ActionList aria-label="List">
<ActionList.Item>Item</ActionList.Item>
</ActionList>,
)

const actionList = container.querySelector('[data-component="ActionList"]')
expect(actionList).toBeInTheDocument()
})

it('renders ActionList.Item with data-component attribute', () => {
const {container} = HTMLRender(
<ActionList aria-label="List">
<ActionList.Item>Item</ActionList.Item>
</ActionList>,
)

const item = container.querySelector('[data-component="ActionList.Item"]')
expect(item).toBeInTheDocument()
})

it('renders ActionList.Item.Wrapper with data-component attribute', () => {
const {container} = HTMLRender(
<ActionList aria-label="List">
<ActionList.Item>Item</ActionList.Item>
</ActionList>,
)

const wrapper = container.querySelector('[data-component="ActionList.Item.Wrapper"]')
expect(wrapper).toBeInTheDocument()
})

it('renders ActionList.Item.Label with data-component attribute', () => {
const {container} = HTMLRender(
<ActionList aria-label="List">
<ActionList.Item>Item</ActionList.Item>
</ActionList>,
)

const label = container.querySelector('[data-component="ActionList.Item.Label"]')
expect(label).toBeInTheDocument()
})

it('renders ActionList.Item--DividerContainer with data-component attribute', () => {
const {container} = HTMLRender(
<ActionList aria-label="List">
<ActionList.Item>Item</ActionList.Item>
</ActionList>,
)

const dividerContainer = container.querySelector('[data-component="ActionList.Item--DividerContainer"]')
expect(dividerContainer).toBeInTheDocument()
})

it('renders ActionList.Group with data-component attribute', () => {
const {container} = HTMLRender(
<ActionList aria-label="List">
<ActionList.Group>
<ActionList.GroupHeading as="h3">Group</ActionList.GroupHeading>
<ActionList.Item>Item</ActionList.Item>
</ActionList.Group>
</ActionList>,
)

const group = container.querySelector('[data-component="ActionList.Group"]')
expect(group).toBeInTheDocument()
})

it('renders ActionList.GroupHeading with data-component attribute', () => {
const {container} = HTMLRender(
<ActionList aria-label="List">
<ActionList.Group>
<ActionList.GroupHeading as="h3">Group Heading</ActionList.GroupHeading>
<ActionList.Item>Item</ActionList.Item>
</ActionList.Group>
</ActionList>,
)

const groupHeading = container.querySelector('[data-component="GroupHeadingWrap"]')
expect(groupHeading).toBeInTheDocument()
})

it('renders ActionList.Divider with data-component attribute', () => {
const {container} = HTMLRender(
<ActionList aria-label="List">
<ActionList.Item>Item 1</ActionList.Item>
<ActionList.Divider />
<ActionList.Item>Item 2</ActionList.Item>
</ActionList>,
)

const divider = container.querySelector('[data-component="ActionList.Divider"]')
expect(divider).toBeInTheDocument()
})

it('renders ActionList.Description with data-component attribute', () => {
const {container} = HTMLRender(
<ActionList aria-label="List">
<ActionList.Item>
Item
<ActionList.Description>Description</ActionList.Description>
</ActionList.Item>
</ActionList>,
)

const description = container.querySelector('[data-component="ActionList.Description"]')
expect(description).toBeInTheDocument()
})

it('renders ActionList.LeadingVisual with data-component attribute', () => {
const {container} = HTMLRender(
<ActionList aria-label="List">
<ActionList.Item>
<ActionList.LeadingVisual>Icon</ActionList.LeadingVisual>
Item
</ActionList.Item>
</ActionList>,
)

const leadingVisual = container.querySelector('[data-component="ActionList.LeadingVisual"]')
expect(leadingVisual).toBeInTheDocument()
})

it('renders ActionList.TrailingVisual with data-component attribute', () => {
const {container} = HTMLRender(
<ActionList aria-label="List">
<ActionList.Item>
Item
<ActionList.TrailingVisual>Icon</ActionList.TrailingVisual>
</ActionList.Item>
</ActionList>,
)

const trailingVisual = container.querySelector('[data-component="ActionList.TrailingVisual"]')
expect(trailingVisual).toBeInTheDocument()
})

it('renders ActionList.Selection with data-component attribute', () => {
const {container} = HTMLRender(
<ActionList selectionVariant="single" aria-label="List">
<ActionList.Item selected>Item</ActionList.Item>
</ActionList>,
)

const selection = container.querySelector('[data-component="ActionList.Selection"]')
expect(selection).toBeInTheDocument()
})

it('renders ActionList.Heading with data-component attribute', () => {
const {container} = HTMLRender(
<ActionList aria-label="List">
<ActionList.Heading as="h2">Heading</ActionList.Heading>
<ActionList.Item>Item</ActionList.Item>
</ActionList>,
)

const heading = container.querySelector('[data-component="ActionList.Heading"]')
expect(heading).toBeInTheDocument()
})

it('renders ActionList.TrailingAction with data-component attribute', () => {
const {container} = HTMLRender(
<ActionList aria-label="List">
<ActionList.Item>
Item
<ActionList.TrailingAction label="Action" />
</ActionList.Item>
</ActionList>,
)

const trailingAction = container.querySelector('[data-component="ActionList.TrailingAction"]')
expect(trailingAction).toBeInTheDocument()
})
})
9 changes: 8 additions & 1 deletion packages/react/src/ActionList/Group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,12 @@ export const Group: FCWithSlotMarker<React.PropsWithChildren<ActionListGroupProp
}

return (
<li className={clsx(className, groupClasses.Group)} role={listRole ? 'none' : undefined} {...props}>
<li
className={clsx(className, groupClasses.Group)}
data-component="ActionList.Group"
role={listRole ? 'none' : undefined}
{...props}
>
<GroupContext.Provider value={{selectionVariant, groupHeadingId}}>
{title && !slots.groupHeading ? (
// Escape hatch: supports old API <ActionList.Group title="group title"> in a non breaking way
Expand Down Expand Up @@ -177,6 +182,7 @@ export const GroupHeading: FCWithSlotMarker<React.PropsWithChildren<ActionListGr
className={groupClasses.GroupHeadingWrap}
aria-hidden="true"
data-variant={variant}
// TODO: next-major: switch for data-component="ActionList.GroupHeading" next major
data-component="GroupHeadingWrap"
as={headingWrapElement}
{...props}
Expand All @@ -192,6 +198,7 @@ export const GroupHeading: FCWithSlotMarker<React.PropsWithChildren<ActionListGr
className={groupClasses.GroupHeadingWrap}
data-variant={variant}
as={headingWrapElement}
// TODO: next-major: switch for data-component="ActionList.GroupHeading" next major
data-component="GroupHeadingWrap"
>
<Heading
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/ActionList/Heading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const Heading = forwardRef(({as, size, children, visuallyHidden = false,
// use custom id if it is provided. Otherwise, use the id from the context
id={props.id ?? headingId}
className={clsx(className, classes.ActionListHeader)}
data-component="ActionList.Heading"
data-list-variant={listVariant}
{...props}
>
Expand Down
Loading
Loading