T4.5/T4.6/T4.7 - S10/S11/S12: it announces, it is legible, focus is visible

Three small accessibility items, done together because they share one probe and
one measurement method. tests/a11y_check.py, 22 checks, all passing.

S10 — ANNOUNCEMENTS. The app had zero aria-live regions; login.html's
role="alert" / role="status" pair was the only correct example in the codebase.
Both toasts now take an optional kind and set the role BEFORE the text, because
assistive technology announces on the content change and a role applied after
describes the next message rather than this one. The sync badge announces
politely.

Admin banners are handled by a rule rather than by editing thirteen assignment
sites: a MutationObserver in console-util.js marks anything carrying `.bad` as
role=alert and everything else role=status. Thirteen edits is thirteen chances to
get it wrong, and any banner added later would have missed out. The probe checks
a banner created after load, which is the case that would have regressed.

S11 — CONTRAST. Re-measured rather than quoting either published figure, as the
file map asked. #8d8d8d is 3.32:1 on white, not "about 2.9:1" as the plan says;
console.css:103's 3.3:1 was right. On the shaded surfaces it is worse - 3.01:1 on
a success banner. --cds-text-helper (#6f6f6f) clears 4.5:1 on all seven
backgrounds the app actually paints, tightest 4.56:1.

Fixed once, in the token, so all three sheets inherit it. console.css's local
override is gone, as the task requires. Disabled text was repointed to
--cds-text-disabled rather than darkened with everything else: making disabled
text MORE legible makes a disabled control look enabled.

The probe measures against the background actually painted behind each element,
walking ancestors for the first non-transparent one - not an assumed white, which
is how "passes on paper" and "fails on the page" come to disagree.

S12 — FOCUS. An app-wide :focus-visible floor in theme-light.css at zero
specificity, so any component can still draw its own. Filled controls get an
explicit rule at class specificity: a blue ring inside a blue button measures
1.00:1, which is not a subtle problem but no indicator at all. console.css's
inset ring had exactly that defect on button.primary.

`outline: none` is down from six to one, and that one (.wpc-search-input) has its
replacement in the rule above it - the shell rings on :focus-within, and ringing
both would draw two rectangles.

TWO REAL DEFECTS THE PROBE FOUND that reading would not have:

  - .wp-navbtn's ring is white, which is right on the near-black app bar and
    invisible on the creator's white header. Same button, same class, two hosts,
    1.00:1 on one of them.
  - The comment drawer is translated off-screen when closed, and a transform
    moves a thing without removing it from the tab order. Its name field,
    textarea, Add button and close button were all still focusable: a keyboard
    user could tab into a panel they could not see and could not tell they were
    in. Now visibility:hidden while closed, with the transition delayed so it
    still animates both ways.

The probe itself needed three corrections, each of which was a wrong answer
before it was a right one, and each worth knowing:

  - focus emulation must be ON, or :focus-visible never matches in headless and
    every element reports clean - a pass that means nothing.
  - which surface a ring is drawn against depends on the offset the BROWSER uses,
    not the one the stylesheet asked for. Chromium redraws a low-contrast author
    ring in white at offset 0 on a filled control, which is more contrast than was
    requested; measuring that against the parent scores it 1.00:1 and calls a
    correct ring a defect.
  - focus() on a hidden control does nothing, so the probe has to ask whether the
    focus actually landed. A closed drawer still has layout; a bounding box is not
    evidence that anyone can reach it.

Metric 7, aria-live regions: was 0 at wave 0, now 13 role/aria-live sites across
7 files.

browser_check 71/71, f_items 5 FIXED / F6 REPRODUCES.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-15 22:20:57 -05:00
parent 0cce0b4191
commit c024cba844
11 changed files with 536 additions and 16 deletions

View File

@@ -18,7 +18,7 @@
--border-strong: var(--cds-border-strong);
--text: var(--cds-text-primary);
--text-muted: var(--cds-text-secondary);
--text-dim: var(--cds-ui-04);
--text-dim: var(--cds-text-helper); /* S11: was --cds-ui-04 (#8d8d8d, 3.32:1) */
--accent: var(--cds-interactive-01);
--accent-dim: var(--cds-highlight);
--accent-green: var(--cds-support-success);
@@ -168,14 +168,22 @@
font-family: var(--sans);
font-size: 14px;
padding: 9px 12px;
outline: none;
transition: border-color .15s, box-shadow .15s;
width: 100%;
}
/* S12 / BL-013. `outline: none` used to sit in the rule above, replaced on focus
by a 3px --accent-dim glow: #edf5ff against a #ffffff field is 1.05:1, which is
not a visible indicator. The border change and the glow stay as secondary cues;
the ring is the theme's, inset over the control's own edge so it does not shift
the layout of a dense form. */
input:focus, textarea:focus, select:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-dim);
}
input:focus-visible, textarea:focus-visible, select:focus-visible {
outline: 2px solid var(--cds-focus);
outline-offset: -2px;
}
textarea { resize: vertical; min-height: 70px; line-height: 1.5; }
::placeholder { color: var(--text-dim); }
@@ -416,11 +424,21 @@
same collision with the layers swapped. --rail-top is the header's measured
height, set by wp-creation-app.js:1328 and already used by .wp-nav for exactly
this — reusing it keeps one definition of "below the header". */
/* S12 / C1: `visibility:hidden` while closed, not just translated off-screen.
A transform moves a thing; it does not remove it from the tab order. The
closed drawer's name field, its textarea, its Add comment button and its ✕
were all still focusable, so a keyboard user tabbing through the form fell
into a panel they could not see and could not tell they were in. Found by
tests/a11y_check.py, which measured a focus ring on a control no sighted
user could be looking at.
The transition delays visibility to the end of the slide when closing, and
applies it immediately when opening, so the panel still animates both ways. */
.cmt-drawer { position:fixed; top:var(--rail-top,48px); right:0; height:calc(100vh - var(--rail-top,48px));
width:380px; max-width:92vw; background:var(--surface);
border-left:1px solid var(--border); box-shadow:var(--shadow-lg); transform:translateX(100%);
transition:transform .24s ease; z-index:61; display:flex; flex-direction:column; }
.cmt-drawer.open { transform:translateX(0); }
visibility:hidden;
transition:transform .24s ease, visibility 0s linear .24s; z-index:61; display:flex; flex-direction:column; }
.cmt-drawer.open { transform:translateX(0); visibility:visible; transition:transform .24s ease, visibility 0s; }
.cmt-head { display:flex; align-items:center; justify-content:space-between; padding:16px 18px;
border-bottom:1px solid var(--border); }
.cmt-title { font-weight:700; font-size:14px; color:var(--text); }
@@ -773,7 +791,9 @@
width: 100%; padding: 7px 10px; font: inherit; font-size: 13px;
border: 1px solid var(--border); border-radius: 6px; background: var(--surface); color: var(--text);
}
.wp-nav-search:focus { outline: none; border-color: var(--accent); }
/* S12: keeps the border cue, drops the bare `outline: none`; the theme's ring
applies on keyboard focus. */
.wp-nav-search:focus { border-color: var(--accent); }
/* -- package rows -- */
.wp-nav-list { flex: 1 1 auto; overflow-y: auto; overflow-x: hidden; padding: 0 8px 14px; }