Rijkshuisstijl restyle + wizard fixes
Chrome: two-tier Rijksoverheid header (white brand bar + lint-blue
breadcrumb bar, route-driven), dark multi-column footer, white page
surface. Session shown via a shared SESSION_PORT token (keeps shared/
free of the auth context).
Overview ("Mijn overzicht") rebuilt to the NL Design System #392 pattern:
side-nav + "Wat moet ik regelen" task list (derived) + "Mijn registratie"
cards. New shared components: card, task-list, side-nav; pure
tasksFromProfile (+spec).
Wizards: grey form panel, connected numbered stepper, form-field
"(verplicht)" markers + styled description/error, full-width inputs.
Propagated to login, detail, change-request, address-fields.
Bug fixes:
- wizard-shell: add FormsModule so NgForm intercepts submit (wizards now
advance; no native GET leaking choices into the URL).
- wizard-shell: error-summary links focus the field instead of navigating
(a fragment href resolved against <base href="/"> reloaded to "/" and
bounced to login).
- wizard-shell: error-summary focus only on the rising edge, so typing
while errors are shown no longer scrolls the page up.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { Component, ElementRef, effect, input, output, untracked, viewChild } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { SpinnerComponent } from '@shared/ui/spinner/spinner.component';
|
||||
@@ -25,7 +26,7 @@ export type WizardStatus = 'editing' | 'submitting' | 'submitted' | 'failed';
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-wizard-shell',
|
||||
imports: [ButtonComponent, AlertComponent, SpinnerComponent, StepperComponent],
|
||||
imports: [FormsModule, ButtonComponent, AlertComponent, SpinnerComponent, StepperComponent],
|
||||
styles: [`
|
||||
.es-title{margin:0 0 var(--rhc-space-max-sm)}
|
||||
.es-list{margin:0;padding-inline-start:var(--rhc-space-max-xl)}
|
||||
@@ -41,13 +42,13 @@ export type WizardStatus = 'editing' | 'submitting' | 'submitted' | 'failed';
|
||||
<h3 id="wizard-error-title" class="rhc-heading nl-heading--level-3 es-title">Er ging iets mis met uw invoer</h3>
|
||||
<ul class="es-list">
|
||||
@for (e of errors(); track e.id) {
|
||||
<li><a [href]="'#' + e.id">{{ e.message }}</a></li>
|
||||
<li><a [href]="'#' + e.id" (click)="goToField($event, e.id)">{{ e.message }}</a></li>
|
||||
}
|
||||
</ul>
|
||||
</app-alert>
|
||||
</div>
|
||||
}
|
||||
<form (ngSubmit)="primary.emit()" class="app-form">
|
||||
<form (ngSubmit)="primary.emit()" class="app-form app-form-panel">
|
||||
<ng-content />
|
||||
<div class="app-button-row app-button-row--spaced">
|
||||
@if (canGoBack()) {
|
||||
@@ -89,6 +90,14 @@ export class WizardShellComponent {
|
||||
cancel = output<void>();
|
||||
retry = output<void>();
|
||||
|
||||
/** Error-summary link: focus the field instead of letting the browser navigate.
|
||||
A fragment href resolves against <base href="/">, not the current route, so
|
||||
a real navigation would reload to "/" and bounce to login. */
|
||||
protected goToField(ev: Event, id: string) {
|
||||
ev.preventDefault();
|
||||
document.getElementById(id)?.focus(); // focus() scrolls the input into view
|
||||
}
|
||||
|
||||
private stepHeading = viewChild<ElementRef<HTMLElement>>('stepHeading');
|
||||
private errorSummary = viewChild<ElementRef<HTMLElement>>('errorSummary');
|
||||
|
||||
@@ -102,14 +111,18 @@ export class WizardShellComponent {
|
||||
if (firstStep) { firstStep = false; return; }
|
||||
untracked(() => queueMicrotask(() => this.stepHeading()?.nativeElement.focus()));
|
||||
});
|
||||
// A11y: when validation errors appear (after a failed submit), move focus to
|
||||
// the error summary so it's announced. The reducer returns a fresh errors
|
||||
// object per attempt, so resubmitting the same invalid step re-announces.
|
||||
// A11y: when validation errors first appear (after a failed submit), move
|
||||
// focus to the error summary so it's announced. Only on the rising edge
|
||||
// (none → some): typing rebuilds the errors array each keystroke, and
|
||||
// re-focusing then would scroll the page up mid-edit. The summary keeps
|
||||
// role="alert", so content changes are still announced without the jump.
|
||||
let firstErr = true;
|
||||
let hadErrors = false;
|
||||
effect(() => {
|
||||
const has = this.errors().length > 0;
|
||||
if (firstErr) { firstErr = false; return; }
|
||||
if (has) untracked(() => queueMicrotask(() => this.errorSummary()?.nativeElement.focus()));
|
||||
if (firstErr) { firstErr = false; hadErrors = has; return; }
|
||||
if (has && !hadErrors) untracked(() => queueMicrotask(() => this.errorSummary()?.nativeElement.focus()));
|
||||
hadErrors = has;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user