Skip to content

feat(fuselage): add onDismiss prop to Chip rendering a dedicated dismiss IconButton - #2145

Open
jeanfbrito wants to merge 1 commit into
mainfrom
feat/chip-dismiss-icon-button
Open

feat(fuselage): add onDismiss prop to Chip rendering a dedicated dismiss IconButton#2145
jeanfbrito wants to merge 1 commit into
mainfrom
feat/chip-dismiss-icon-button

Conversation

@jeanfbrito

Copy link
Copy Markdown
Member

Proposed changes (including videos or screenshots)

Problem

Chip's dismiss action is currently triggered by clicking anywhere on the chip — the whole root element is a <button>, and the ✕ is just a decorative <Icon>. This makes the close affordance imprecise (any accidental click on the label or avatar removes the selection) and misleading for assistive technology, since the accessible name of the "close button" is the entire chip label.

Solution

This PR introduces an opt-in onDismiss prop that moves the dismiss trigger to a dedicated, real button:

  • New mode (onDismiss provided): the chip root becomes a non-interactive <span> and an internal IconButton (icon='cross', size mini) is rendered as the only dismiss trigger. The button:
    • carries an accessible name via the new dismissLabel prop (default: "Dismiss");
    • is natively keyboard-operable (Tab + Enter/Space);
    • calls preventDefault() on mousedown so it never steals focus from multiselect/autocomplete anchor inputs;
    • honors disabled.
    • onClick/onMouseDown, when passed alongside onDismiss, behave as ordinary handlers on the chip body — they no longer mean "dismiss".
  • Legacy mode (no onDismiss): rendering is byte-for-byte identical to the current release — root <button class="rcx-box rcx-chip">, whole-chip click dismisses, decorative cross icon. This behavior is now documented as deprecated (prose in the component JSDoc and an @deprecated tag on renderDismissSymbol only, so the component itself is not flagged as deprecated at call sites).
// Before (still works, now deprecated behavior)
<Chip onClick={handleRemove}>Marie Rowe</Chip>

// After
<Chip onDismiss={handleRemove}>Marie Rowe</Chip>

Backward compatibility

This is intentionally a non-breaking minor release (changeset included). We audited all 11 direct Chip usages in the main Rocket.Chat repository before deciding on this design:

  • All of them pass onClick as the removal/selection handler → the legacy path keeps that contract untouched.
  • Existing snapshot tests in Rocket.Chat record <button class="rcx-chip"> as the chip root → legacy DOM is unchanged.
  • Tests using screen.getByRole('button') on legacy chips keep passing.
  • No consumer uses button-only HTML attributes (form, type, name), so the type surface remains compatible.

Internal consumers migrated

AutoComplete, MultiSelect/SelectedOptions, and PaginatedMultiSelect now use onDismiss, so every product built on these pickers gets the improved dismiss behavior automatically by upgrading fuselage — no code changes required downstream.

Notes on the migration:

  • AutoComplete: removal no longer relies on event.currentTarget.value (which would resolve to the inner IconButton after the change); a removeValue(value) helper is used instead. The public onRemove contract of renderSelected is untouched.
  • SelectedOptions: keeps its external prop surface (onMouseDown) and maps it internally to onDismiss, so MultiSelect needed no changes.
  • PaginatedMultiSelect: kept role='option'/tabIndex={-1} on the chips — they implement the ARIA listbox pattern (parent role='listbox') and are unrelated to dismiss focusability.

Storybook

New/updated stories cover every combination: Default, With Thumb Url, Legacy Dismissible (deprecated), Dismissible, Custom Dismiss Label, Disabled (both modes side by side), and With Thumb. Docs and argTypes describe the new props and flag the deprecated ones.

How it was verified

  • Unit tests: 40/40 passing across the affected suites (Chip, AutoComplete, MultiSelect, Options), plus 11/11 on PaginatedSelect/PaginatedMultiSelect. New Chip specs assert: body click does not dismiss, IconButton click dismisses exactly once, accessible name (default and custom), disabled blocks the button, and onClick coexists with onDismiss without triggering dismissal.
  • Static checks: eslint (0 errors), stylelint, prettier, tsc --noEmit, and the d.ts build (tsc -p tsconfig.build.json --emitDeclarationOnly) all clean.
  • Manual verification in Storybook (live browser):
    • Clicking the chip body logs no action; clicking the ✕ logs dismiss — trigger confined to the button.
    • In MultiSelect, removing a selected option via its ✕ removes only that option and the anchor keeps focus (no dropdown flicker), confirming the mousedown-preventDefault guard works.
    • Legacy story renders identically to the previous release.
    • The one a11y-addon violation shown on Chip stories (label color contrast 4.39:1) is pre-existing — it reproduces identically on the legacy story and comes from the current chip color tokens, untouched here.

Why a span root instead of nesting buttons

A <button> inside a <button> is invalid HTML and unreliably handled by browsers, so the dismissible variant's root must stop being a button. That structural change is exactly what's gated behind the opt-in onDismiss prop to keep existing consumers safe.

🤖 Generated with Claude Code

…iss IconButton

Chip's dismiss action used to be triggered by clicking anywhere on the
chip (the whole root was a <button>). The new opt-in onDismiss prop
renders a non-interactive <span> root with an internal IconButton
(cross, mini) as the only dismiss trigger, improving click precision
and accessibility. The legacy whole-chip dismiss path is preserved
byte-for-byte and documented as deprecated. Internal consumers
(AutoComplete, MultiSelect/SelectedOptions, PaginatedMultiSelect) are
migrated to onDismiss.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Aug 6, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ab6d7fc

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@rocket.chat/fuselage Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

onMouseDown,
}: SelectedOptionsProps) {
return (
<Chip tabIndex={tabIndex} onDismiss={onMouseDown}>

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.

only the IconButton should be focusable

}
{...(spanRest as unknown as Record<string, unknown>)}
>
<Margins all='x4'>

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.

looking at the rendered element it looks odd. Maybe we can add a gab in the style of the Chip, instead of keeping this Margin

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants