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';
import { StepperComponent } from '@shared/ui/stepper/stepper.component';
/** CIBG procesnavigatie primary-button copy for a non-final step: "Naar stap 2 - Werk".
Shared so every wizard's `primaryLabel` reads the same way. */
export const naarStapLabel = (stepNumber: number, stepLabel: string) =>
$localize`:@@wizard.naarStap:Naar stap ${stepNumber}:nummer: - ${stepLabel}:label:`;
/** A flat validation error pointing at a field: `id` matches the field's anchor. */
export interface WizardError {
readonly id: string;
readonly message: string;
}
export type WizardStatus = 'editing' | 'submitting' | 'submitted' | 'failed';
/**
* Template: the canonical shell every wizard renders into, so they cannot drift.
* It owns the consistent outline — CIBG stappenindicator (title merged in) + error
* summary + the horizontal
}
@case ('submitting') {
{{ submittingLabel() }}
}
@case ('submitted') {
}
@case ('failed') {
{{ errorMessage() }}
}
}
`,
})
export class WizardShellComponent {
steps = input.required();
current = input.required();
stepTitle = input.required();
/** Overall process name, shown above the step title (e.g. "Herregistratie aanvragen"). */
processName = input('');
status = input.required();
primaryLabel = input.required();
canGoBack = input(false);
errors = input([]);
errorMessage = input('');
submittingLabel = input($localize`:@@wizard.submitting:Aanvraag wordt verwerkt…`);
primary = output();
back = output();
cancel = output();
retry = output();
/** A visited step number was clicked in the stepper — back-navigation only. */
goToStep = output();
/** Error-summary link: focus the field instead of letting the browser navigate.
A fragment href resolves against , 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 stepper = viewChild(StepperComponent);
private errorSummary = viewChild>('errorSummary');
constructor() {
// A11y: move focus to the step title when the step changes (skip first run
// so we don't grab focus on initial load). Tracks current(), which is value-
// stable across keystrokes, so typing never steals focus.
let firstStep = true;
effect(() => {
this.current();
if (firstStep) {
firstStep = false;
return;
}
untracked(() => queueMicrotask(() => this.stepper()?.focusTitle()));
});
// 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;
hadErrors = has;
return;
}
if (has && !hadErrors)
untracked(() => queueMicrotask(() => this.errorSummary()?.nativeElement.focus()));
hadErrors = has;
});
}
}