Files
atomic-design-poc/src/app/shared/ui/text-input/text-input.component.ts
T
ehoandClaude Sonnet 5 f3de30b72c feat(fp): WP-16 — component a11y: description wiring + alert role
Wires text-input's aria-describedby to the form-field description div
(the BSN hint was rendered but never announced), pins desc-before-error
ordering, and switches alert to role=alert for errors vs role=status
for info/ok/warning. Composition contract enforced by story play tests
(form-field+text-input, alert per variant) run in the WP-01 CI gate.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 08:15:24 +02:00

77 lines
2.0 KiB
TypeScript

import { Component, booleanAttribute, forwardRef, input } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
/** Atom: text input. Utrecht textbox wired up as a form control (ngModel/reactive). */
@Component({
selector: 'app-text-input',
styles: [
`
:host {
display: block;
}
input {
inline-size: 100%;
box-sizing: border-box;
}
`,
],
template: `
<input
class="form-control"
[class.is-invalid]="invalid()"
[type]="type()"
[id]="inputId()"
[attr.aria-invalid]="invalid() ? 'true' : null"
[attr.aria-describedby]="describedBy()"
[placeholder]="placeholder()"
[disabled]="disabled"
[value]="value"
(input)="onInput($event)"
(blur)="onTouched()"
/>
`,
providers: [
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => TextInputComponent), multi: true },
],
})
export class TextInputComponent implements ControlValueAccessor {
type = input<'text' | 'password' | 'email'>('text');
placeholder = input('');
invalid = input(false);
inputId = input<string>();
/** Set when the paired form-field renders a `-desc` hint, so it gets announced. */
hasDescription = input(false, { transform: booleanAttribute });
value = '';
disabled = false;
onChange: (v: string) => void = () => {};
onTouched: () => void = () => {};
describedBy(): string | null {
const id = this.inputId();
if (!id) return null;
const ids = [
...(this.hasDescription() ? [`${id}-desc`] : []),
...(this.invalid() ? [`${id}-error`] : []),
];
return ids.length ? ids.join(' ') : null;
}
onInput(e: Event) {
this.value = (e.target as HTMLInputElement).value;
this.onChange(this.value);
}
writeValue(v: string) {
this.value = v ?? '';
}
registerOnChange(fn: (v: string) => void) {
this.onChange = fn;
}
registerOnTouched(fn: () => void) {
this.onTouched = fn;
}
setDisabledState(d: boolean) {
this.disabled = d;
}
}