feat(toegankelijkheid): keuzemenu-blokken met het toetsenbord bedienbaar (#1162) #1550

Merged
brenno merged 7 commits from keuzemenu-toetsenbord into main 2026-08-18 10:45:48 +00:00
Owner

Wat dit oplost

De blokken en categoriepillen van een keuze-menudia (#1162) waren een kale GestureDetector over een getekende kaart: geen knoprol, geen focus, geen toetsenbordroute. Wie met een klikker presenteert of geen muis kan gebruiken, kwam een menudia niet door — en dat is nu juist het diatype dat over navigeren gaat (WCAG 2.1.1). Het stond als bekend gat in docs/ACCESSIBILITY.md; die tekst beschrijft nu wat het dóét.

Toets Doet
Tab / Shift+Tab langs de categorieën, dan de blokken in leesvolgorde
Enter · · spatie volgt de sprong, of wisselt van categorie
Escape geeft de toetsen terug aan de dia

Enter en spatie zijn in de presentator óók "volgende dia". Dat botst niet: een toetsaanslag gaat eerst naar het onderdeel met de focus. De pijltjestoetsen blijven van de presentatie — die onderscheppen zou betekenen dat je met de focus op een blok niet verder kunt. Een schermlezer krijgt de knoprol met de uitleg achter het label (Prijzen. Wat het kost), en de tekst ín de kaart is uitgesloten zodat er niets dubbel klinkt. Een tekstblok zonder doel blijft bewust een gewoon blok, geen knop.

Eén gedeelde widget voor kaart, schijf en pil: drie kopieën van focus-, toets- en semantiekafhandeling zijn drie plekken waar er één achterblijft, en juist bij toegankelijkheid kijkt daar niemand naar tot iemand het nodig heeft.

Vier keuringsrondes op één focusring

De gedragsproeven stonden na de eerste commit groen. De beeldkeuring vond daarna in vier rondes vijf echte fouten, waarvan ik er drie zélf had geïntroduceerd tijdens het repareren van de vorige. Ze staan hier omdat ze samen één les zijn.

  1. De ring was een dichte plaat. De halo was een BoxShadow zonder vervaging — dat tekent een gevulde vorm, en in de voorgrond zonder achtergrondkleur overschilderde die het gefocuste blok volledig. Label, uitleg en pijl weg, precies op het blok dat de presentator moet kunnen lezen.
  2. De ring at het blok op. Border.all tekent bínnen de doos, en die ruimte was nergens gereserveerd. De ring is een fractie van de diabreedte terwijl het blok krimpt met het aantal blokken: bij zestien blokken at een ring van 19 px een schijf van 84 px op. Nu hangt hij in een Stack met Clip.none buiten het blok — de layout wordt niet geraakt.
  3. Het accent was onzichtbaar. Bij dat herschrijven raakte de tekenvolgorde omgedraaid: beide banden stonden op een achtergrond-decoratie, die schildert vóór haar kind uit, dus de brede contrastlijn dekte de smalle accentband af. Eenkleurige donut.
  4. De ring liep over de buurschijven. Twaalf schijven op 1280 staan 98 px uit elkaar met een doorsnede van 84 — 15 px lucht tegen een ring van 19. Mijn commentaar bewéérde dat de tussenruimte ruimer was dan de ring dik is; dat had ik aangenomen, niet gemeten. De dikte volgt nu uit die lucht. Plus: bij 1920 werd de bovenste schijf vlak afgeknipt (Clip.hardEdge).
  5. De tweede kleur viel weg bij het standaardprofiel. In LibreKAT zijn tekst- en accentkleur allebei #003399. De eerste reparatie ("neem de verste van tekst en achtergrond, op luminantie") loste dat op maar was systematisch scheef: in een deugdelijk thema ligt de achtergrond per definitie aan het uiterste van het luminantiebereik, dus die wint bijna altijd — en dat is precies de kandidaat die géén inkt zet. Drie van de vier meegeleverde profielen verloren zo hun tweede band. Nu is de tekstkleur de eerste keus, met de achtergrond als terugval bij een echte botsing, en de maatstaf is het grootste kanaalverschil in plaats van luminantie (rood naast teal is een duidelijke tweede band terwijl hun helderheid vrijwel gelijk is).

De les, en wat eraan is gedaan

Elke ronde stonden mijn proeven groen, en elke ronde toetsten ze het verkeerde: of Tab focust, of Enter springt, of er een rand is, of de twee kleuren verschillen. Geen ervan keek naar wat er werkelijk op de dia stond. Wat er nu wordt gemeten:

  • de ring is een omtrek — geen color, gradient of boxShadow op een ringdecoratie;
  • hij omsluit het label in plaats van eroverheen te liggen;
  • focussen verschuift niets op de dia (gemeten op een ring van zestien schijven);
  • er staan twee kleuren en het accent ligt bovenop;
  • de ringdikte past in de lucht tussen twee buren, narekend van twee tot dertig schijven;
  • de tweede band zet inkt op elk meegeleverd profiel — de achtergrond mag alleen worden gekozen als de tekstkleur echt niet kan.

Elke reparatie is één keer rood gezien tegen de ongerepareerde code, met een kopie van het bestand in plaats van git checkout (dat gooide eerder deze sessie tweemaal ongecommit werk weg).

Geen goldens. De keuring vroeg er vier. Op deze machine faalt 27 van de 43 bestaande goldens, inclusief diatypes waar niets aan is geraakt — dit is dus niet de referentiemachine, en hier gegenereerde goldens zouden alleen bij mij kloppen. De metende proeven hierboven dekken alle vijf de gevonden fouten.

Bewust niet opgelost

De categoriepil is bij LibreKAT 3 px accent breed. Dikker kan niet zonder over de buurpil te lopen, en het is geen plek waar iemand vastloopt: de pillen komen als eerste in de tabvolgorde, er zijn er een handvol, de open pil is zelf al vetgedrukt en accentkleurig, en wisselen kan ook via de blokken. Bij de andere zeven gemeten profielen zet diezelfde pil 4 px accent plus een tweede band. Gemeten oordeel, door de keuring bevestigd.

Poorten

make check groen (9700+ tests, dekkingsvloer en per-bestandsvloer), make check-secrets groen, make sast groen (0 findings). DAST niet gedraaid — deze wijziging raakt geen geserveerd oppervlak. docs/ACCESSIBILITY.md, docs/SHORTCUTS.md, docs/USER_GUIDE.md (beide talen) en de CHANGELOG zijn bij. Geen nieuwe interfaceteksten, dus geen vertaalronde.

## Wat dit oplost De blokken en categoriepillen van een keuze-menudia (#1162) waren een kale `GestureDetector` over een getekende kaart: geen knoprol, geen focus, geen toetsenbordroute. Wie met een klikker presenteert of geen muis kan gebruiken, kwam een menudia niet door — en dat is nu juist het diatype dat over navigeren gaat (WCAG 2.1.1). Het stond als bekend gat in `docs/ACCESSIBILITY.md`; die tekst beschrijft nu wat het dóét. | Toets | Doet | | --- | --- | | `Tab` / `Shift+Tab` | langs de categorieën, dan de blokken in leesvolgorde | | `Enter` · `↵` · spatie | volgt de sprong, of wisselt van categorie | | `Escape` | geeft de toetsen terug aan de dia | Enter en spatie zijn in de presentator óók "volgende dia". Dat botst niet: een toetsaanslag gaat eerst naar het onderdeel met de focus. De pijltjestoetsen blijven van de presentatie — die onderscheppen zou betekenen dat je met de focus op een blok niet verder kunt. Een schermlezer krijgt de knoprol met de uitleg achter het label (`Prijzen. Wat het kost`), en de tekst ín de kaart is uitgesloten zodat er niets dubbel klinkt. Een tekstblok zonder doel blijft bewust een gewoon blok, geen knop. Eén gedeelde widget voor kaart, schijf en pil: drie kopieën van focus-, toets- en semantiekafhandeling zijn drie plekken waar er één achterblijft, en juist bij toegankelijkheid kijkt daar niemand naar tot iemand het nodig heeft. ## Vier keuringsrondes op één focusring De gedragsproeven stonden na de eerste commit groen. De beeldkeuring vond daarna in vier rondes vijf echte fouten, waarvan ik er drie zélf had geïntroduceerd tijdens het repareren van de vorige. Ze staan hier omdat ze samen één les zijn. 1. **De ring was een dichte plaat.** De halo was een `BoxShadow` zonder vervaging — dat tekent een *gevulde* vorm, en in de voorgrond zonder achtergrondkleur overschilderde die het gefocuste blok volledig. Label, uitleg en pijl weg, precies op het blok dat de presentator moet kunnen lezen. 2. **De ring at het blok op.** `Border.all` tekent bínnen de doos, en die ruimte was nergens gereserveerd. De ring is een fractie van de diabreedte terwijl het blok krimpt met het aantal blokken: bij zestien blokken at een ring van 19 px een schijf van 84 px op. Nu hangt hij in een `Stack` met `Clip.none` buiten het blok — de layout wordt niet geraakt. 3. **Het accent was onzichtbaar.** Bij dat herschrijven raakte de tekenvolgorde omgedraaid: beide banden stonden op een achtergrond-decoratie, die schildert vóór haar kind uit, dus de brede contrastlijn dekte de smalle accentband af. Eenkleurige donut. 4. **De ring liep over de buurschijven.** Twaalf schijven op 1280 staan 98 px uit elkaar met een doorsnede van 84 — 15 px lucht tegen een ring van 19. Mijn commentaar bewéérde dat de tussenruimte ruimer was dan de ring dik is; dat had ik aangenomen, niet gemeten. De dikte volgt nu uit die lucht. Plus: bij 1920 werd de bovenste schijf vlak afgeknipt (`Clip.hardEdge`). 5. **De tweede kleur viel weg bij het standaardprofiel.** In LibreKAT zijn tekst- en accentkleur allebei `#003399`. De eerste reparatie ("neem de verste van tekst en achtergrond, op luminantie") loste dat op maar was systematisch scheef: in een deugdelijk thema ligt de achtergrond per definitie aan het uiterste van het luminantiebereik, dus die wint bijna altijd — en dat is precies de kandidaat die géén inkt zet. Drie van de vier meegeleverde profielen verloren zo hun tweede band. Nu is de tekstkleur de eerste keus, met de achtergrond als terugval bij een echte botsing, en de maatstaf is het grootste kanaalverschil in plaats van luminantie (rood naast teal is een duidelijke tweede band terwijl hun helderheid vrijwel gelijk is). ## De les, en wat eraan is gedaan Elke ronde stonden mijn proeven groen, en elke ronde toetsten ze het verkeerde: of Tab focust, of Enter springt, of er *een rand* is, of de twee kleuren *verschillen*. Geen ervan keek naar wat er werkelijk op de dia stond. Wat er nu wordt gemeten: - de ring is een omtrek — geen `color`, `gradient` of `boxShadow` op een ringdecoratie; - hij omsluit het label in plaats van eroverheen te liggen; - focussen verschuift niets op de dia (gemeten op een ring van zestien schijven); - er staan twee kleuren en het accent ligt bovenop; - de ringdikte past in de lucht tussen twee buren, narekend van twee tot dertig schijven; - de tweede band zet inkt op **elk meegeleverd profiel** — de achtergrond mag alleen worden gekozen als de tekstkleur echt niet kan. Elke reparatie is één keer rood gezien tegen de ongerepareerde code, met een kopie van het bestand in plaats van `git checkout` (dat gooide eerder deze sessie tweemaal ongecommit werk weg). **Geen goldens.** De keuring vroeg er vier. Op deze machine faalt 27 van de 43 bestaande goldens, inclusief diatypes waar niets aan is geraakt — dit is dus niet de referentiemachine, en hier gegenereerde goldens zouden alleen bij mij kloppen. De metende proeven hierboven dekken alle vijf de gevonden fouten. ## Bewust niet opgelost De categoriepil is bij LibreKAT 3 px accent breed. Dikker kan niet zonder over de buurpil te lopen, en het is geen plek waar iemand vastloopt: de pillen komen als eerste in de tabvolgorde, er zijn er een handvol, de open pil is zelf al vetgedrukt en accentkleurig, en wisselen kan ook via de blokken. Bij de andere zeven gemeten profielen zet diezelfde pil 4 px accent plus een tweede band. Gemeten oordeel, door de keuring bevestigd. ## Poorten `make check` groen (9700+ tests, dekkingsvloer en per-bestandsvloer), `make check-secrets` groen, `make sast` groen (0 findings). DAST niet gedraaid — deze wijziging raakt geen geserveerd oppervlak. `docs/ACCESSIBILITY.md`, `docs/SHORTCUTS.md`, `docs/USER_GUIDE.md` (beide talen) en de CHANGELOG zijn bij. Geen nieuwe interfaceteksten, dus geen vertaalronde.
De blokken en de categoriepillen waren een kale GestureDetector over een
getekende kaart: geen knoprol, geen focus, geen toetsenbordroute. Wie met een
klikker presenteert of geen muis kan gebruiken, kwam een menudia niet door —
en dat is nu juist het diatype dat over navigeren gaat (WCAG 2.1.1).

Tab en Shift+Tab lopen erlangs, eerst de categorieen en dan de blokken in
leesvolgorde. Enter, numpad-Enter en de spatiebalk activeren. Escape geeft de
toetsen terug aan de dia.

Twee dingen die niet vanzelf goed gingen:

- Enter viel door naar de presentator, die er "volgende dia" van maakt. Twee
  proeven stonden daardoor groen om de verkeerde reden: ze landden op de dia
  na het menu, die toevallig ook de doeldia was. De doeldia in die proeven is
  nu bewust niet de buurdia, en Enter staat expliciet in de shortcuts in
  plaats van op de standaardafspraken van het platform te leunen.
- Kaal unfocus() bij Escape zette de focus op de omhullende scope, waarna de
  presentator niets meer kreeg: je drukte Escape en daarna deed de spatiebalk
  niets. De focus gaat nu naar de dichtstbijzijnde voorouder die zelf toetsen
  afhandelt — generiek, zodat het blok de presentator niet hoeft te kennen.

De focusring is een accentring met een halo eromheen, zichtbaar vanaf de
achterste rij en op elke achtergrond. Hij is van de presentator: op het
beamervenster is niets aanklikbaar, dus ook niets focusbaar.

Een schermlezer krijgt de knoprol met de uitleg achter het label; de tekst in
de kaart is uitgesloten zodat er niets dubbel wordt voorgelezen. Een tekstblok
zonder doel blijft bewust een gewoon blok, geen knop.

Een gedeelde widget voor kaart, schijf en pil: drie kopieen van focus-, toets-
en semantiekafhandeling zijn drie plekken waar er een achterblijft, en juist
bij toegankelijkheid kijkt daar niemand naar tot iemand het nodig heeft.
De halo was een BoxShadow zonder vervaging. Dat tekent geen omtrek maar een
gevulde vorm; normaal verdwijnt die onder de achtergrond van de decoratie,
maar deze had er geen en stond in de voorgrond. Gevolg: het blok met de focus
werd volledig overschilderd in de tekstkleur — label, uitleg en pijl weg,
precies op het blok dat de presentator moest kunnen lezen. Op alle drie de
vormen, op de pillen, en in elk thema.

Nu twee geneste randen: buitenom het accent, daarbinnen een dunnere lijn in
de tekstkleur. Dat tekent wel omtrekken, en het kleurverschil doet hetzelfde
werk als de gloed had moeten doen — op welke achtergrond de dia ook staat,
een van de twee steekt af.

De ring op een schijf is zwaarder geworden (w*0.008 tegen w*0.005 bij een
kaart): een springende schijf draagt zelf al een accentrand van w*0.005, dus
met dezelfde dikte was "gefocust" niet meer dan een tintje verschil.

De zes proeven eromheen stonden groen en waren goed geschreven, maar toetsten
alleen gedrag — geen ervan keek naar wat de focus tékent. De nieuwe proef eist
dat elke voorgrond-decoratie een rand heeft en geen vulling, verloop of
schaduw, en dat label en uitleg leesbaar blijven. Een keer rood gezien tegen
de plaat-versie.
Border.all tekent binnen de doosgrenzen, en die ruimte was nergens
gereserveerd. De ring is een fractie van de diabreedte terwijl het blok
krimpt met het aantal blokken erop: bij het gedocumenteerde maximum at een
ring van 19 px een schijf van 84 px op. Het label werd aangesneden —
"Vacatures" werd "Vaca..." — en bij de brede kaart in "onder elkaar" verdween
de eerste letter onder de lijn. De rasterkaart had er geen last van, want daar
is het blok groter; precies het soort verschil dat je pas op het plafond ziet.

De ring hangt nu in een Stack met Clip.none buiten de doos. Dat raakt de
layout niet: het blok blijft even groot en de tekst staat waar hij stond, met
of zonder focus. De ruimte ernaast is er — de tussenruimte in het raster en op
de ring is ruimer dan de ring dik is. De hoekstraal van de ring is die van het
blok plus zijn eigen dikte, zodat hij evenwijdig loopt.

De vorige proef kon dit niet vangen: hij eiste dat label en uitleg vindbaar
zijn, maar een Text die overschilderd wordt is nog steeds vindbaar. Nu wordt
gemeten dat de ring het label omsluit in plaats van eroverheen te liggen, en
dat de focus niets op de dia verschuift (op een ring van zestien schijven, waar
het misging).
Drie bevindingen uit de derde beeldkeuring, waarvan de eerste er bij het
herschrijven van vorige ronde zelf in is gekomen.

1. Het accent was onzichtbaar. Beide ringen stonden op een achtergrond-
   decoratie, en die schildert voor haar kind uit: de brede contrastlijn
   binnenin dekte de smalle accentband buitenom volledig af. Gemeten over alle
   vormen en thema's was de ring eenkleurig. De volgorde is nu omgedraaid — de
   contrastlijn buiten, het accent als kind eroverheen — zodat er weer twee
   banden staan. Het commentaar beschreef de omgekeerde volgorde van wat er
   stond; dat klopt nu ook.

2. De ring liep over de buurschijven heen. Hij hangt buiten de schijf en eet
   dus van de lucht tussen twee buren, en bij een volle ring is die krap:
   twaalf schijven op 1280 staan 98 px uit elkaar met een doorsnede van 84, dus
   15 px lucht tegen een ring van 19 px. De dikte volgt nu uit die lucht
   (menuDiscRingWidth), met een proef die van twee tot dertig schijven
   narekent dat hij past. Bij weinig schijven blijft hij op zijn volle dikte.

3. Bij 1920x1080 werd de bovenste schijf vlak afgeknipt: de Stack van de ring
   stond op Clip.hardEdge, en of een schijf de rand raakt hangt van de
   rendermaat af. Nu Clip.none.

De proef van vorige ronde zag geen van drieen: hij eiste een rand zonder
vulling en dat de ring het label omsluit, en dat klopt ook bij een
eenkleurige ring die over de buurschijf ligt. Er wordt nu gemeten dat er twee
kleuren staan en dat het accent bovenop ligt. Allebei een keer rood gezien
tegen de gesaboteerde code.
De focusring dankt zijn zichtbaarheid aan het verschil tussen zijn twee
banden: op welke achtergrond de dia ook staat, een ervan steekt af. Dat
vangnet verdween zodra beide banden dezelfde kleur kregen — en dat is geen
bedacht randgeval. In het meegeleverde LibreKAT-profiel zijn textColor en
accentColor allebei #003399, en dat profiel staat standaard geselecteerd. De
ring viel daar terug op een enkele blauwe band: dezelfde uitkomst als de
eenkleurige donut van vorige ronde, nu via het thema in plaats van via de
verfvolgorde.

De tweede band is nu die van tekst- of achtergrondkleur die het verst van het
accent af ligt. Een derde kleur verzinnen zou de themaregel breken; kiezen
tussen wat de dia toch al draagt niet.

De bestaande proef kon dit niet zien: die draait op een profiel waar accent en
tekst wel verschillen. De nieuwe zet ze allebei op #003399 en eist dat er nog
steeds twee kleuren in de ring staan. Een keer rood gezien.
De vorige regel koos "de verste van tekst en achtergrond, op luminantie". Die
is systematisch scheef: in een deugdelijk thema ligt de achtergrond per
definitie aan het uiterste van het luminantiebereik — dat is wat de tekst erop
leesbaar maakt — en het accent ergens ertussen. De achtergrond wint dan bijna
altijd, en dat is nu juist de kandidaat die geen inkt zet: een band in de
diakleur is een gat.

Gevolg, gemeten over de vier meegeleverde profielen: drie ervan verloren hun
tweede band. Standaard (#222222 op 15,9:1) en Security (#1E293B op 14,6:1)
hadden een prima tweede kleur en ruilden die in voor de dia zelf. Dat was
bijvangst van de LibreKAT-reparatie, niet de bedoeling.

Nu andersom: de tekstkleur is de eerste keus, en de achtergrond is de terugval
voor het ene geval waarvoor hij bedoeld is — een tekstkleur die tegen het
accent aan ligt, zoals LibreKAT waar ze identiek zijn.

En de maatstaf is niet langer luminantie maar het grootste kanaalverschil.
Twee kleuren met dezelfde helderheid kunnen prima uit elkaar te houden zijn:
rood naast teal is een duidelijke tweede band terwijl hun luminantie bijna
gelijk is, en de oude regel wees teal daarom af.

De proef eiste alleen dat de twee kleuren verschillen; de achtergrond telt
daar als een kleur, dus die kon dit niet zien. Er wordt nu over elk
meegeleverd profiel nagelopen dat de tweede band inkt zet, en dat de
achtergrond alleen wordt gekozen als de tekstkleur echt niet kan. Een keer
rood gezien tegen de oude regel.
docs(code): de focusring belooft geen twee banden meer als hij er één heeft
All checks were successful
scans / scans (pull_request) Successful in 1m54s
static-gate / static-gate (pull_request) Successful in 4m43s
0d5337cb2e
Bij de terugval op de achtergrond zijn er geen twee banden maar een band met
lucht, en draagt het accent de ring alleen. De doc-comment beweerde dat het
verschil tussen de twee banden de zichtbaarheid draagt — waar in zeven van de
negen gemeten profielen, niet waar in de twee waar de terugval intreedt. Een
commentaar dat een belofte doet die de code niet waarmaakt is erger dan geen
commentaar.
brenno merged commit 72f0c5ee5f into main 2026-08-18 10:45:48 +00:00
Sign in to join this conversation.
No description provided.