From f3e5745145706d9e3b4528f8331466da2d6aa8bf Mon Sep 17 00:00:00 2001 From: Edwin van den Houdt Date: Mon, 7 Sep 2026 11:53:03 +0200 Subject: [PATCH 1/9] fix: the wizards' seed input never arrived (RD-39) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All three wizard containers read `this.seed()` in the constructor. Angular binds component inputs after the constructor runs, so the value was always the `initial` default, `seeded !== initial` was always false, and every mount took the `draftSync.resume()` branch. The `seed` input was dead code. The two single-step forms built on the same idiom read the input inside the microtask and work correctly. That contrast is the diagnosis. Impact: 21 seeded wizard stories rendered step 1 instead of the state they asked for. Storybook is this repo's UI test surface, so the states with no other coverage were exactly the ones not rendering — Submitting, Submitted, Failed, Ingediend, Mislukt. The a11y runner checks that whatever rendered is accessible, never that the right thing rendered, so nothing caught it. Production was unaffected: no route binds `seed`. Read the input inside the microtask, matching the two forms. Turn the spec's old `componentInstance.dispatch(...)` workaround into a real regression test through `componentRef.setInput('seed', ...)`. Verified: with the intake fix reverted the two spec cases fail; with it, 319 pass. `npm run ci --full` is green, and the newly rendered markup produced no axe violations. A browser check of seven seeded stories across all three wizards asserts text only reachable from a seed. Co-Authored-By: Claude Opus 5 --- .../herregistratie-wizard.component.ts | 11 ++- .../intake-wizard.component.spec.ts | 30 +++++-- .../intake-wizard/intake-wizard.component.ts | 11 ++- .../registratie-wizard.component.ts | 11 ++- .../RD-39-seed-input-timing.md | 82 +++++++++++++++++++ docs/project/readable-codebase/README.md | 1 + libs/shared/docs/behaviour-spec.mdx | 3 +- 7 files changed, 129 insertions(+), 20 deletions(-) create mode 100644 docs/project/readable-codebase/RD-39-seed-input-timing.md diff --git a/apps/ssp/src/app/herregistratie/ui/herregistratie-wizard/herregistratie-wizard.component.ts b/apps/ssp/src/app/herregistratie/ui/herregistratie-wizard/herregistratie-wizard.component.ts index 254da49..1e99ba8 100644 --- a/apps/ssp/src/app/herregistratie/ui/herregistratie-wizard/herregistratie-wizard.component.ts +++ b/apps/ssp/src/app/herregistratie/ui/herregistratie-wizard/herregistratie-wizard.component.ts @@ -263,10 +263,13 @@ export class HerregistratieWizardComponent { constructor() { // An explicit seed (stories/tests) wins; otherwise resume the backend draft // (`?aanvraag=`) or start fresh. Persistence is the draftSync controller's job. - const seeded = this.seed(); - queueMicrotask(() => - seeded !== initial ? this.dispatch({ tag: 'Seed', state: seeded }) : this.draftSync.resume(), - ); + // Read `seed()` INSIDE the microtask: Angular binds inputs after the constructor + // runs, so an eager read here always returns the `initial` default. + queueMicrotask(() => { + const seeded = this.seed(); + if (seeded !== initial) this.dispatch({ tag: 'Seed', state: seeded }); + else void this.draftSync.resume(); + }); } /** Reset the wizard to a fresh, empty start. */ diff --git a/apps/ssp/src/app/herregistratie/ui/intake-wizard/intake-wizard.component.spec.ts b/apps/ssp/src/app/herregistratie/ui/intake-wizard/intake-wizard.component.spec.ts index 69b0804..fe153e9 100644 --- a/apps/ssp/src/app/herregistratie/ui/intake-wizard/intake-wizard.component.spec.ts +++ b/apps/ssp/src/app/herregistratie/ui/intake-wizard/intake-wizard.component.spec.ts @@ -26,14 +26,30 @@ const buitenlandJa: IntakeState = { scholingThreshold: 1000, }; +/** Mount with a seed and let the constructor's microtask apply it. */ +async function mountSeeded(state: IntakeState) { + TestBed.configureTestingModule({ + providers: [provideHttpClient(), provideApiClient()], + }); + const fixture = TestBed.createComponent(IntakeWizardComponent); + fixture.componentRef.setInput('seed', state); + await Promise.resolve(); // the seed is applied in a queueMicrotask + fixture.detectChanges(); + return fixture; +} + describe('IntakeWizardComponent', () => { - it('renders each field group as its own grey
', () => { - TestBed.configureTestingModule({ - providers: [provideHttpClient(), provideApiClient()], - }); - const fixture = TestBed.createComponent(IntakeWizardComponent); - fixture.componentInstance.dispatch({ tag: 'Seed', state: buitenlandJa }); - fixture.detectChanges(); + // Regression: the constructor must read `seed()` INSIDE its microtask. Angular binds + // inputs after the constructor runs, so an eager read silently yields the `initial` + // default and every seeded story renders step 1 instead of the state it asked for. + it('honours the seed input', async () => { + const fixture = await mountSeeded(buitenlandJa); + + expect(fixture.componentInstance.state()).toEqual(buitenlandJa); + }); + + it('renders each field group as its own grey
', async () => { + const fixture = await mountSeeded(buitenlandJa); const fieldsets: HTMLElement[] = Array.from( fixture.nativeElement.querySelectorAll('form.form-horizontal fieldset'), diff --git a/apps/ssp/src/app/herregistratie/ui/intake-wizard/intake-wizard.component.ts b/apps/ssp/src/app/herregistratie/ui/intake-wizard/intake-wizard.component.ts index bfe20b9..56cbab7 100644 --- a/apps/ssp/src/app/herregistratie/ui/intake-wizard/intake-wizard.component.ts +++ b/apps/ssp/src/app/herregistratie/ui/intake-wizard/intake-wizard.component.ts @@ -212,10 +212,13 @@ export class IntakeWizardComponent { constructor() { // An explicit seed (stories/tests) wins; otherwise resume the backend draft // (`?aanvraag=`) or start fresh. Persistence is the draftSync controller's job. - const seeded = this.seed(); - queueMicrotask(() => - seeded !== initial ? this.dispatch({ tag: 'Seed', state: seeded }) : this.draftSync.resume(), - ); + // Read `seed()` INSIDE the microtask: Angular binds inputs after the constructor + // runs, so an eager read here always returns the `initial` default. + queueMicrotask(() => { + const seeded = this.seed(); + if (seeded !== initial) this.dispatch({ tag: 'Seed', state: seeded }); + else void this.draftSync.resume(); + }); // Apply the server-owned threshold into machine state as it arrives. Track // only the policy value; untrack the dispatch (it reads the state signal // internally, which would otherwise make this effect loop on its own write). diff --git a/apps/ssp/src/app/registratie/ui/registratie-wizard/registratie-wizard.component.ts b/apps/ssp/src/app/registratie/ui/registratie-wizard/registratie-wizard.component.ts index 4f8241d..eb2a740 100644 --- a/apps/ssp/src/app/registratie/ui/registratie-wizard/registratie-wizard.component.ts +++ b/apps/ssp/src/app/registratie/ui/registratie-wizard/registratie-wizard.component.ts @@ -233,10 +233,13 @@ export class RegistratieWizardComponent { constructor() { // An explicit seed (stories/tests) wins; otherwise resume from the backend draft // (`?aanvraag=`), or start fresh. Persistence is the draftSync controller's job. - const seeded = this.seed(); - queueMicrotask(() => - seeded !== initial ? this.dispatch({ tag: 'Seed', state: seeded }) : this.draftSync.resume(), - ); + // Read `seed()` INSIDE the microtask: Angular binds inputs after the constructor + // runs, so an eager read here always returns the `initial` default. + queueMicrotask(() => { + const seeded = this.seed(); + if (seeded !== initial) this.dispatch({ tag: 'Seed', state: seeded }); + else void this.draftSync.resume(); + }); // Prefill the address from the BRP lookup as it arrives. Track only the facade's // parsed prefill signal; untrack the dispatch (it reads the state signal, which // would otherwise make this effect loop on its own write). Don't clobber diff --git a/docs/project/readable-codebase/RD-39-seed-input-timing.md b/docs/project/readable-codebase/RD-39-seed-input-timing.md new file mode 100644 index 0000000..0f57bca --- /dev/null +++ b/docs/project/readable-codebase/RD-39-seed-input-timing.md @@ -0,0 +1,82 @@ +# RD-39 — The wizards' `seed` input never arrived + +Status: done +Source: found while planning the machine-wiring migration (ADR-0007 arc) + +## Why + +All three wizard containers read their `seed` input **in the constructor**: + +```ts +const seeded = this.seed(); // always the `initial` default +queueMicrotask(() => + seeded !== initial ? this.dispatch({ tag: 'Seed', state: seeded }) : this.draftSync.resume(), +); +``` + +Angular binds component inputs **after** the constructor runs. So `seeded` was +always the `initial` default, `seeded !== initial` was always false, and every +mount took the `draftSync.resume()` branch. The `seed` input was dead code. + +The two single-step forms built on the same idiom prove the diagnosis by +contrast. They read the input **inside** the microtask, and they work: + +``` +change-request-form.component.ts:191 queueMicrotask(() => this.dispatch({ tag: 'Seed', state: this.seed() })); +besluit-form.component.ts:176 queueMicrotask(() => this.dispatch({ tag: 'Seed', state: this.seed() })); +``` + +## Impact + +Every seeded wizard story rendered step 1 instead of the state it asked for — +21 stories across the three wizards. Storybook is this repo's UI test surface +(CLAUDE.md "Testing"), so the states that had no other coverage were exactly +the ones silently not rendering: `Submitting`, `Submitted`, `Failed`, +`Ingediend`, `Mislukt`. + +Nothing caught it. `.storybook-ssp/test-runner.ts` runs axe only — it checks +that whatever rendered is accessible, never that the right thing rendered. +`intake-wizard.component.spec.ts` had worked around it by calling +`componentInstance.dispatch(...)` instead of setting the input, which is the +shape of a test written against a broken input path. + +**Production was unaffected**: no route binds `seed`, so the resume branch was +always the correct one there. + +## Decisions + +1. **Read `seed()` inside the microtask**, matching the two forms. Six lines + across three files. The `if/else` replaces the ternary because the branches + are statements, not values. +2. **Keep the microtask.** It is what defers the dispatch past input binding. + The larger fix — `start(seed)` on an application store called from + `ngOnInit` — belongs to the ADR-0007 migration, not here. This ticket makes + the existing seam correct; it does not move it. +3. **Turn the workaround into the regression test.** + `intake-wizard.component.spec.ts` now mounts through + `componentRef.setInput('seed', …)` via a `mountSeeded` helper, and a new + `honours the seed input` case asserts the machine state directly. Both fail + on the old code, which is the point. +4. **Do not touch `draftSync.enabled`.** `enabled: () => this.seed() === +initial` reads the input lazily inside a lambda called from an effect, so it + was already correct. Changing the input to `| null` is part of ADR-0007. + +## Verification performed + +- `npx ng test ssp` with the intake fix reverted: **2 failed** (both new spec + cases). With the fix: **319 passed**. That differential is the proof. +- `npm run ci --full` green, including 112 storybook a11y tests. The newly + rendered markup (``, error alerts, ``) + produced **no** axe violations — the predicted a11y fallout did not happen. +- Browser check against the built Storybook, seven seeded stories across all + three wizards, each asserted to contain text only reachable from its seed + (`Netwerkfout` for `Failed`, `referentienummer` for `Ingediend`, `Documenten` + for step 3), plus a negative control that step 1 does **not** show step 3's + title. All seven pass. + +## Trap for the next person + +`Failed`, `Submitted`, `Ingediend` and `Mislukt` are unreachable without a +network submit, so they can only be produced by a seed. If a future change +breaks the input path again, those four stories silently fall back to step 1 +and axe still passes. The spec added here is the guard; keep it. diff --git a/docs/project/readable-codebase/README.md b/docs/project/readable-codebase/README.md index 773b4fd..de98e1b 100644 --- a/docs/project/readable-codebase/README.md +++ b/docs/project/readable-codebase/README.md @@ -133,6 +133,7 @@ two. Note that RD-15 exists because 22 abandoned agent worktrees are still on di | RD-36 | `ui/dashboard/` → `ui/overzicht-secties/` + 2 stale `dashboard.page` paths | 04 | yes | todo | | RD-37 | **a11y:** 5 suppressions name a closed ticket — decide the `li[…]` host | 01 | yes | todo | | RD-38 | One member order for the 3 wizard containers + 2 pure extractions | 22, 23 | | done | +| RD-39 | **Bug:** the wizards' `seed` input never arrived (21 stories) | 38 | yes | done | The ID order already respects every dependency, so it is the recommended running order. diff --git a/libs/shared/docs/behaviour-spec.mdx b/libs/shared/docs/behaviour-spec.mdx index 632bb84..be0c6e2 100644 --- a/libs/shared/docs/behaviour-spec.mdx +++ b/libs/shared/docs/behaviour-spec.mdx @@ -20,7 +20,7 @@ tested where._ Every bullet below is a real test name from the suite — an `it()` title (frontend) or a test method name (backend), read as a sentence. Nothing here is hand-written prose: this page -**is** the suite, reshaped for a business reader. 562 frontend behaviours across +**is** the suite, reshaped for a business reader. 563 frontend behaviours across 9 contexts; 261 backend behaviours across 42 test classes. @@ -383,6 +383,7 @@ classes. #### IntakeWizardComponent +- honours the seed input - renders each field group as its own grey <fieldset> #### STEPS (fixed) and inline questions From 2aa343f255ba3dff9f9b95e786d43d5bcdf068fc Mon Sep 17 00:00:00 2001 From: Edwin van den Houdt Date: Tue, 8 Sep 2026 22:39:40 +0200 Subject: [PATCH 2/9] chore: cap container memory in docker compose The API container used server garbage collection. Server GC makes one heap per CPU, which is 22 heaps on this host. The two frontend containers sized the Node heap from host RAM (16 GB), because compose sets no cgroup limit. Set DOTNET_gcServer=0 and NODE_OPTIONS=--max-old-space-size=2048. This bounds the memory that `docker compose up` needs. Co-Authored-By: Claude Opus 5 --- docker-compose.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 2966c6d..4968a3a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,8 @@ services: command: dotnet run --project src/BigRegister.Api --urls http://+:5000 environment: - ASPNETCORE_ENVIRONMENT=Development + # ponytail: Server GC makes one heap per CPU (22 here); workstation GC makes one. + - DOTNET_gcServer=0 volumes: # ':z' relabels for SELinux (Fedora/RHEL); harmless on other hosts. # WP-22: no separate volume needed for the SQLite file — `dotnet run` sets @@ -41,6 +43,8 @@ services: # switcher actually switches. ponytail: `--no-fund --loglevel=error` silences npm 11 noise. command: sh -c "npm ci --no-fund --loglevel=error && npx ng build ssp --configuration development --localize && node scripts/serve-i18n.mjs" environment: + # ponytail: without a cgroup limit Node sizes its heap from host RAM (16 GB). + - NODE_OPTIONS=--max-old-space-size=2048 - PORT=4200 - API_PROXY_TARGET=http://api:5000 - APP_DIST_ROOT=dist/ssp/browser @@ -57,6 +61,8 @@ services: working_dir: /app command: sh -c "npm ci --no-fund --loglevel=error && npx ng build behandelportal --configuration development --localize && node scripts/serve-i18n.mjs" environment: + # ponytail: without a cgroup limit Node sizes its heap from host RAM (16 GB). + - NODE_OPTIONS=--max-old-space-size=2048 - PORT=4201 - API_PROXY_TARGET=http://api:5000 - APP_DIST_ROOT=dist/behandelportal/browser From 097e8468e094cafd336f3664db9f226497160f9b Mon Sep 17 00:00:00 2001 From: Edwin van den Houdt Date: Tue, 8 Sep 2026 22:51:04 +0200 Subject: [PATCH 3/9] fix: keuzelijst rows become the
  • (RD-37) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit app-choice-link rendered a component host between the keuzelijst
      and its
    • . This broke the axe list/listitem rule for assistive technology. Five story suppressions named WP-11 as the fix, but WP-11 closed with no open ticket left to own the defect. choice-link now uses selector: 'li[app-choice-link]', the same attribute-host pattern as application-link. The host carries the keuzelijst__list-item class; the template drops its own
    • . Position: relative stays on .keuzelijst__link so the stretched-link overlay still resolves against the card, not the host. aanvraag-block needed no component change: it renders a CIBG melding, never an
    • . Only its story wrapped it in a
        , which is what axe rejected. The wrapper is removed, and the four non-Concept stories are deleted — the component's template only renders for status Concept, so they rendered nothing. All five a11y: { disable: true } suppressions are gone, with no replacement. atomic-design.mdx now records that both molecules are the
      • , kept separate for the vendored CSS they bind, not for list semantics. npm run ci --full passes, axe included. Co-Authored-By: Claude Opus 5 --- .../aanvraag-block/aanvraag-block.stories.ts | 54 +------- .../wat-moet-ik-regelen.section.stories.ts | 8 -- .../RD-37-a11y-suppressions.md | 130 ++++++++++++++++++ docs/project/readable-codebase/README.md | 2 +- libs/shared/docs/atomic-design.mdx | 14 +- .../choice-link/choice-link.component.ts | 42 +++--- .../choice-link/choice-link.stories.ts | 10 +- .../choice-list/choice-list.stories.ts | 9 +- .../task-list/task-list.component.ts | 2 +- .../molecules/task-list/task-list.stories.ts | 6 - 10 files changed, 171 insertions(+), 106 deletions(-) create mode 100644 docs/project/readable-codebase/RD-37-a11y-suppressions.md diff --git a/apps/ssp/src/app/registratie/ui/aanvraag-block/aanvraag-block.stories.ts b/apps/ssp/src/app/registratie/ui/aanvraag-block/aanvraag-block.stories.ts index 449567c..c4fbe4b 100644 --- a/apps/ssp/src/app/registratie/ui/aanvraag-block/aanvraag-block.stories.ts +++ b/apps/ssp/src/app/registratie/ui/aanvraag-block/aanvraag-block.stories.ts @@ -17,57 +17,17 @@ const meta: Meta = { title: 'Domein/Registratie/Aanvraag Block', component: AanvraagBlockComponent, decorators: [applicationConfig({ providers: [provideRouter([])] })], - render: (args) => ({ - props: args, - // A row is an
      • — the keuzelijst styling needs the real list context. - template: `
        `, - }), - parameters: { - // Structural: app-aanvraag-block's host sits between the keuzelijst
          and its
        • - // — axe's list/listitem rule needs them adjacent regardless of `display:contents`. - // WP-11 (CIBG markup fidelity) reworks this markup; see docs/project/backlog/WP-11-markup-fidelity.md. - a11y: { disable: true }, - }, + // A Concept renders as a CIBG melding (block element), not a keuzelijst
        • — no
            + // wrapper. Production agrees: mijn-aanvragen.section.ts renders this block for concepten + // only, outside any list. + render: (args) => ({ props: args, template: `` }), }; export default meta; type Story = StoryObj; -// One story per status variant; the block renders its own body + actions. -// A Concept renders as a CIBG melding (block element), not a keuzelijst
          • — no
              wrapper. +// The whole template sits inside `@if (aanvraag().status.tag === 'Concept')`, so this is +// the only status that renders anything. Submitted/resolved aanvragen render through +// application-link, which has its own stories. export const Concept: Story = { args: { aanvraag: { ...base, status: { tag: 'Concept', stepIndex: 1, stepCount: 3 } } }, - render: (args) => ({ props: args, template: `` }), -}; -export const InBehandelingAuto: Story = { - args: { - aanvraag: { - ...base, - status: { tag: 'InBehandeling', referentie: 'BIG-2026-456789', manual: false }, - }, - }, -}; -export const InBehandelingManual: Story = { - args: { - aanvraag: { - ...base, - type: 'registratie', - status: { tag: 'InBehandeling', referentie: 'BIG-2026-456789', manual: true }, - }, - }, -}; -export const Goedgekeurd: Story = { - args: { aanvraag: { ...base, status: { tag: 'Goedgekeurd', referentie: 'BIG-2026-456789' } } }, -}; -export const Afgewezen: Story = { - args: { - aanvraag: { - ...base, - type: 'herregistratie', - status: { - tag: 'Afgewezen', - referentie: 'BIG-2026-456789', - reden: 'Aanvraag afgewezen: geen gewerkte uren geregistreerd.', - }, - }, - }, }; diff --git a/apps/ssp/src/app/registratie/ui/dashboard/wat-moet-ik-regelen.section.stories.ts b/apps/ssp/src/app/registratie/ui/dashboard/wat-moet-ik-regelen.section.stories.ts index 05fed6a..2bba22d 100644 --- a/apps/ssp/src/app/registratie/ui/dashboard/wat-moet-ik-regelen.section.stories.ts +++ b/apps/ssp/src/app/registratie/ui/dashboard/wat-moet-ik-regelen.section.stories.ts @@ -66,14 +66,6 @@ export const MetTaken: Story = { ], }), ], - parameters: { - // Structural: app-choice-link's host sits between the keuzelijst
                and its
              • - // — axe's list/listitem rule needs them adjacent regardless of `display:contents`. - // Same pre-existing gap as task-list.stories.ts and choice-list.stories.ts. WP-11 - // (CIBG markup fidelity) reworks this markup; see - // docs/project/backlog/WP-11-markup-fidelity.md. - a11y: { disable: true }, - }, }; export const NietsOpenstaand: Story = { decorators: [ diff --git a/docs/project/readable-codebase/RD-37-a11y-suppressions.md b/docs/project/readable-codebase/RD-37-a11y-suppressions.md new file mode 100644 index 0000000..253e376 --- /dev/null +++ b/docs/project/readable-codebase/RD-37-a11y-suppressions.md @@ -0,0 +1,130 @@ +# RD-37 — Five a11y suppressions name a ticket that closed + +Status: done +Phase: 5 — fix the docs that describe this flow + +## Why + +Five stories carry `a11y: { disable: true }`. Four of the reasons say "WP-11 (CIBG markup +fidelity) reworks this markup". WP-11 is `Status: done`, and so is WP-13, the gap register that +WP-11 handed its remainder to. No open ticket owns the defect. The README rule — "no check +disabled without a reference to the ticket that removes it" — holds only in letter. + +RD-30 archives `docs/project/backlog/`. This ticket runs first, so the archive move does not +rewrite five paths that must disappear. + +The defect is shipped, not story-only. `app-choice-link` renders a component host between the +keuzelijst `
                  ` and its `
                • `. This breaks the axe `list`/`listitem` rule for assistive +technology. `display: contents` does not repair it. + +## Read first + +- `docs/project/readable-codebase/PLAN.md`, phase 5 item 0 (line 790). +- `libs/shared/src/ui/molecules/choice-link/choice-link.component.ts` — the defect. +- `libs/shared/src/ui/molecules/application-link/application-link.component.ts:18` — the + precedent. WP-11 made the host **be** the `
                • `. That component is axe-clean today. +- `libs/shared/docs/atomic-design.mdx:113` — the convergence table row that calls the split + deliberate. + +## The question this ticket had to answer first + +Does an `li[…]` attribute host still match the vendored CIBG keuzelijst CSS? + +**Yes.** Verified against `public/cibg-huisstijl/css/huisstijl.css`. Every keuzelijst rule keys +off a bare class: + +``` +.keuzelijst__list{padding-left:0} +.keuzelijst__list-item{list-style:none;margin-bottom:1.5rem;position:relative} +.keuzelijst__link{…} +.keuzelijst__link:after{…} .keuzelijst__link:focus,.keuzelijst__link:hover{…} +``` + +There is no `ul > li` child combinator and no `li a` descendant chain. This is the difference +from the aanvragen pattern, whose vendored chain **is** `.dashboard-block.applications li a`. +An attribute host on the `
                • ` therefore keeps every keuzelijst selector matching, as long as +the class `keuzelijst__list-item` moves to the host element. + +## Decisions (pre-made, do not relitigate) + +1. **`choice-link` becomes `selector: 'li[app-choice-link]'`.** The host carries the class + through `host: { class: 'keuzelijst__list-item' }`. The template drops its outer `
                • `. + `:host { display: contents }` goes away, because the host is now the list item. +2. **Keep `position: relative` on `.keuzelijst__link`.** The title is a `.stretched-link`. Its + `::after` overlay must resolve against the card, not against the `
                • `. Do not move that + rule to the host. +3. **`aanvraag-block` needs no component change. Its suppression reason is wrong.** The + component renders a CIBG melding (`app-alert`), never an `
                • `. Only the story's meta + `render` wraps it in `
                    `, and that wrapper is what axe rejects. + Production agrees: `mijn-aanvragen.section.ts:44` renders the block for `concepten_()` only, + outside any list. Delete the wrapper from the meta render. +4. **Delete the four non-Concept stories in `aanvraag-block.stories.ts`.** The whole template + sits inside `@if (aanvraag().status.tag === 'Concept')`, so `InBehandelingAuto`, + `InBehandelingManual`, `Goedgekeurd` and `Afgewezen` render nothing at all. Keep `Concept`, + and give it the meta render. Submitted and resolved aanvragen render through + `application-link`, which has its own stories. +5. **Correct `atomic-design.mdx:113`.** After this ticket both molecules **are** the `
                  • `. + The pair stays separate because they bind different vendored patterns, not because of list + semantics. Rewrite that half of the cell; keep the verdict. +6. **All five suppressions go.** No suppression, no replacement ticket. If `ci --full` still + reports a violation, stop and report it. Do not re-add a disable. + +## Files + +- `libs/shared/src/ui/molecules/choice-link/choice-link.component.ts` — host, class, template, + the header comment. +- `libs/shared/src/ui/molecules/task-list/task-list.component.ts:25` — the one production call + site: `` becomes `
                  • `. +- `libs/shared/src/ui/molecules/choice-link/choice-link.stories.ts` — call site + suppression. +- `libs/shared/src/ui/molecules/choice-list/choice-list.stories.ts` — call sites + suppression. +- `libs/shared/src/ui/molecules/task-list/task-list.stories.ts` — suppression. +- `apps/ssp/src/app/registratie/ui/dashboard/wat-moet-ik-regelen.section.stories.ts` — + suppression. +- `apps/ssp/src/app/registratie/ui/aanvraag-block/aanvraag-block.stories.ts` — wrapper, four + stories, suppression. +- `libs/shared/docs/atomic-design.mdx` — the convergence row. + +## Steps + +1. Convert `choice-link` to the `li[…]` host. +2. Update the one production call site and the two story templates. An attribute host needs a + closing tag: `
                  • `, not a self-closing element. +3. Fix `aanvraag-block.stories.ts` per decisions 3 and 4. +4. Delete all five `a11y: { disable: true }` blocks and their comments. +5. Correct the `atomic-design.mdx` row. +6. Run `npm run ci --full`. + +## Acceptance criteria + +- [x] No `a11y: { disable: true }` remains in `apps/` or `libs/`. +- [x] `grep -rn "WP-11" apps libs` returns nothing that points at the archived backlog. +- [x] `npm run ci --full` is green, axe included. +- [x] The keuzelijst still looks unchanged: chevron, hover accent, focus accent, and the + non-interactive `--static` row. + +## Verification + +1. `npm run ci --full`. +2. `npm run storybook` — Choice Link, Choice List, Task List and Wat Moet Ik Regelen run with + the a11y addon on. Check the three Choice Link stories by eye: `Navigatie`, `Actie` and + `NietInteractief` must keep their current appearance. +3. `npm start`, open `http://localhost:4200/dashboard` — the "Wat moet ik regelen" list renders + as before, and each row is still clickable over its whole surface. + +## Out of scope + +- `application-link` and `application-list`. Both are already axe-clean. +- The `--static` modifier and the CIBG-gap register. This ticket moves a host element; it adds + no new hand-rolled surface. + +## Risks + +1. **A self-closing attribute host silently renders nothing.** Angular needs + `
                  • `. The build does not fail; the row disappears. Check the + dashboard by eye, per Verification step 3. +2. **`stretched-link` covers the wrong box.** If `position: relative` lands on the host instead + of on `.keuzelijst__link`, the whole `
                  • ` becomes the click target, including its + `margin-bottom`. Keep the rule where it is. +3. **The `choiceActions` slot must stay above the overlay.** It projects inside + `.keuzelijst__link` and relies on its own `position: relative; z-index: 2` at the call site. + The host move must not change the projection point. diff --git a/docs/project/readable-codebase/README.md b/docs/project/readable-codebase/README.md index de98e1b..18fd57b 100644 --- a/docs/project/readable-codebase/README.md +++ b/docs/project/readable-codebase/README.md @@ -131,7 +131,7 @@ two. Note that RD-15 exists because 22 abandoned agent worktrees are still on di | RD-34 | _(optional)_ `NO_SUBORGS`/`NO_TABLES` become `RemoteData.Empty` | 11 | | todo | | RD-35 | _(optional, last, alone)_ upload `type:` discriminant to `tag:` | 27 | | todo | | RD-36 | `ui/dashboard/` → `ui/overzicht-secties/` + 2 stale `dashboard.page` paths | 04 | yes | todo | -| RD-37 | **a11y:** 5 suppressions name a closed ticket — decide the `li[…]` host | 01 | yes | todo | +| RD-37 | **a11y:** 5 suppressions name a closed ticket — decide the `li[…]` host | 01 | yes | done | | RD-38 | One member order for the 3 wizard containers + 2 pure extractions | 22, 23 | | done | | RD-39 | **Bug:** the wizards' `seed` input never arrived (21 stories) | 38 | yes | done | diff --git a/libs/shared/docs/atomic-design.mdx b/libs/shared/docs/atomic-design.mdx index c37a72f..58abbc0 100644 --- a/libs/shared/docs/atomic-design.mdx +++ b/libs/shared/docs/atomic-design.mdx @@ -108,13 +108,13 @@ the next person doesn't spend an afternoon re-deciding. (Deliberate CIBG-specifi live in [CIBG gaps](?path=/docs/foundations-cibg-gap-register--docs); the FE⇄DS "same shape, different context" cases in [Domain-driven design](?path=/docs/foundations-domain-driven-design--docs).) -| Pair | Why kept separate | -| ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `choice-link` vs `application-link` | Share the same `to`/`clickable`/`activate` navigation triad, but bind **different vendored patterns** — CIBG _Keuzelijst_ (`.keuzelijst__link`, `.stretched-link`) vs _Aanvragen_ (`.dashboard-block.applications li a`) — with different list/host semantics (`app-choice-link` renders an inner `
                  • `; `application-link` **is** the `
                  • `). Merging would fight the vendored CSS. Extract the shared triad into a mixin only if it grows. | -| `text-input` / `radio-group` / `checkbox` | Share only the standard Angular **ControlValueAccessor** boilerplate (the `writeValue`/`registerOn*`/`setDisabledState` block). They render genuinely different controls, so they stay three atoms. A base CVA class is the only DRY move — a refactor, not a component merge, and not worth it at three. | -| `button variant="subtle"` (`.btn-link`) vs `app-link` | A subtle button _looks_ like a link but is an **action** (`