feat: add BareButton primitive, migrate role=button sites#7733
Conversation
|
@SahilJat is attempting to deploy a commit to the Flagsmith Team on Vercel. A member of the Team first needs to authorize it. |
There was a problem hiding this comment.
Good job @SahilJat! Two things worth tidying before this lands:
1. Co-locate the styles with the component.
Our newer components keep their SCSS next to them (e.g. SelectableCard.scss, ContentCard.scss, CenteredModal.scss) rather than as a global partial. Could you move this to web/components/base/forms/BareButton.scss, import it from the component (import './BareButton.scss' in BareButton.tsx), and drop the @import 'bare-button' line from web/styles/components/_index.scss?
2. Make the reset zero-specificity.
.bare-btn is (0,1,0) — the same as a single consumer class like .selectable-card. When composed, all: unset ties with the consumer's styles and the winner falls to stylesheet load order (which matters even more once this loads via component import). .page/.chip-icon only survive because they're nested (0,2,0); .selectable-card is a flat class and could get flattened. Wrapping the reset in :where() drops it to (0,0,0) so any consumer class deterministically wins. Keep the state rules at normal specificity so the focus ring still applies:
// Reset at zero specificity so consumer classes always win, regardless of load order.
:where(.bare-btn) {
all: unset;
display: inline-flex;
align-items: center;
cursor: pointer;
box-sizing: border-box;
}
.bare-btn {
&:disabled,
&[aria-disabled='true'] { cursor: default; pointer-events: none; }
&:focus-visible {
outline: 2px solid var(--color-border-action);
outline-offset: 2px;
border-radius: var(--radius-sm);
}
}Also, can you please rebase your PR and fix the conflicts ?
Thanks for submitting a PR! Please check the boxes below:
[X] I have read the Contributing Guide /Flagsmith/flagsmith/blob/main/CONTRIBUTING.md.
[ ] I have added information to docs/ if required so people know about the feature.
[X] I have filled in the "Changes" section below.
[X] I have filled in the "How did you test this code" section below.
Changes
Closes #7626
New BareButton primitive
When we need a clickable surface that doesn't look like a button (stepper steps, selectable cards, list rows, chip-delete icons), the previous options were both problematic:
•
• + per-component all: unset — repeats the reset everywhere, easy to drift
BareButton renders a native
with all browser defaults reset via a single .bare-btn CSS class. It gives focus management, disabled semantics, keyboard handling and screen-reader
role for free.
New files:
• web/components/base/forms/BareButton.tsx — component with forwardRef , accepts all
props• web/styles/components/_bare-button.scss — all: unset + minimal layout primitives + focus-visible outline + disabled cursor
• documentation/components/BareButton.stories.tsx — Storybook stories (Default, Disabled, AsSelectableCard, AsChipDelete)
• web/components/base/forms/tests/BareButton.test.ts — Jest unit test (exports, displayName, forwardRef, prop types)
Migrated all role='button' sites in web/ :
• SelectableCard.tsx —
• ChipInput.tsx — → , added aria-label for accessibility
• Paging.js — 3×
How did you test this code?
• Verified the button renders with no browser default styling (no border, no background)
• Verified Tab focuses the button and shows a focus-visible outline
• Verified Enter and Space activate the click handler
• Verified the Disabled story is non-interactive (no cursor, cannot click or focus)
• Verified the AsSelectableCard and AsChipDelete stories demonstrate correct migration patterns
grep -rn "role='button'" frontend/web/ # no results
grep -rn 'role="button"' frontend/web/ # only the JSDoc comment in BareButton.tsx