Types
Import these from @grafloria/renderer.
Types
ContainmentAction
Also has every member of String, listed on its own entry.
FOCUS CONTAINMENT — focus must never rest on geometry the user cannot see.
This is a real WCAG failure (2.4.11 Focus Not Obscured / 2.4.7 Focus Visible) and, before this wave, we failed it outright: keyboard focus walked the whole graph while the camera sat still, so tabbing past the edge of the viewport left the focus ring on a node that was scrolled off-screen — announced, but invisible. A sighted keyboard user simply lost the cursor.
The fix, in order of escalation:
- Already fully visible (with padding) → DO NOTHING. Not panning is the most important case: a stationary camera is what makes the diagram readable, and gratuitous re-centring on every focus step is nauseating.
- Off-screen but it FITS at the current zoom → pan the minimum distance that brings it inside the padded box. Minimum, not centring: the user keeps their mental map.
- Too big to fit at the current zoom → zoom OUT via
fitToBounds, which is the existing camera maths. We do not fork it.
Motion honours prefers-reduced-motion: an animated pan becomes an instant
jump. All camera writes go through {@link ViewportController} — this class
owns policy, not geometry.
.
tstype ContainmentAction = 'none' | 'pan' | 'zoom';
Politeness
Also has every member of String, listed on its own entry.
The managed aria-live REGION — the canvas's voice.
Two live regions, not one: an AT will not reliably re-announce a node whose politeness changed, and mixing an error into the polite queue means it waits behind whatever selection chatter is already in flight. So:
polite— selection, movement, focus, connect/disconnect;assertive— validation failures and errors, which interrupt.
DE-DUPLICATION is the whole design problem. A naive live region wired to a render loop says "Process node, 2 connections" sixty times a second and is worse than useless — the user turns it off. Three defences, all here:
- IDENTICAL-MESSAGE SUPPRESSION. The same text in the same channel is
dropped, unless
forceis set (a repeated action the user did repeat — nudging twice — is legitimately worth re-announcing). - COALESCING. Announcements inside a short window replace, rather than queue behind, the previous one. Holding an arrow key produces the FINAL position, not fifty intermediate ones.
- MODEL-CHANGE DRIVEN. The controller is called on model change, never
from the render loop.
getSpeakCount()lets a test prove a quiet frame speaks zero times.
Framework-free: takes a container, owns two divs, no Angular anywhere.
.
tstype Politeness = 'polite' | 'assertive';
Was this page helpful?