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';
/** 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 — stepper + focusable step heading + error
* summary + the
}
@case ('submitting') {
{{ submittingLabel() }}
}
@case ('submitted') {
}
@case ('failed') {
{{ errorMessage() }}
}
}
`,
})
export class WizardShellComponent {
steps = input.required();
current = input.required();
stepTitle = input.required();
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();
/** 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 stepHeading = viewChild>('stepHeading');
private errorSummary = viewChild>('errorSummary');
constructor() {
// A11y: move focus to the step heading 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.stepHeading()?.nativeElement.focus()));
});
// 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;
});
}
}