a11y+privacy(callouts): toegankelijkheidspoort #1801 (#1844) #1851

Merged
brenno merged 4 commits from gate/1844-a11y-screenreader into main 2026-08-29 15:24:38 +00:00
Owner

Closes #1844 (deels — het vinkje 'echte schermlezer' volgt in een reactie op het issue zodra de VoiceOver-ronde gelopen is).

De toegankelijkheidspoort uit IMAGE_CALLOUTS.md §12 tegen de gebouwde oppervlakken gelegd, in plaats van tegen het ontwerp. Vier echte gebreken kwamen eruit.

Wat er stuk was

  1. De markering noemde haar letter twee keer. Semantics(container: true, label: 'B, de inlaat') met een zichtbare Text('B') eronder levert één knoop met label B, de inlaat\nB. Een schermlezer zegt de referentieletter dan dubbel. Nu staat de zichtbare letter met ExcludeSemantics buiten de boom — hij is al het eerste woord van de naam.

  2. De bullet had geen Flutter-tegenhanger van aria-describedby. De HTML-export koppelde de bullet al aan de verborgen beschrijving; in de app stond de beschrijving nergens. Nu draagt de bullet hem als hint op één samengevoegde knoop, zodat het lezen van de bullet de betekenis één keer meeleest.

  3. PPTX en ODP lieten hun alt-tekstsleuf leeg. descr op de vorm en <svg:desc> in het frame bestonden allebei en stonden allebei leeg. §12.2 eist dat de betekenis niet weggegooid wordt waar het doelformaat er een plek voor heeft. Nu gevuld met calloutAltText: de alt-tekst van het beeld plus A: … per beschrijving. Een gerasterde dia blijft structureel ontoegankelijk — dat blijft zo staan in ACCESSIBILITY.md.

  4. Privacylek. OciWacht scánde calloutDescription (er was zelfs een test voor), maar PrivacyProjection redigeerde het veld niet. Wie een bevinding kreeg en de dia op redigeren zette, verstuurde de tekst alsnog: in het .md, de HTML-export en de TikZ-notities. Punt 3 hierboven maakte dat lek stiller in plaats van kleiner — de alt-tekstsleuf staat nergens op het beeld. De projectie redigeert het veld nu op dezelfde fragment-index als de scanner hem meldt; de geometrie blijft staan.

Wat er niet bewaakt werd

De drie widgettoetsen in callout_reveal_test.dart renderden nooit een markering: CalloutOverlay tekent niets vóór de intrinsieke beeldmaat bekend is, en die komt uit een echte decode die onder flutter test niet loopt. expect(find.text('A'), findsNothing) was dus even groen mét als zónder het onthullingsfilter. Ze draaien nu binnen tester.runAsync op een PNG op schijf. Mutatieproef: het filter uitschakelen (if (false && …)) maakt drie toetsen rood — daarvóór nul.

Ontwerpdoc: revisie 5

§12.2 sprak §7 tegen. Het vroeg de HTML-export om verborgen niet-onthulde groepen en één aria-live-region per dia, terwijl §7 zegt dat een statische export elke groep toont. Er is dus niets te verbergen en geen stap om aan te kondigen; een dode live region is slechter dan geen, want het is opmaak die gedrag belooft dat het bestand niet heeft. Die twee eisen horen bij het Flutter-oppervlak, dat wél stapt — en dáár staan ze ook geïmplementeerd (sendAnnouncement in presenter_navigation.dart). Een toets valt om zodra de HTML-export tóch gaat stappen, zodat het paar niet vergeten kan worden.

Wat er nog niet in zit

De verificatie onder een echte schermlezer (VoiceOver) is nog niet gelopen; die volgt als reactie op #1844. Alles wat hier zit is de laag eronder: de accessibility tree zoals een schermlezer hem krijgt, geen DOM-aanname.

Test plan

  • make check groen
  • make check-secrets groen (gitleaks + trufflehog, 0 geverifieerd)
  • make sast groen (semgrep, 4 regels over 1186 bestanden, 0 bevindingen)
  • Nieuw: test/callout_accessibility_test.dart — 15 toetsen over de semantics-boom, de HTML-a11y-eigenschappen en de alt-tekstsleuf van PPTX/ODP
  • Nieuw: drie privacytoetsen in callout_privacy_and_limits_test.dart, eerst rood tegen de onherstelde projectie
  • callout_reveal_test.dart toetst nu het echte beeld; mutatieproef gedaan
  • VoiceOver-ronde (volgt op het issue)

