Repository navigation
docs(spec): Badge records its shipped contract as current - #8263
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
PR Analysis Report
The exact-head preview is not available yet. Generated by PR Enrichment workflow | View current CI run |
There was a problem hiding this comment.
This record matches Badge's shipped behavior for default usage, but as a new current contract it needs an engineering owner's decision before it governs: an engineering owner records it by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner. Independently of that decision, three corrections are needed:
- AR2 says tone color is never the only carrier of status, but the record allows an empty
label, which renders a color-only badge with nothing in the accessibility tree; describe what Badge itself does and leave a non-empty label to the callsite. - AR1 says the root carries no role and is not focusable, while FR7 passes a caller's
roleandtabIndexthrough; say instead that Badge adds none of its own. - The verification map maps FR5 (the
neutraldefault and tone paint) and FR6's unthemed fallback to tests that would not fail if those changed; narrow those rows and name the gaps.
[Automated review]
|
|
||
| ## Accessibility contract | ||
|
|
||
| - **AR1 — Static text.** The root carries no role, is not focusable, and exposes |
There was a problem hiding this comment.
AR1 says the root carries no role and is not focusable, but FR7 sends supported ARIA props to the root and BaseProps keeps role and tabIndex, so <Badge role="status" tabIndex={0} label="Syncing" /> is a focusable live region. Suggested wording: "Badge adds no role, tabIndex, or live region of its own; caller ARIA reaches the root under FR7."
| - **AR1 — Static text.** The root carries no role, is not focusable, and exposes | ||
| its label as ordinary text in reading order. Badge creates no live region, so | ||
| a changed label is not announced. | ||
| - **AR2 — Meaning in text.** The required label carries the badge's meaning; |
There was a problem hiding this comment.
As written, AR2 promises the tone is never the only carrier of status, but the Label row allows an empty string, and <Badge variant="error" label="" /> renders a color-only pill with nothing in the accessibility tree. Consider stating what Badge does and leaving a non-empty label to the callsite (Badge.doc.mjs already says to always include a text label), or listing the empty-label case under Current-state impact.
|
|
||
| | Contract | Verification | Representative states | Mutation or failure expectation | Audit section | | ||
| | ------------- | --------------------------------------------------- | -------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ---------------------- | | ||
| | FR2, FR5, FR7 | `Badge.test.tsx` | Default, semantic and color tones, icon, ref | Dropping the icon, the ref, a passthrough prop, or variant reflection fails DOM assertions. | `audit:Badge/behavior` | |
There was a problem hiding this comment.
No assertion in Badge.test.tsx fails if the default stops being neutral or a tone's paint changes: the tone tests only call getByText, and data-variant is checked only for purple and success. FR2's icon-before-label order isn't asserted either. Suggest narrowing this row to what the tests prove and naming FR5 and FR2's order as verification gaps.
| | FR2, FR5, FR7 | `Badge.test.tsx` | Default, semantic and color tones, icon, ref | Dropping the icon, the ref, a passthrough prop, or variant reflection fails DOM assertions. | `audit:Badge/behavior` | | ||
| | FR4, AR4 | `Badge.test.tsx` full-label suite | String, number, rich, and empty labels | Setting `title` from a rich or empty label, or dropping it for a string or number, fails. | `audit:Badge/a11y` | | ||
| | FR1, FR3 | `Badge.stories.tsx` long-label fixture in a browser | Long labels in tight space | A label that wraps or escapes the badge is visible in the rendered fixture. | `audit:Badge/layout` | | ||
| | FR6 | `extensibleAxes.test.ts`, `themingTargets.test.ts` | `BadgeVariantMap`, `badge` target | Removing `variant` from the target reflection or the doc's `visualProps` fails the structural checks. | `audit:Badge/theming` | |
There was a problem hiding this comment.
These structural tests say in their own header that they can't judge a fallback, and the verification for architecture:component-theming-surface INV14 also names a focused no-match fallback test, which Badge doesn't have yet. Suggest naming FR6's fallback sentence as a verification gap in this row.
There was a problem hiding this comment.
The AR1, AR2 and verification-map updates resolve the earlier corrections, and the record matches Badge's shipped behavior, but as a new current contract it still needs an engineering owner's decision before it governs: an engineering owner records it by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner. Independently of that decision, two record corrections remain:
- The verification map has no row for AR1–AR3 and covers only the
titlehalf of AR4; name their evidence or the gap (none of the listed checks fails if Badge adds its own role, tab stop, live region or icon semantics), and note that FR2's icon-before-label order is untested. - The Review checks cell still says to reject "an interactive or focusable badge", while AR1 now lets a caller
roleortabIndexthrough; reword it to "a badge that adds its own role, tab stop, or live region".
[Automated review]
| | End-user impact | Readers see a compact status or category label that stays inside its container; assistive technology reads the full label text. | | ||
| | Builder impact | None; the record describes shipped behavior. | | ||
| | Compatibility/readiness | Released behavior recorded as current; no API, default, or output changes. One implementation gap is listed under Current-state impact. | | ||
| | Review checks | Reject a badge that wraps or escapes its container, a `title` derived from a rich label, an interactive or focusable badge, or a variant that is not reflected on `badge`. | |
There was a problem hiding this comment.
This projection still says to reject "an interactive or focusable badge", but AR1 (lines 146–148) now lets a caller role or tabIndex through under FR7, so the cell adds a rule the body does not have. Suggested wording: "a badge that adds its own role, tab stop, or live region".
| | FR2, FR7 | `Badge.test.tsx` | Icon, ref, passthrough | Dropping the icon, the ref, or a passthrough prop fails DOM assertions. | `audit:Badge/behavior` | | ||
| | FR5 | `Badge.test.tsx` `data-variant` assertions; `Badge.stories.tsx` variant fixtures in a browser | `success` and `purple` reflection; every tone rendered | A lost `data-variant` for those tones fails. Gap: no automated check covers the `neutral` default's reflection or any tone's paint; a paint change is visible only in the rendered fixtures. | `audit:Badge/theming` | | ||
| | FR4, AR4 | `Badge.test.tsx` full-label suite | String, number, rich, and empty labels | Setting `title` from a rich or empty label, or dropping it for a string or number, fails. | `audit:Badge/a11y` | | ||
| | FR1, FR3 | `Badge.stories.tsx` long-label fixture in a browser | Long labels in tight space | A label that wraps or escapes the badge is visible in the rendered fixture. | `audit:Badge/layout` | | ||
| | FR6 | `extensibleAxes.test.ts`, `themingTargets.test.ts` | `BadgeVariantMap`, `badge` target | Removing `variant` from the target reflection or the doc's `visualProps` fails the structural checks. Gap: no test renders a theme-added value, so the unthemed fallback is verified by source review only. | `audit:Badge/theming` | | ||
| | FR8, PR1 | `serverSafeComponents.test.ts` | Import graph | Adding a hook or a client-only import to Badge fails the server-safety invariant. | `audit:Badge/behavior` | |
There was a problem hiding this comment.
AR1–AR3 have no row in this map, and the FR4/AR4 row fails only when the title changes. None of the listed files fails if Badge adds its own role, tab stop, live region or icon semantics, or truncates the label text, so consider a row such as "AR1–AR3, AR4 DOM text | source review | Gap: no listed check fails if Badge adds its own role, tab stop, live region or icon semantics", and name FR2's icon-before-label order as a gap in the first row.
There was a problem hiding this comment.
This record matches Badge's shipped behavior and the corrections from the earlier rounds are in, so nothing else needs to change in it. As a new current contract it still needs an engineering owner's decision before it governs: an engineering owner records it by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner. Open #8236 changes the badge height and minimum width that FR1 and FR3 pin, so whichever of the two lands second should reconcile them.
[Automated review]
|
/approve-spec 7c16ab1 |
Exact decision needed
Record Badge's shipped behavior as a
currentcomponent contract (component:Badge). No behavior, API, or output change.Existing authority searched
pnpm specs:for packages/core/src/Badge/Badge.tsx: only directory-wide architecture records apply. The record linksarchitecture:public-component-api(INV5, INV6, INV8: passthrough, styling inputs, ref) andarchitecture:component-theming-surface(INV2, INV6, INV14: target, state reflection, extensible axis), the two whose clauses it relies on. No family or design record names Badge.Badge.spec.mdexisted on main.Decision
One record,
packages/core/src/Badge/Badge.spec.md(authority: current,approved_by: cixzhang,approved_at: 2026-10-24). It owns:titlederived from a string or number label (FR4);badge, and the unthemed fallback of a theme-added variant (FR5, FR6);Rejected alternatives
None.
Claim scope and non-goals
Covers only Badge as shipped on main. Product choice of status/category and wording, the supplied icon's exposure (
component:Icon), sibling presentations (component:Token,component:StatusDot), and theme token contrast stay with their owners. The theming-anatomy JSON block is omitted becauseBadge.doc.mjsanatomy lists no root part to carry thebadgetarget; the record states the target mapping in prose. No new behavior is decided; OQ1 stays open.Evidence and implementation relationship
Badge.tsxbase and label styles (inline-flex,--spacing-5height,--spacing-2inline padding,--radius-full, supporting type, medium weight,nowrap,maxWidth: 100%,minWidth: 0, labeloverflow: hidden+text-overflow: ellipsis);Badge.stories.tsx"Long labels in tight space" fixture.Badge.tsxrender order (icon, then labelspan,gap: --spacing-1);Badge.test.tsx"renders with icon".Badge.tsxlabelTitle;Badge.test.tsxfull-label suite (string, number, rich, empty).Badge.tsxvariant styles andthemeProps('badge', {variant});index.tsBadgeVariantMap;Badge.test.tsxdata-variantassertions;extensibleAxes.test.tsandthemingTargets.test.tsstructural checks. A theme-added value indexes no entry in the variant style map, so only the base styles apply.Badge.tsxmergeProps(themeProps, stylex.props(base, variant, xstyle), className, style)thentitle, then...props;Badge.test.tsxref and passthrough tests.Badge.tsx;serverSafeComponents.test.tslists Badge as server-safe by import graph.spanwith no role,tabIndex, or live region; the icon is rendered as supplied.Current-state impact
titleon pointer hover; keyboard and touch users cannot reveal it (Badge: full label should be reachable by focus and on touch, and only when it is actually clipped #5585). Assistive technology receives the full DOM text.Writing rubric results
#+ digits,/pull/,/issues/).Scope
titleattribute andbadgetarget.Validation
pnpm check:knowledge: alignedgit diff --check: clean