rastrillo / fiesta Public

Show a disabled seg-tabs button as one: faint, default cursor, no hover

A disabled seg-tabs button kept the pointer cursor, the enabled tab's
text colour and its hover, so it read as a choice that would work and
a click did nothing. It now takes the faint text a disabled page chip
or calendar day has and the default cursor a disabled rst-btn has,
which is the convention Fiesta already keeps (it uses no not-allowed
cursor anywhere). The rule comes after the hover and the thumb, which
weigh the same, so a hovered or pressed disabled button stays faint.

The rule is written with the child combinator, like the other button
rules, and its two selectors are exact newOnOldMarkup entries. The
promise gates' grammar admits :disabled beside :hover and :active: a
state only narrows a compound and never stands for a name, and two
grammar rows hold that (a disabled button under a new name passes, one
under only 1.0 names is refused for lacking a positive requirement).

TestASegTabButtonLooksLikeASegTabLink gains a disabled button in its
track and checks, in every theme, scheme and pointer, that its text is
the faint colour (with a control that faint differs from a tab's rest
colour), its cursor is default, and a forced hover and press leave it
faint. Red by assertion before the rule in all 12 legs; with the rule
moved before the hover, only the hover assertion fails.

Size, gzip level 6 (+14 here; the buttons' change is +107 of 140):
  fiesta.css        23,239 -> 23,253 (+14)
  fiesta-plain.css  22,524 -> 22,538 (+14)
  fiesta-signal.css 22,636 -> 22,650 (+14)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 8c3b9a0f31cc902d8cfc10255be84dd062b299eb parent c1bb9ae
4 files changed, +45 −4
  • check/baseline_test.go +2 −0
  • check/segtabs_browser_test.go +31 −1
  • check/selectors_test.go +6 −3
  • css/tokens.css +6 −0

CI failed — run details

CI log
=== gofmt ===
=== no-rastrillo ===
go test -run '^(TestFiestaNeverImportsRastrillo|TestThePublicPackagesLinkNoBrowser)$' -count=1 .

amadan: step no-rastrillo failed: signal: terminated

amadan: job failed at step no-rastrillo — its output is above, not at the tail of this log
diff --git a/check/baseline_test.go b/check/baseline_test.go
index b138c45..0209ed3 100644
--- a/check/baseline_test.go
+++ b/check/baseline_test.go
@@ -135,6 +135,8 @@ var newOnOldMarkup = map[string]string{
".rst-seg-tabs > button": segTabButton,
".rst-seg-tabs > button:hover": segTabButton,
`.rst-seg-tabs > button[aria-pressed="true"]`: segTabButton,
+ "[rst-seg-tabs] > button:disabled": segTabButton,
+ ".rst-seg-tabs > button:disabled": segTabButton,
}
// segTabButton is each entry's why. Every entry uses the child
diff --git a/check/segtabs_browser_test.go b/check/segtabs_browser_test.go
index e4ae9db..3b6f059 100644
--- a/check/segtabs_browser_test.go
+++ b/check/segtabs_browser_test.go
@@ -21,11 +21,13 @@ import (
)
// segTabsTrack is one track holding the current link, a plain link,
-// the pressed button and an unpressed one, so each button is read
+// the pressed button, an unpressed one and a disabled one, so each
+// button is read
// beside the link it must look like, under the same rules.
const segTabsTrack = `<main rst-page><div rst-seg-tabs role="group" aria-label="View">` +
`<a id="current" href="#list" aria-current="page">List</a><a id="link" href="#grid">Grid</a>` +
`<button id="pressed" type="button" aria-pressed="true">List</button><button id="unpressed" type="button" aria-pressed="false">Grid</button>` +
+ `<button id="disabled" type="button" aria-pressed="false" disabled>Map</button>` +
`</div></main>`
// segTabsPages serves the track at /<theme>/<scheme>/ under tokens.css
@@ -120,6 +122,7 @@ func TestASegTabButtonLooksLikeASegTabLink(t *testing.T) {
}
forceState(t, ctx, "#link")
forceState(t, ctx, "#unpressed")
+ disabledLooksIdle(t, ctx, where, rest)
}
}
cancel()
@@ -152,3 +155,30 @@ func TestASegTabButtonLooksLikeASegTabLink(t *testing.T) {
mustRun(t, ctx, emulation.SetEmulatedMedia())
}
}
+
+// disabledLooksIdle: a disabled button says it is not listening, as
+// Fiesta's other disabled controls do: faint text, the default cursor,
+// and no answer to a hover. With the enabled tab's colour, pointer and
+// hover it read as a choice that would work, and a click did nothing.
+// rest is a plain link's resting colour, which the faint text must not
+// be, or the check below could not tell the two apart.
+func disabledLooksIdle(t *testing.T, ctx context.Context, where, rest string) {
+ t.Helper()
+ var faint string
+ at(t, ctx, `(() => { const p = document.createElement("span"); p.style.color = "var(--rst-text-faint)"; document.body.append(p); const c = getComputedStyle(p).color; p.remove(); return JSON.stringify(c); })()`, &faint)
+ faint = canonColours(faint)
+ if faint == rest {
+ t.Errorf("%s: CONTROL: the faint text colour is %s, a plain tab's own; a disabled button cannot be told from an enabled one by colour", where, faint)
+ }
+ if c := computed(t, ctx, "#disabled", "color"); c != faint {
+ t.Errorf("%s: a disabled button's text is %s, want the faint text colour %s", where, c, faint)
+ }
+ if c := computed(t, ctx, "#disabled", "cursor"); c != "default" {
+ t.Errorf("%s: a disabled button's cursor is %q, want \"default\": a pointer promises a click that does nothing", where, c)
+ }
+ forceState(t, ctx, "#disabled", "hover", "active")
+ if c := computed(t, ctx, "#disabled", "color"); c != faint {
+ t.Errorf("%s: a hovered, pressed disabled button turns %s; it must stay %s", where, c, faint)
+ }
+ forceState(t, ctx, "#disabled")
+}
diff --git a/check/selectors_test.go b/check/selectors_test.go
index 0d6b332..d3cf3d7 100644
--- a/check/selectors_test.go
+++ b/check/selectors_test.go
@@ -21,9 +21,10 @@ import (
// - an alternative is compounds joined by descendant, > or +;
// - a compound is an optional type selector, then classes, attribute
// selectors ([a], [a="v"], [a~="v"]), the pseudo-classes :hover,
-// :active, :focus-visible, :first-child, :last-child,
+// :active, :focus-visible, :disabled, :first-child, :last-child,
// :nth-child(An+B), :has() and :not(), and last ::before or
-// ::after;
+// ::after (a state like :disabled only narrows, and never stands
+// for a name);
// - :is(), :where(), * and anything else are outside it, except *
// inside a :not() argument, where it can only narrow.
@@ -333,7 +334,7 @@ func (p *selParser) attr() (attrSel, error) {
func (p *selParser) pseudo(inNot bool) (pseudo, error) {
ps := pseudo{Name: strings.ToLower(p.ident())}
switch ps.Name {
- case "hover", "active", "focus-visible", "first-child", "last-child":
+ case "hover", "active", "focus-visible", "disabled", "first-child", "last-child":
if p.peek() == '(' {
return ps, p.errorf("`:%s(` is outside the grammar", ps.Name)
}
@@ -566,7 +567,9 @@ func TestOnlyNewMarkupPassesTheGrammar(t *testing.T) {
{`.rst-\73nippet`, false, "escape"},
{`[rst-dropdown~=""]`, false, "matches nothing"},
{`.rst-page[rst-row]`, false, "positively requires"},
+ {"[rst-page] button:disabled", false, "positively requires"},
{"[rst-snippet]", true, ""},
+ {"[rst-snippet] > button:disabled", true, ""},
{"[rst-page-foot]:not(:has(> *))", true, ""},
{`[rst-row]:has(> [rst-status~="inline"]) > [rst-status]`, true, ""},
{"[rst-page-foot] [rst-dropdown]", true, ""},
diff --git a/css/tokens.css b/css/tokens.css
index 35002fd..0c6a3f0 100644
--- a/css/tokens.css
+++ b/css/tokens.css
@@ -2147,6 +2147,12 @@ small.rst-field__hint, small[rst-field-hint] { display: block; font-size: var(--
made whichever element made it.
No press rule, because a pressed tab has no fill today (A15). */
.rst-seg-tabs a[aria-current], [rst-seg-tabs] a[aria-current], .rst-seg-tabs > button[aria-pressed="true"], [rst-seg-tabs] > button[aria-pressed="true"] { background: var(--rst-surface); box-shadow: inset var(--rst-hl-x) 1px 0 var(--rst-depth-hl, transparent), var(--rst-shadow-lift); color: var(--rst-text); font-weight: 600; }
+/* A disabled button is not listening: faint text, as a disabled page
+ chip or calendar day has, and the default cursor, as a disabled
+ rst-btn has. It comes after the hover and the thumb, which weigh the
+ same, so neither a hover nor a pressed state brings back the colour
+ of a tab that would answer a click. */
+.rst-seg-tabs > button:disabled, [rst-seg-tabs] > button:disabled { color: var(--rst-text-faint); cursor: default; }
/* form layout — the interview rhythm and the save bar. */
.rst-form-flow > .rst-field + .rst-field, [rst-form-flow] > [rst-field] + [rst-field] { margin-top: var(--rst-sp-5); }