shared Badge: disabled without onClick now renders a real disabled button

argus caught this reviewing PR #4110: Badge only applied the disabled
prop inside its onClick-present branch (a real <button disabled>).
When onClick is undefined -- exactly the case every current caller
hits when its gating condition is false, since onClick and disabled
are computed off the same condition -- it fell through to a plain
<span> that never reads disabled and never gets the
.ui-badge-interactive class the dimming CSS depends on. WantedMenu
and the matrix-account trigger both silently lost the disabled
affordance to this.

Fixed at the root: both the button-vs-span branch and the
interactive-styling class now key off disabled || onClick (extracted
to one interactive flag) instead of onClick alone.

Verified: tsc --noEmit clean on both swarm-ui and agent (Badge's two
consumer packages), nix fmt clean. Screenshot against a throwaway
mock roster (deleted before this commit, never tracked) showing a
hiveless row and a destroyed row both visibly dimmed now, next to an
enabled row at full opacity -- previously all three looked identical.
This commit is contained in:
iris 2026-09-08 14:00:07 +02:00
commit 3ff61e49b8

View file

@ -51,15 +51,27 @@ export interface BadgeProps {
/** Decorative glyph before the label; `aria-hidden`, the text is still the real label. */
icon?: ComponentChildren;
/**
* Present renders a `<button>` (real control, min touch target),
* `aria-expanded` reflecting `expanded`. Absent renders a plain
* `<span>` (status display only, e.g. "alive"/"idle 6m"). No visible
* disclosure caret (mara: "remove the chevrons") `hover` +
* `aria-expanded`'s outline/bg (Badge.css) are the only affordance.
* Present (or `disabled` is true see below) renders a `<button>`
* (real control, min touch target), `aria-expanded` reflecting
* `expanded`. Both absent renders a plain `<span>` (status display
* only, e.g. "alive"/"idle 6m"). No visible disclosure caret (mara:
* "remove the chevrons") `hover` + `aria-expanded`'s outline/bg
* (Badge.css) are the only affordance.
*/
onClick?: (e: MouseEvent) => void;
/** Only meaningful with `onClick` — drives `aria-expanded`. */
expanded?: boolean;
/**
* Renders as a real `<button disabled>` even with no `onClick`
* every current caller computes both off the same gating condition
* (`onClick={x ? fn : undefined}`, `disabled={!x}`), so `onClick`
* going `undefined` is exactly when this is `true`. A `disabled`
* badge still means "there's an action here, not right now," which
* is button semantics + the dimming CSS below, not a plain
* non-interactive span (argus, filed as its own issue
* `WantedMenu`'s and `LinkMatrixAccountForm`'s trigger, both of which
* silently lost the dimmed affordance to this exact gap).
*/
disabled?: boolean;
class?: string;
title?: string;
@ -77,11 +89,17 @@ export function Badge({
class: extraClass,
title,
}: BadgeProps) {
// `disabled` alone still means "button, just not clickable right
// now" — see the prop's own doc above for why this can't just check
// `onClick`. Both the branch below and the interactive-styling class
// key off this one derived flag, not off `onClick` separately, so
// they can no longer drift apart the way they used to.
const interactive = Boolean(onClick) || Boolean(disabled);
const classes = [
"ui-badge",
`ui-badge-${tone}`,
variant === "quiet" && "ui-badge-quiet",
onClick && "ui-badge-interactive",
interactive && "ui-badge-interactive",
extraClass,
]
.filter(Boolean)
@ -97,7 +115,7 @@ export function Badge({
<span class="ui-badge-value">{value}</span>
</>
);
if (onClick) {
if (interactive) {
return (
<button
type="button"