Bewaker

Geraakt: een publieke belofte (ACCESSIBILITY.md, USER_GUIDE) en waar gegevens van de gebruiker landen (de exportsleuf). Beide kanten opgeschreven in plaats van stilzwijgend: de gids zegt nu expliciet dat de beschrijving gewone inhoud is die meereist en gescand én geredigeerd wordt, en ACCESSIBILITY.md houdt de claim 'de exports zijn geen toegankelijke documenten' overeind — één regel tekst per dia is geen structuur.

Het bestandsformaat is niet geraakt: er komt niets bij in het .md.

🤖 Generated with Claude Code

Closes #1844 (deels — het vinkje 'echte schermlezer' volgt in een reactie op het issue zodra de VoiceOver-ronde gelopen is). De toegankelijkheidspoort uit IMAGE_CALLOUTS.md §12 tegen de gebouwde oppervlakken gelegd, in plaats van tegen het ontwerp. Vier echte gebreken kwamen eruit. ## Wat er stuk was 1. **De markering noemde haar letter twee keer.** `Semantics(container: true, label: 'B, de inlaat')` met een zichtbare `Text('B')` eronder levert één knoop met label `B, de inlaat\nB`. Een schermlezer zegt de referentieletter dan dubbel. Nu staat de zichtbare letter met `ExcludeSemantics` buiten de boom — hij is al het eerste woord van de naam. 2. **De bullet had geen Flutter-tegenhanger van `aria-describedby`.** De HTML-export koppelde de bullet al aan de verborgen beschrijving; in de app stond de beschrijving nergens. Nu draagt de bullet hem als `hint` op één samengevoegde knoop, zodat het lezen van de bullet de betekenis één keer meeleest. 3. **PPTX en ODP lieten hun alt-tekstsleuf leeg.** `descr` op de vorm en `<svg:desc>` in het frame bestonden allebei en stonden allebei leeg. §12.2 eist dat de betekenis niet weggegooid wordt waar het doelformaat er een plek voor heeft. Nu gevuld met `calloutAltText`: de alt-tekst van het beeld plus `A: …` per beschrijving. Een gerasterde dia blijft structureel ontoegankelijk — dat blijft zo staan in ACCESSIBILITY.md. 4. **Privacylek.** OciWacht scánde `calloutDescription` (er was zelfs een test voor), maar `PrivacyProjection` redigeerde het veld niet. Wie een bevinding kreeg en de dia op *redigeren* zette, verstuurde de tekst alsnog: in het `.md`, de HTML-export en de TikZ-notities. Punt 3 hierboven maakte dat lek stiller in plaats van kleiner — de alt-tekstsleuf staat nergens op het beeld. De projectie redigeert het veld nu op dezelfde fragment-index als de scanner hem meldt; de geometrie blijft staan. ## Wat er niet bewaakt werd De drie widgettoetsen in `callout_reveal_test.dart` renderden nooit een markering: `CalloutOverlay` tekent niets vóór de intrinsieke beeldmaat bekend is, en die komt uit een echte decode die onder `flutter test` niet loopt. `expect(find.text('A'), findsNothing)` was dus even groen mét als zónder het onthullingsfilter. Ze draaien nu binnen `tester.runAsync` op een PNG op schijf. Mutatieproef: het filter uitschakelen (`if (false && …)`) maakt drie toetsen rood — daarvóór nul. ## Ontwerpdoc: revisie 5 §12.2 sprak §7 tegen. Het vroeg de **HTML-export** om verborgen niet-onthulde groepen en één `aria-live`-region per dia, terwijl §7 zegt dat een statische export elke groep toont. Er is dus niets te verbergen en geen stap om aan te kondigen; een dode live region is slechter dan geen, want het is opmaak die gedrag belooft dat het bestand niet heeft. Die twee eisen horen bij het Flutter-oppervlak, dat wél stapt — en dáár staan ze ook geïmplementeerd (`sendAnnouncement` in `presenter_navigation.dart`). Een toets valt om zodra de HTML-export tóch gaat stappen, zodat het paar niet vergeten kan worden. ## Wat er nog niet in zit De verificatie **onder een echte schermlezer** (VoiceOver) is nog niet gelopen; die volgt als reactie op #1844. Alles wat hier zit is de laag eronder: de accessibility tree zoals een schermlezer hem krijgt, geen DOM-aanname. ## Test plan - [x] `make check` groen - [x] `make check-secrets` groen (gitleaks + trufflehog, 0 geverifieerd) - [x] `make sast` groen (semgrep, 4 regels over 1186 bestanden, 0 bevindingen) - [x] Nieuw: `test/callout_accessibility_test.dart` — 15 toetsen over de semantics-boom, de HTML-a11y-eigenschappen en de alt-tekstsleuf van PPTX/ODP - [x] Nieuw: drie privacytoetsen in `callout_privacy_and_limits_test.dart`, eerst rood tegen de onherstelde projectie - [x] `callout_reveal_test.dart` toetst nu het echte beeld; mutatieproef gedaan - [ ] VoiceOver-ronde (volgt op het issue) ### Bewaker Geraakt: een publieke belofte (ACCESSIBILITY.md, USER_GUIDE) en waar gegevens van de gebruiker landen (de exportsleuf). Beide kanten opgeschreven in plaats van stilzwijgend: de gids zegt nu expliciet dat de beschrijving gewone inhoud is die meereist en gescand én geredigeerd wordt, en ACCESSIBILITY.md houdt de claim 'de exports zijn geen toegankelijke documenten' overeind — één regel tekst per dia is geen structuur. Het bestandsformaat is niet geraakt: er komt niets bij in het `.md`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
De toegankelijkheidspoort van #1801 tegen de gebouwde oppervlakken gelegd.
Drie echte gebreken kwamen eruit:

