feat(fuselage): Add onDismiss and size props to Chip rendering a dedicated dismiss IconButton - #2145
Open
jeanfbrito wants to merge 13 commits into
Open
feat(fuselage): Add onDismiss and size props to Chip rendering a dedicated dismiss IconButton#2145jeanfbrito wants to merge 13 commits into
onDismiss and size props to Chip rendering a dedicated dismiss IconButton#2145jeanfbrito wants to merge 13 commits into
Conversation
🦋 Changeset detectedLatest commit: edec7bc The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
dougfabris
requested changes
Aug 6, 2026
ivans-netto
force-pushed
the
feat/chip-dismiss-icon-button
branch
from
August 10, 2026 14:22
ab6d7fc to
6ec0482
Compare
onDismiss prop to Chip rendering a dedicated dismiss IconButtononDismiss and size props to Chip rendering a dedicated dismiss IconButton
ivans-netto
added a commit
that referenced
this pull request
Aug 10, 2026
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… Chip label The previous font token (secondary-info) rendered at 4.39:1 against the chip's secondary background, failing WCAG AA; button(on-secondary) is the matching foreground (12.71:1 light, 9.08:1 dark). The label also moves from the p2 to the p2m font scale — same 14px/20px style at medium weight, sourced from the typography tokens instead of a hardcoded font-weight. Disabled states keep their dedicated tokens. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When onDismiss is provided, the chip root becomes a non-interactive <span> and a dedicated, accessible IconButton is the only dismiss trigger: dismissLabel sets its accessible name and tooltip, icon (default 'cross') lets it express other actions (e.g. chevron-down), and size selects the dimensions — medium (28px chip/button, 20px avatar) or small (20px chip/button, 16px avatar). The dismissible chip is spaced by its own CSS (gap + inline-start padding only, button flush against the other edges). The size prop is forwarded internally as chipSize because `size` is a Box styling prop that withBoxStyling would otherwise consume. Legacy mode (no onDismiss) renders byte-for-byte the same DOM as before and is documented as deprecated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ivans-netto
added a commit
that referenced
this pull request
Aug 10, 2026
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ivans-netto
force-pushed
the
feat/chip-dismiss-icon-button
branch
from
August 10, 2026 19:43
696ec85 to
c339d5b
Compare
Main stories showcase the onDismiss chip (Default, Dismissible with default/custom label, Sizes with avatar and icon applications, thumbs, OtherActions, Disabled), each variant captioned via a ChipVariants helper following the Badge stories pattern. The whole legacy scope is isolated in a single deprecated LegacyDismissible story. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AutoComplete, MultiSelect/SelectedOptions and PaginatedMultiSelect now
use the dedicated dismiss IconButton. AutoComplete removal no longer
relies on event.currentTarget.value; SelectedOptions keeps its external
onMouseDown contract, maps it to onDismiss, and stops forwarding
tabIndex so the IconButton is the only focusable element.
PaginatedMultiSelect keeps role='option'/tabIndex={-1} (ARIA listbox).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ivans-netto
force-pushed
the
feat/chip-dismiss-icon-button
branch
from
August 10, 2026 19:46
c339d5b to
80db667
Compare
Regenerated in the Playwright Docker container: Chip stories (label color/weight, flush dismiss button, new Sizes/WithIcon/OtherActions/ LegacyDismissible stories) plus the AutoComplete and MultiSelect stories that render chips. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…Chip `Margins` reaches children two ways, and the dismissible chip was caught by both once it stopped wrapping its content in a `Margins` of its own: - via the `BoxTransforms` context, which pushed a margin onto the inner `Box`-based IconButton and inflated the chip (28px -> 36px inside MultiSelect). The chip now resets that context for its children, the same way `Box` does, since its inner spacing is owned by `gap`. - via a className patched onto its direct children, which `SelectedOptions` was dropping when it destructured its props — losing the spacing between MultiSelect chips. It forwards them again. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
leadingIcon renders an icon before the label as a flex sibling, so the chip centers it vertically and sizes it from `size` (20px on medium, 16px on small). Icons passed inside `children` instead align on the text baseline and always read slightly high. A text-only label also gains a 4px inline-start margin, matching the ~8px visual inset the trailing IconButton's inner padding produces on the other side; a leading avatar or icon keeps the plain 4px padding. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The inner IconButton no longer carries the margin class leaked from the consumer's Margins, and the chip root carries its spacing class again. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Proposed changes (including videos or screenshots)
Jira task: DSN-183 — Review chips (fuselage)
Before / After
Before — the whole chip is a
<button>and any click dismisses; the ✕ is decorative, the label fails AA contrast:After — a dedicated, accessible dismiss
IconButton(default and customdismissLabel):Sizes —
medium(28px) andsmall(20px), with avatar (20px/16px) and leading-icon applications:Other trailing actions — the
iconprop generalizes the trailingIconButtonbeyond dismiss: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
onDismissprop that moves the dismiss trigger to a dedicated, real button:onDismissprovided): the chip root becomes a non-interactive<span>and an internalIconButton(icon='cross') is rendered as the only dismiss trigger. The button:title) via the newdismissLabelprop (default:"Dismiss");preventDefault()onmousedownso it never steals focus from multiselect/autocomplete anchor inputs;disabled.onClick/onMouseDown, when passed alongsideonDismiss, behave as ordinary handlers on the chip body — they no longer mean "dismiss".gap+paddingon.rcx-chip--dismissible) instead of aMarginswrapper. Per design, the chip has no top/bottom/right padding — the dismissIconButtonsits flush against those edges — and 4px inline-start padding. A text-only label gets 4px more, so its ~8px visual inset matches what the dismiss button's inner padding produces on the other side; a leading avatar or icon keeps the plain 4px.sizeprop selects the chip's dimensions:medium(default) renders a 28px dismissIconButton(28px chip) with a 20px default avatar,smalla 20px one (20px chip) with a 16px default avatar.leadingIconprop renders an icon before the label as a flex sibling (vertically centered by the chip, sized bysize), and a newiconprop customizes the trailingIconButtonicon (defaultcross), so the same anatomy can express other actions — e.g.'chevron-down'for a chip that opens a filters menu. Sincesizecollides with the Box styling prop of the same name (consumed bywithBoxStylingbefore reaching the component), the exportedChipextracts it and forwards it internally aschipSize.onDismiss): the DOM 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@deprecatedtag onrenderDismissSymbolonly, so the component itself is not flagged as deprecated at call sites).Accessibility: label contrast fix
The chip label color moved from
font(secondary-info)tobutton(on-secondary)— the foreground token that pairs with the chip'sbutton-secondarybackground. This fixes the WCAG AA failure previously flagged by the a11y addon on every Chip story (4.39:1): now 12.71:1 on the light theme and 9.08:1 on the dark theme. Disabled states keep their dedicated tokens in both modes. Per design, the chip label also moved from thep2font scale top2m(same 14px/20px style, medium weight — from the typography tokens, no hardcodedfont-weight). Since these tokens are shared, legacy chips get both updates too — the only visual changes to the legacy mode (its DOM remains untouched).Fixes found while polishing the layout
Dropping the internal
Marginswrapper exposed two ways a consumer'sMarginsreached the chip, both fixed here:BoxTransformscontext, which pushed a margin onto the innerBox-basedIconButtonand inflated the chip (28px → 36px insideMultiSelect). The chip now resets that context for its children, exactly asBoxdoes, since its inner spacing is owned bygap.SelectedOptionswas dropping when it destructured its props — losing the 4px spacing betweenMultiSelectchips.Verified against
mainin the same viewport: chip height (28px), outer margin (4px) and wrapping behavior now match, with the chip only ~9px wider from the larger dismiss button.Backward compatibility
This is intentionally a non-breaking
minorrelease (changeset included). We audited all 11 directChipusages in the main Rocket.Chat repository before deciding on this design:onClickas the removal/selection handler → the legacy path keeps that contract untouched.<button class="rcx-chip">as the chip root → legacy DOM is unchanged.screen.getByRole('button')on legacy chips keep passing.form,type,name), so the type surface remains compatible.Internal consumers migrated
AutoComplete,MultiSelect/SelectedOptions, andPaginatedMultiSelectnow useonDismiss, 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 onevent.currentTarget.value(which would resolve to the innerIconButtonafter the change); aremoveValue(value)helper is used instead. The publiconRemovecontract ofrenderSelectedis untouched.SelectedOptions: keeps its external prop surface (onMouseDown) and maps it internally toonDismiss.MultiSelectno longer passestabIndex, so the dismissIconButtonis the only focusable element (review feedback), and the remaining props are forwarded so the surroundingMarginscan still space the chips.PaginatedMultiSelect: keptrole='option'/tabIndex={-1}on the chips — they implement the ARIA listbox pattern (parentrole='listbox') and are unrelated to dismiss focusability.Storybook
All main stories now showcase the new
onDismisschip:Default,Dismissible(default and customdismissLabel, captioned side by side),Sizes(medium/small, showing both avatar and leading-icon applications),With Thumb Url,With Thumb,With Icon(leadingIconinstead of an avatar),Other Actions(trailingiconbeyond dismiss: dropdown and edit), andDisabled. The entire legacy (whole-chip dismiss) scope is isolated in a single deprecatedLegacy Dismissiblestory showing every legacy possibility captioned:dismissible,with thumb,disabled, andno handler. Docs and argTypes describe the new props (including thedismissLabeltooltip) and flag the deprecated ones.How it was verified
Chip,AutoComplete,MultiSelect,Options,PaginatedSelect/PaginatedMultiSelect). NewChipspecs assert: body click does not dismiss, IconButton click dismisses exactly once, accessible name and tooltip (default and custom),disabledblocks the button, andonClickcoexists withonDismisswithout triggering dismissal.eslint(0 errors),stylelint,prettier,tsc --noEmit, and thed.tsbuild (tsc -p tsconfig.build.json --emitDeclarationOnly) all clean.dismiss— trigger confined to the button.MultiSelect, removing a selected option via its ✕ removes only that option and the anchor keeps focus (no dropdown flicker), confirming themousedown-preventDefaultguard works. The accessibility tree shows the dismissIconButtons as the only interactive elements inside the chips.Why a
spanroot instead of nesting buttonsA
<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-inonDismissprop to keep existing consumers safe.🤖 Generated with Claude Code