Skip to content

Combobox.ChipWithRemove: Improve chip and remove button accessible names - #82042

Closed
mirka wants to merge 7 commits into
trunkfrom
fix-combobox-chip-aria-label
Closed

mirka wants to merge 7 commits into
trunkfrom
fix-combobox-chip-aria-label

Conversation

@mirka

@mirka mirka commented Aug 25, 2026 •

Copy link
Copy Markdown
Member

What?

Improves accessible naming for Combobox.ChipWithRemove chips and their remove buttons.

Why?

When virtual focus moves to a chip via arrow keys, screen readers were announcing the chip name followed by "Remove", because the nested remove button label was being aggregated into the chip's accessible name. Separately, every remove button shared the same generic "Remove" accessible name when activated directly.

This follows the pattern used in Base UI's Combobox multiple-select example, which sets an explicit aria-label on the chip.

How?

  • Set aria-label on the chip from string children when no explicit label is passed.
  • Derive the remove button accessible name from string children as a translated Remove %s.
  • Keep the remove button tooltip label as Remove, since the chip name is already visible beside the X.
  • Compose the remove button with Tooltip + Button + Icon directly, rather than changing IconButton's one-label contract.
  • Add removeLabel (tooltip) and removeAriaLabel (accessible name) props on ChipWithRemove.
  • Document that custom chipsContent with non-string chip content should pass explicit aria-label and removeAriaLabel.

Testing Instructions

  1. Run Storybook and open Design System / Components / Form / Primitives / SearchableChipSelect.
  2. In Default, select a few items.
  3. With VoiceOver (or another screen reader), arrow from the search input to a chip and confirm only the chip name is announced.
  4. In Chrome DevTools, inspect a chip remove button and confirm its accessible name is "Remove [chip name]" (not just "Remove"). Alternatively, open the VoiceOver rotor → Buttons and confirm the same labels appear there.
  5. Hover a chip remove button and confirm the tooltip shows "Remove", not "Remove [chip name]".

@mirka mirka self-assigned this Aug 25, 2026
@github-actions github-actions Bot added the [Package] UI /packages/ui label Aug 25, 2026
@mirka mirka added the [Type] Bug An existing feature does not function as intended label Aug 25, 2026
@github-actions

Copy link
Copy Markdown

Size Change: 0 B

Total Size: 7.91 MB

compressed-size-action

@mirka
mirka marked this pull request as ready for review August 25, 2026 21:20
@mirka
mirka requested a review from a team as a code owner August 25, 2026 21:20
@github-actions

github-actions Bot commented Aug 25, 2026 •

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: mirka <0mirka00@git.wordpress.org>
Co-authored-by: ciampo <mciampini@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

ref={ ref }
className={ clsx( styles.chip, className ) }
{ ...restProps }
aria-label={ chipAriaLabel }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Tecnically we're adding an aria-label to a plain div, but the ARIA generic role prohibits naming, and ARIA in HTML prohibits aria-label on roles that cannot be named.

Should we give the default chip a nameable role or element, maybe group or something else?

Comment on lines +35 to +43
const removeAriaLabel =
removeAriaLabelProp ??
( typeof children === 'string'
? sprintf(
/* translators: %s: chip label. */
__( 'Remove %s' ),
children
)
: __( 'Remove' ) );

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Previously we documented removeLabel as the remove button's accessible label. This PR changes it to tooltip-only, so existing code still compiles but gets a different accessible name. For example, removeLabel="Delete Apple" can produce a Delete Apple tooltip and a button named Remove, which can be problematic.

Should we removeLabel as the fallback when removeAriaLabel is absent? Something like

removeAriaLabelProp ??
	removeLabelProp ??
	( chipAriaLabel
		? sprintf( __( 'Remove %s' ), chipAriaLabel )
		: __( 'Remove' ) );

This would preserve existing consumers, while letting an explicit chip label provide the default remove-button context.

Feel free to ignore if we don't care as much about introducing these small regressions in existing usage

<Combobox.ChipWithRemove
key={ item.id }
removeLabel={ `Remove ${ item.value }` }
removeAriaLabel={ `Remove ${ item.value }` }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should we test a value that is different from the default? Otherwise the test would still pass even when the prop is ignored

@mirka

mirka commented Sep 17, 2026

Copy link
Copy Markdown
Member Author

Closing in favor of #82863.

@mirka mirka closed this Sep 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Package] UI /packages/ui [Type] Bug An existing feature does not function as intended

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants