fix(cibg): wizard fields render as grey fieldset groups

Wizard steps rendered bare .form-group divs, so CIBG's
".form-horizontal fieldset { background:#f1f5f9; margin-bottom:1.25em }"
never matched and inputs showed on white instead of the grey CIBG surface.

Wrap each logical field group per step in a <fieldset> (intake, herregistratie
and registratie wizards); CIBG then gives every group its grey surface with a
1.25em gap between groups. The shell stays group-agnostic (no outer fieldset,
which would hide the white gaps). address-fields already used a <fieldset>.

Adds intake-wizard.component.spec.ts asserting the buitenland step renders its
groups as separate fieldsets (guards against the wrapping being dropped again).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
eho
2026-07-20 13:39:13 +02:00
co-authored by Claude Opus 4.8
parent f6c837f281
commit 950fb5f0b2
5 changed files with 292 additions and 226 deletions
@@ -64,59 +64,63 @@ import { UploadState, initialUpload, deliveryRefs } from '@shared/upload/upload.
> >
@switch (step()) { @switch (step()) {
@case (1) { @case (1) {
<app-form-field <fieldset>
i18n-label="@@herregWizard.urenLabel" <app-form-field
label="Gewerkte uren (afgelopen 5 jaar)" i18n-label="@@herregWizard.urenLabel"
fieldId="uren" label="Gewerkte uren (afgelopen 5 jaar)"
required fieldId="uren"
[error]="errUren()" required
> [error]="errUren()"
<app-text-input >
inputId="uren" <app-text-input
[ngModel]="draft().uren" inputId="uren"
(ngModelChange)="dispatch({ tag: 'SetField', key: 'uren', value: $event })" [ngModel]="draft().uren"
name="uren" (ngModelChange)="dispatch({ tag: 'SetField', key: 'uren', value: $event })"
[invalid]="!!errUren()" name="uren"
i18n-placeholder="@@herregWizard.urenPlaceholder" [invalid]="!!errUren()"
placeholder="bijv. 4160" i18n-placeholder="@@herregWizard.urenPlaceholder"
/> placeholder="bijv. 4160"
</app-form-field> />
<app-form-field </app-form-field>
i18n-label="@@herregWizard.jarenLabel" <app-form-field
label="Aantal jaren werkzaam" i18n-label="@@herregWizard.jarenLabel"
fieldId="jaren" label="Aantal jaren werkzaam"
required fieldId="jaren"
[error]="errJaren()" required
> [error]="errJaren()"
<app-text-input >
inputId="jaren" <app-text-input
[ngModel]="draft().jaren" inputId="jaren"
(ngModelChange)="dispatch({ tag: 'SetField', key: 'jaren', value: $event })" [ngModel]="draft().jaren"
name="jaren" (ngModelChange)="dispatch({ tag: 'SetField', key: 'jaren', value: $event })"
[invalid]="!!errJaren()" name="jaren"
i18n-placeholder="@@herregWizard.jarenPlaceholder" [invalid]="!!errJaren()"
placeholder="bijv. 5" i18n-placeholder="@@herregWizard.jarenPlaceholder"
/> placeholder="bijv. 5"
</app-form-field> />
</app-form-field>
</fieldset>
} }
@case (2) { @case (2) {
<app-form-field <fieldset>
i18n-label="@@herregWizard.puntenLabel" <app-form-field
label="Behaalde nascholingspunten" i18n-label="@@herregWizard.puntenLabel"
fieldId="punten" label="Behaalde nascholingspunten"
required fieldId="punten"
[error]="errPunten()" required
> [error]="errPunten()"
<app-text-input >
inputId="punten" <app-text-input
[ngModel]="draft().punten" inputId="punten"
(ngModelChange)="dispatch({ tag: 'SetField', key: 'punten', value: $event })" [ngModel]="draft().punten"
name="punten" (ngModelChange)="dispatch({ tag: 'SetField', key: 'punten', value: $event })"
[invalid]="!!errPunten()" name="punten"
i18n-placeholder="@@herregWizard.puntenPlaceholder" [invalid]="!!errPunten()"
placeholder="bijv. 200" i18n-placeholder="@@herregWizard.puntenPlaceholder"
/> placeholder="bijv. 200"
</app-form-field> />
</app-form-field>
</fieldset>
} }
@case (3) { @case (3) {
<app-document-upload <app-document-upload
@@ -0,0 +1,36 @@
import { provideHttpClient } from '@angular/common/http';
import { TestBed } from '@angular/core/testing';
import { describe, expect, it } from 'vitest';
import { provideApiClient } from '@shared/infrastructure/api-client.provider';
import { IntakeWizardComponent } from './intake-wizard.component';
import { IntakeState } from '@herregistratie/domain/intake.machine';
// Regression: wizard steps must render their logical field groups as separate CIBG grey
// <fieldset> blocks (`.form-horizontal fieldset` ⇒ #f1f5f9, 1.25em gap). If the fieldset
// wrapping is dropped, the inputs revert to bare white. The buitenland step with
// buitenlandGewerkt='ja' has two groups (the question + the land/uren follow-up), so it
// must render ≥2 fieldsets, each holding a form-group.
const buitenlandJa: IntakeState = {
tag: 'Answering',
answers: { buitenlandGewerkt: 'ja' },
cursor: 0,
errors: {},
scholingThreshold: 1000,
};
describe('IntakeWizardComponent', () => {
it('renders each field group as its own grey <fieldset>', () => {
TestBed.configureTestingModule({
providers: [provideHttpClient(), provideApiClient()],
});
const fixture = TestBed.createComponent(IntakeWizardComponent);
fixture.componentInstance.dispatch({ tag: 'Seed', state: buitenlandJa });
fixture.detectChanges();
const fieldsets: HTMLElement[] = Array.from(
fixture.nativeElement.querySelectorAll('form.form-horizontal fieldset'),
);
expect(fieldsets.length).toBeGreaterThanOrEqual(2);
fieldsets.forEach((fs) => expect(fs.querySelector('.form-group')).toBeTruthy());
});
});
@@ -71,105 +71,115 @@ import { IntakePolicyStore } from '@herregistratie/application/intake-policy.sto
> >
@switch (step()) { @switch (step()) {
@case ('buitenland') { @case ('buitenland') {
<app-form-field <fieldset>
i18n-label="@@intake.q.buitenland" <app-form-field
label="Heeft u de afgelopen 5 jaar buiten Nederland gewerkt?" i18n-label="@@intake.q.buitenland"
fieldId="buitenlandGewerkt" label="Heeft u de afgelopen 5 jaar buiten Nederland gewerkt?"
required fieldId="buitenlandGewerkt"
[error]="err('buitenlandGewerkt')" required
> [error]="err('buitenlandGewerkt')"
<app-radio-group >
name="buitenlandGewerkt" <app-radio-group
[options]="jaNee" name="buitenlandGewerkt"
[ngModel]="answers().buitenlandGewerkt ?? ''" [options]="jaNee"
(ngModelChange)="set('buitenlandGewerkt', $event)" [ngModel]="answers().buitenlandGewerkt ?? ''"
/> (ngModelChange)="set('buitenlandGewerkt', $event)"
</app-form-field> />
</app-form-field>
</fieldset>
@if (answers().buitenlandGewerkt === 'ja') { @if (answers().buitenlandGewerkt === 'ja') {
<app-form-field <fieldset>
i18n-label="@@intake.q.land" <app-form-field
label="In welk land?" i18n-label="@@intake.q.land"
fieldId="land" label="In welk land?"
required fieldId="land"
[error]="err('land')" required
> [error]="err('land')"
<app-text-input >
inputId="land" <app-text-input
[ngModel]="answers().land ?? ''" inputId="land"
(ngModelChange)="set('land', $event)" [ngModel]="answers().land ?? ''"
name="land" (ngModelChange)="set('land', $event)"
i18n-placeholder="@@intake.q.landPlaceholder" name="land"
placeholder="bijv. België" i18n-placeholder="@@intake.q.landPlaceholder"
/> placeholder="bijv. België"
</app-form-field> />
<app-form-field </app-form-field>
i18n-label="@@intake.q.buitenlandseUren" <app-form-field
label="Hoeveel uur heeft u daar gewerkt?" i18n-label="@@intake.q.buitenlandseUren"
fieldId="buitenlandseUren" label="Hoeveel uur heeft u daar gewerkt?"
required fieldId="buitenlandseUren"
[error]="err('buitenlandseUren')" required
> [error]="err('buitenlandseUren')"
<app-text-input >
inputId="buitenlandseUren" <app-text-input
[ngModel]="answers().buitenlandseUren ?? ''" inputId="buitenlandseUren"
(ngModelChange)="set('buitenlandseUren', $event)" [ngModel]="answers().buitenlandseUren ?? ''"
name="buitenlandseUren" (ngModelChange)="set('buitenlandseUren', $event)"
i18n-placeholder="@@intake.q.buitenlandseUrenPlaceholder" name="buitenlandseUren"
placeholder="bijv. 800" i18n-placeholder="@@intake.q.buitenlandseUrenPlaceholder"
/> placeholder="bijv. 800"
</app-form-field> />
</app-form-field>
</fieldset>
} }
} }
@case ('werk') { @case ('werk') {
<app-form-field <fieldset>
i18n-label="@@intake.q.urenNl"
label="Gewerkte uren in Nederland (afgelopen 5 jaar)"
fieldId="uren"
required
[error]="err('uren')"
>
<app-text-input
inputId="uren"
[ngModel]="answers().uren ?? ''"
(ngModelChange)="set('uren', $event)"
name="uren"
i18n-placeholder="@@intake.q.urenNlPlaceholder"
placeholder="bijv. 4160"
/>
</app-form-field>
@if (scholingZichtbaar()) {
<app-form-field <app-form-field
i18n-label="@@intake.q.scholing" i18n-label="@@intake.q.urenNl"
label="U werkte relatief weinig uren. Heeft u aanvullende scholing gevolgd?" label="Gewerkte uren in Nederland (afgelopen 5 jaar)"
fieldId="scholingGevolgd" fieldId="uren"
required required
[error]="err('scholingGevolgd')" [error]="err('uren')"
>
<app-radio-group
name="scholingGevolgd"
[options]="jaNee"
[ngModel]="answers().scholingGevolgd ?? ''"
(ngModelChange)="set('scholingGevolgd', $event)"
/>
</app-form-field>
}
@if (answers().scholingGevolgd === 'ja') {
<app-form-field
i18n-label="@@intake.q.punten"
label="Behaalde nascholingspunten"
fieldId="punten"
required
[error]="err('punten')"
> >
<app-text-input <app-text-input
inputId="punten" inputId="uren"
[ngModel]="answers().punten ?? ''" [ngModel]="answers().uren ?? ''"
(ngModelChange)="set('punten', $event)" (ngModelChange)="set('uren', $event)"
name="punten" name="uren"
i18n-placeholder="@@intake.q.puntenPlaceholder" i18n-placeholder="@@intake.q.urenNlPlaceholder"
placeholder="bijv. 200" placeholder="bijv. 4160"
/> />
</app-form-field> </app-form-field>
</fieldset>
@if (scholingZichtbaar()) {
<fieldset>
<app-form-field
i18n-label="@@intake.q.scholing"
label="U werkte relatief weinig uren. Heeft u aanvullende scholing gevolgd?"
fieldId="scholingGevolgd"
required
[error]="err('scholingGevolgd')"
>
<app-radio-group
name="scholingGevolgd"
[options]="jaNee"
[ngModel]="answers().scholingGevolgd ?? ''"
(ngModelChange)="set('scholingGevolgd', $event)"
/>
</app-form-field>
</fieldset>
}
@if (answers().scholingGevolgd === 'ja') {
<fieldset>
<app-form-field
i18n-label="@@intake.q.punten"
label="Behaalde nascholingspunten"
fieldId="punten"
required
[error]="err('punten')"
>
<app-text-input
inputId="punten"
[ngModel]="answers().punten ?? ''"
(ngModelChange)="set('punten', $event)"
name="punten"
i18n-placeholder="@@intake.q.puntenPlaceholder"
placeholder="bijv. 200"
/>
</app-form-field>
</fieldset>
} }
} }
@case ('review') { @case ('review') {
@@ -130,40 +130,44 @@ const NL_TAALVAARDIGHEID_VRAAG = 'nl-taalvaardigheid';
}" }"
(fieldChange)="set($event.key, $event.value)" (fieldChange)="set($event.key, $event.value)"
/> />
<app-form-field <fieldset>
i18n-label="@@regWizard.correspondentieLabel"
label="Hoe wilt u correspondentie ontvangen?"
fieldId="correspondentie"
required
[error]="err('correspondentie')"
>
<app-radio-group
name="correspondentie"
[options]="kanalen"
[invalid]="!!err('correspondentie')"
[ngModel]="draft().correspondentie ?? ''"
(ngModelChange)="setKanaal($event)"
/>
</app-form-field>
@if (draft().correspondentie === 'email') {
<app-form-field <app-form-field
i18n-label="@@regWizard.emailLabel" i18n-label="@@regWizard.correspondentieLabel"
label="E-mailadres" label="Hoe wilt u correspondentie ontvangen?"
fieldId="email" fieldId="correspondentie"
required required
[error]="err('email')" [error]="err('correspondentie')"
> >
<app-text-input <app-radio-group
inputId="email" name="correspondentie"
type="email" [options]="kanalen"
[invalid]="!!err('email')" [invalid]="!!err('correspondentie')"
[ngModel]="draft().email ?? ''" [ngModel]="draft().correspondentie ?? ''"
(ngModelChange)="set('email', $event)" (ngModelChange)="setKanaal($event)"
name="email"
i18n-placeholder="@@regWizard.emailPlaceholder"
placeholder="naam@voorbeeld.nl"
/> />
</app-form-field> </app-form-field>
</fieldset>
@if (draft().correspondentie === 'email') {
<fieldset>
<app-form-field
i18n-label="@@regWizard.emailLabel"
label="E-mailadres"
fieldId="email"
required
[error]="err('email')"
>
<app-text-input
inputId="email"
type="email"
[invalid]="!!err('email')"
[ngModel]="draft().email ?? ''"
(ngModelChange)="set('email', $event)"
name="email"
i18n-placeholder="@@regWizard.emailPlaceholder"
placeholder="naam@voorbeeld.nl"
/>
</app-form-field>
</fieldset>
} }
} }
} }
@@ -171,21 +175,23 @@ const NL_TAALVAARDIGHEID_VRAAG = 'nl-taalvaardigheid';
<app-async [data]="lookupRd()"> <app-async [data]="lookupRd()">
<ng-template appAsyncLoaded> <ng-template appAsyncLoaded>
@if (duoData(); as data) { @if (duoData(); as data) {
<app-form-field <fieldset>
i18n-label="@@regWizard.diplomaLabel" <app-form-field
label="Kies het diploma waarmee u zich wilt registreren" i18n-label="@@regWizard.diplomaLabel"
fieldId="diploma" label="Kies het diploma waarmee u zich wilt registreren"
required fieldId="diploma"
[error]="err('diploma')" required
> [error]="err('diploma')"
<app-radio-group >
name="diploma" <app-radio-group
[options]="diplomaOptions(data)" name="diploma"
[invalid]="!!err('diploma')" [options]="diplomaOptions(data)"
[ngModel]="diplomaKeuze()" [invalid]="!!err('diploma')"
(ngModelChange)="onDiplomaKeuze(data, $event)" [ngModel]="diplomaKeuze()"
/> (ngModelChange)="onDiplomaKeuze(data, $event)"
</app-form-field> />
</app-form-field>
</fieldset>
@if (handmatigActief()) { @if (handmatigActief()) {
<app-alert type="warning" i18n="@@regWizard.handmatigWaarschuwing" <app-alert type="warning" i18n="@@regWizard.handmatigWaarschuwing"
@@ -193,20 +199,22 @@ const NL_TAALVAARDIGHEID_VRAAG = 'nl-taalvaardigheid';
uw beroep en beantwoord de aanvullende vragen; uw aanvraag wordt daarna uw beroep en beantwoord de aanvullende vragen; uw aanvraag wordt daarna
handmatig beoordeeld.</app-alert handmatig beoordeeld.</app-alert
> >
<app-form-field <fieldset>
i18n-label="@@regWizard.beroepLabel" <app-form-field
label="Voor welk beroep wilt u zich registreren?" i18n-label="@@regWizard.beroepLabel"
fieldId="hm-beroep" label="Voor welk beroep wilt u zich registreren?"
[error]="err('diploma')" fieldId="hm-beroep"
> [error]="err('diploma')"
<app-radio-group >
name="hm-beroep" <app-radio-group
[options]="beroepOptions(data)" name="hm-beroep"
[invalid]="!!err('diploma')" [options]="beroepOptions(data)"
[ngModel]="draft().beroep ?? ''" [invalid]="!!err('diploma')"
(ngModelChange)="dispatch({ tag: 'DeclareerBeroep', beroep: $event })" [ngModel]="draft().beroep ?? ''"
/> (ngModelChange)="dispatch({ tag: 'DeclareerBeroep', beroep: $event })"
</app-form-field> />
</app-form-field>
</fieldset>
} @else if (draft().beroep) { } @else if (draft().beroep) {
<dl class="mb-0 app-section"> <dl class="mb-0 app-section">
<div <div
@@ -218,35 +226,39 @@ const NL_TAALVAARDIGHEID_VRAAG = 'nl-taalvaardigheid';
</dl> </dl>
} }
@for (q of actieveVragen(data); track q.id) { @if (actieveVragen(data).length) {
<app-form-field <fieldset>
[label]="q.vraag" @for (q of actieveVragen(data); track q.id) {
[fieldId]="'vraag-' + q.id" <app-form-field
[error]="vraagErr(q.id)" [label]="q.vraag"
> [fieldId]="'vraag-' + q.id"
@if (q.type === 'ja-nee') { [error]="vraagErr(q.id)"
<app-radio-group >
[name]="'vraag-' + q.id" @if (q.type === 'ja-nee') {
[options]="jaNee" <app-radio-group
[invalid]="!!vraagErr(q.id)" [name]="'vraag-' + q.id"
[ngModel]="antwoord(q.id)" [options]="jaNee"
(ngModelChange)=" [invalid]="!!vraagErr(q.id)"
dispatch({ tag: 'SetAntwoord', vraagId: q.id, value: $event }) [ngModel]="antwoord(q.id)"
" (ngModelChange)="
[ngModelOptions]="{ standalone: true }" dispatch({ tag: 'SetAntwoord', vraagId: q.id, value: $event })
/> "
} @else { [ngModelOptions]="{ standalone: true }"
<app-text-input />
[inputId]="'vraag-' + q.id" } @else {
[invalid]="!!vraagErr(q.id)" <app-text-input
[ngModel]="antwoord(q.id)" [inputId]="'vraag-' + q.id"
(ngModelChange)=" [invalid]="!!vraagErr(q.id)"
dispatch({ tag: 'SetAntwoord', vraagId: q.id, value: $event }) [ngModel]="antwoord(q.id)"
" (ngModelChange)="
[ngModelOptions]="{ standalone: true }" dispatch({ tag: 'SetAntwoord', vraagId: q.id, value: $event })
/> "
[ngModelOptions]="{ standalone: true }"
/>
}
</app-form-field>
} }
</app-form-field> </fieldset>
} }
} }
</ng-template> </ng-template>
@@ -90,6 +90,10 @@ export type WizardStatus = 'editing' | 'submitting' | 'submitted' | 'failed';
<span class="meta" i18n="@@form.verplichteVelden">* verplichte velden</span> <span class="meta" i18n="@@form.verplichteVelden">* verplichte velden</span>
</div> </div>
</div> </div>
<!-- Wizard pages wrap their field groups in <fieldset>s; CIBG's
".form-horizontal fieldset" gives each a grey #f1f5f9 surface with a 1.25em
gap. The shell stays group-agnostic and does NOT add its own fieldset (an
outer grey fieldset would hide the white gaps between the page groups). -->
<ng-content /> <ng-content />
<hr /> <hr />
<div class="d-flex flex-column flex-sm-row-reverse"> <div class="d-flex flex-column flex-sm-row-reverse">