- de markering kondigde haar letter twee keer aan: één keer als eerste woord
  van de samengestelde naam, en nog eens omdat de zichtbare Text zelf in de
  boom stond. Nu ExcludeSemantics;
- de bullet had geen Flutter-tegenhanger van het aria-describedby dat de
  HTML-export al had. Nu de beschrijving als hint op één samengevoegde knoop;
- PPTX en ODP lieten de alt-tekstsleuf leeg. Nu descr en <svg:desc>, gevuld
  met de alt-tekst plus de callout-beschrijvingen (calloutAltText).

En één gat in de bewaking: de drie widgettoetsen in callout_reveal_test
renderden nooit een markering — de overlay wacht op een echte decode — dus
findsNothing bewees niets. Ze draaien nu binnen runAsync op een PNG op schijf;
het onthullingsfilter uitschakelen maakt drie toetsen rood.

§12.2 van het ontwerp sprak zichzelf tegen: het vroeg de statische HTML-export
om verborgen groepen en een live region, terwijl §7 zegt dat een statische
export elke groep toont. Die twee eisen horen bij het Flutter-oppervlak, dat
wél stapt; revisie 5 legt dat vast en een toets valt om zodra de export toch
gaat stappen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
De alt-tekstsleuf duwde ExportService.export twee regels over het plafond.
De twee rasterbouwers zijn nu eigen methoden; het plafond zakt mee van 151
naar 148 in plaats van omhoog.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
OciWacht scande `calloutDescription` al — een e-mailadres in een beschrijving
gaf keurig een bevinding — maar de projectie liet het veld ongemoeid. Wie de
dia daarna op `redigeren` zette, verstuurde de tekst alsnog: in het `.md`, in
de HTML-export en in de TikZ-notities. Met de alt-tekstsleuf uit de vorige
commit kwam daar een plek bij die niemand op het beeld ziet staan, en dat maakt
het lek stiller in plaats van kleiner.

De projectie redigeert het veld nu op dezelfde fragment-index als de scanner
hem meldt. De geometrie blijft staan: alleen de prose gaat door de redactie.

De regressietest stond eerst rood op de onherstelde code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
docs(callouts): §8 vertelt wat "scanbaar" tot #1844 wél en niet deed
All checks were successful
scans / scans (pull_request) Successful in 2m14s
static-gate / static-gate (pull_request) Successful in 5m24s
8548beabb0
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
brenno merged commit fea3c3211d into main 2026-08-29 15:24:38 +00:00
Sign in to join this conversation.
No description provided.