Requirement
Every async state change communicates through exactly one of two channels, chosen by what the change is:
- Transient status (saving indicator, success toast, background update): a live
region (
role="status"/aria-live) that announces without moving focus. - Context replacement (a panel replacing the acting surface, an error that needs
action): move keyboard focus to the revealed surface or its first actionable
element. The focused element's content is voiced by the focus move itself — do
NOT also mark it
role="alert", or the same message announces twice.
Never both channels for the same message; never neither.
Rationale
Merges the accessibility checklist AA "Status messages" (announceable without focus) with the
focus-movement gap caught twice in loop runs 1–2 (post-submit focus stranded on a
hidden control; then a banner with role="alert" + programmatic focus announcing
twice). The two failures are one design decision made badly: which channel does this
state change use? This control forces the choice at plan time — CMP-3's state
enumeration is where each state declares its channel.
Passes when
- Toast "Attendance submitted." is
role="status", auto-dismissing, focus untouched. - On submit success that hides the acting button, focus moves to the revealed panel's first actionable element.
- An error banner that requires action receives focus (
tabindex="-1"+.focus()) and carries NOrole="alert"— the focus move is the announcement. - A background auto-save indicator updates a polite live region only.
Fails when
- Focus rests on a hidden/removed control after a state change (stranded focus).
- The same message is announced twice (live-region role AND programmatic focus).
- A state change is visually obvious but silent to AT (no live region, no focus).
- Focus is yanked for a transient message the user didn't act on (focus theft).
How to verify
Deterministic half — planned checks/async-states extension: for each state surface,
assert (live-region role XOR focus-target wiring). Until built: verify manually via
DOM inspection (document.activeElement after each transition) and label it so.
Judgment half — the evaluator grades the channel choice per state: transient vs
context-replacement, and quotes any double-channel message.