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>
This commit is contained in:
@@ -55,7 +55,7 @@ for its existing violations, so every WP ends green.
|
|||||||
| [WP-13](WP-13-cibg-gap-register.md) | CIBG-gap register + hygiene + MDX | 2 · CIBG | done |
|
| [WP-13](WP-13-cibg-gap-register.md) | CIBG-gap register + hygiene + MDX | 2 · CIBG | done |
|
||||||
| [WP-14](WP-14-storybook-taxonomy.md) | Storybook taxonomy reorg + Layers MDX | 3 · Storybook | done |
|
| [WP-14](WP-14-storybook-taxonomy.md) | Storybook taxonomy reorg + Layers MDX | 3 · Storybook | done |
|
||||||
| [WP-15](WP-15-missing-stories.md) | Missing stories: shell + brief components | 3 · Storybook | done |
|
| [WP-15](WP-15-missing-stories.md) | Missing stories: shell + brief components | 3 · Storybook | done |
|
||||||
| [WP-16](WP-16-component-a11y.md) | Component a11y: description wiring + alert role | 4 · a11y | todo |
|
| [WP-16](WP-16-component-a11y.md) | Component a11y: description wiring + alert role | 4 · a11y | done |
|
||||||
| [WP-17](WP-17-app-a11y.md) | App-level a11y: route focus, template lint, WCAG checklist | 4 · a11y | todo |
|
| [WP-17](WP-17-app-a11y.md) | App-level a11y: route focus, template lint, WCAG checklist | 4 · a11y | todo |
|
||||||
| [WP-18](WP-18-abac-capability-spine.md) | ABAC capability spine (Principal + capabilities, phase P1) | 5 · productie-volwassenheid | done |
|
| [WP-18](WP-18-abac-capability-spine.md) | ABAC capability spine (Principal + capabilities, phase P1) | 5 · productie-volwassenheid | done |
|
||||||
| [WP-19](WP-19-e2e-smoke.md) | Playwright e2e smoke | 5 · productie-volwassenheid | todo |
|
| [WP-19](WP-19-e2e-smoke.md) | Playwright e2e smoke | 5 · productie-volwassenheid | todo |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# WP-16 — Component a11y: description wiring + alert role
|
# WP-16 — Component a11y: description wiring + alert role
|
||||||
|
|
||||||
Status: todo
|
Status: done (pending commit)
|
||||||
Phase: 4 — a11y
|
Phase: 4 — a11y
|
||||||
|
|
||||||
## Why
|
## Why
|
||||||
@@ -57,12 +57,25 @@ Audit findings axe can't (fully) catch:
|
|||||||
|
|
||||||
## Acceptance criteria
|
## Acceptance criteria
|
||||||
|
|
||||||
- [ ] Description text is programmatically associated in the canonical composition;
|
- [x] Description text is programmatically associated in the canonical composition;
|
||||||
login-form BSN hint announced.
|
login-form BSN hint announced.
|
||||||
- [ ] `-error` id appended exactly when invalid; order stable.
|
- [x] `-error` id appended exactly when invalid; order stable.
|
||||||
- [ ] Error alerts are `role="alert"`; play tests assert both behaviors and run in the
|
- [x] Error alerts are `role="alert"`; play tests assert both behaviors and run in the
|
||||||
CI gate.
|
CI gate.
|
||||||
|
|
||||||
|
## Deviation from the original plan
|
||||||
|
|
||||||
|
`radio-group`/`checkbox` were left unchanged — grepping every call site found zero
|
||||||
|
consumers pairing either with a `form-field` `description` (only the login-form BSN
|
||||||
|
field, which uses `text-input`). The WP's own Files note ("describedby joins; only
|
||||||
|
where the component takes a hint") already carved out this exact case — adding
|
||||||
|
`hasDescription` to atoms with no live description consumer would be unused surface,
|
||||||
|
not a fix. `radio-group` already had correct `-error`-only wiring; untouched.
|
||||||
|
`describedBy()` was added directly on `text-input` rather than factored into a shared
|
||||||
|
`shared/kernel` helper — one consumer, ~5 lines, not worth the indirection yet.
|
||||||
|
Manual screen-reader spot check (optional per the WP) skipped; the play test is the
|
||||||
|
enforced check going forward.
|
||||||
|
|
||||||
## Verification
|
## Verification
|
||||||
|
|
||||||
GREEN + `npm run test-storybook:ci` (includes the new play tests).
|
GREEN + `npm run test-storybook:ci` (includes the new play tests).
|
||||||
|
|||||||
@@ -11905,7 +11905,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "AlertComponent",
|
"name": "AlertComponent",
|
||||||
"id": "component-AlertComponent-859bbe82cccea40581a39ebff20da27d3087f9cca4ec7a25457e0f7a69f1478d7af94539fbcf20efcc96afff2a0f3252d0c88c566f00cb4eea117d1e0c0769e8",
|
"id": "component-AlertComponent-e38aceca94288853333b2fd4ad37535f923420f61c9ba25f100a4035a558c0ada40d88b7e7ddd136f6d6099bb7bc9695de3c513720c0b343b5792a4543de19f5",
|
||||||
"file": "src/app/shared/ui/alert/alert.component.ts",
|
"file": "src/app/shared/ui/alert/alert.component.ts",
|
||||||
"encapsulation": [],
|
"encapsulation": [],
|
||||||
"entryComponents": [],
|
"entryComponents": [],
|
||||||
@@ -11917,7 +11917,7 @@
|
|||||||
"styles": [
|
"styles": [
|
||||||
"\n .feedback > div {\n flex: 1 1 auto;\n min-width: 0;\n }\n "
|
"\n .feedback > div {\n flex: 1 1 auto;\n min-width: 0;\n }\n "
|
||||||
],
|
],
|
||||||
"template": "<div\n class=\"feedback\"\n [class.feedback-info]=\"type() === 'info'\"\n [class.feedback-success]=\"type() === 'ok'\"\n [class.feedback-warning]=\"type() === 'warning'\"\n [class.feedback-error]=\"type() === 'error'\"\n role=\"status\"\n aria-atomic=\"true\"\n>\n <span class=\"icon\"\n ><span class=\"visually-hidden\">{{ iconLabels[type()] }}</span></span\n >\n <div><ng-content /></div>\n</div>\n",
|
"template": "<div\n class=\"feedback\"\n [class.feedback-info]=\"type() === 'info'\"\n [class.feedback-success]=\"type() === 'ok'\"\n [class.feedback-warning]=\"type() === 'warning'\"\n [class.feedback-error]=\"type() === 'error'\"\n [attr.role]=\"type() === 'error' ? 'alert' : 'status'\"\n aria-atomic=\"true\"\n>\n <span class=\"icon\"\n ><span class=\"visually-hidden\">{{ iconLabels[type()] }}</span></span\n >\n <div><ng-content /></div>\n</div>\n",
|
||||||
"templateUrl": [],
|
"templateUrl": [],
|
||||||
"viewProviders": [],
|
"viewProviders": [],
|
||||||
"hostDirectives": [],
|
"hostDirectives": [],
|
||||||
@@ -11931,7 +11931,7 @@
|
|||||||
"indexKey": "",
|
"indexKey": "",
|
||||||
"optional": false,
|
"optional": false,
|
||||||
"description": "",
|
"description": "",
|
||||||
"line": 45,
|
"line": 48,
|
||||||
"required": false
|
"required": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -11946,7 +11946,7 @@
|
|||||||
"indexKey": "",
|
"indexKey": "",
|
||||||
"optional": false,
|
"optional": false,
|
||||||
"description": "",
|
"description": "",
|
||||||
"line": 46,
|
"line": 49,
|
||||||
"modifierKind": [
|
"modifierKind": [
|
||||||
124,
|
124,
|
||||||
148
|
148
|
||||||
@@ -11960,10 +11960,10 @@
|
|||||||
"hostListeners": [],
|
"hostListeners": [],
|
||||||
"standalone": false,
|
"standalone": false,
|
||||||
"imports": [],
|
"imports": [],
|
||||||
"description": "<p>Atom: alert/message banner — the CIBG Huisstijl "melding"\n(designsystem.cibg.nl/componenten/meldingen). Thin wrapper over the vendored\n<code>.feedback feedback-*</code> classes: the design system owns surface + icon; we add\nonly the icon's a11y label and a content wrapper (<code>.feedback</code> is a flex row).</p>\n",
|
"description": "<p>Atom: alert/message banner — the CIBG Huisstijl "melding"\n(designsystem.cibg.nl/componenten/meldingen). Thin wrapper over the vendored\n<code>.feedback feedback-*</code> classes: the design system owns surface + icon; we add\nonly the icon's a11y label and a content wrapper (<code>.feedback</code> is a flex row).\nErrors are <code>role="alert"</code> (assertive — interrupts) since they need immediate\nattention; other variants stay <code>role="status"</code> (polite) so success/info banners\ndon't interrupt what the user is doing.</p>\n",
|
||||||
"rawdescription": "\nAtom: alert/message banner — the CIBG Huisstijl \"melding\"\n(designsystem.cibg.nl/componenten/meldingen). Thin wrapper over the vendored\n`.feedback feedback-*` classes: the design system owns surface + icon; we add\nonly the icon's a11y label and a content wrapper (`.feedback` is a flex row).",
|
"rawdescription": "\nAtom: alert/message banner — the CIBG Huisstijl \"melding\"\n(designsystem.cibg.nl/componenten/meldingen). Thin wrapper over the vendored\n`.feedback feedback-*` classes: the design system owns surface + icon; we add\nonly the icon's a11y label and a content wrapper (`.feedback` is a flex row).\nErrors are `role=\"alert\"` (assertive — interrupts) since they need immediate\nattention; other variants stay `role=\"status\"` (polite) so success/info banners\ndon't interrupt what the user is doing.",
|
||||||
"type": "component",
|
"type": "component",
|
||||||
"sourceCode": "import { Component, input } from '@angular/core';\n\ntype AlertType = 'info' | 'ok' | 'warning' | 'error';\n\n// visually-hidden alternative for the status icon (CIBG a11y requirement).\nconst ICON_LABELS: Record<AlertType, string> = {\n info: $localize`:@@alert.icon.info:Informatie`,\n ok: $localize`:@@alert.icon.ok:Gelukt`,\n warning: $localize`:@@alert.icon.warning:Waarschuwing`,\n error: $localize`:@@alert.icon.error:Foutmelding`,\n};\n\n/** Atom: alert/message banner — the CIBG Huisstijl \"melding\"\n (designsystem.cibg.nl/componenten/meldingen). Thin wrapper over the vendored\n `.feedback feedback-*` classes: the design system owns surface + icon; we add\n only the icon's a11y label and a content wrapper (`.feedback` is a flex row). */\n@Component({\n selector: 'app-alert',\n styles: [\n `\n .feedback > div {\n flex: 1 1 auto;\n min-width: 0;\n }\n `,\n ],\n template: `\n <div\n class=\"feedback\"\n [class.feedback-info]=\"type() === 'info'\"\n [class.feedback-success]=\"type() === 'ok'\"\n [class.feedback-warning]=\"type() === 'warning'\"\n [class.feedback-error]=\"type() === 'error'\"\n role=\"status\"\n aria-atomic=\"true\"\n >\n <span class=\"icon\"\n ><span class=\"visually-hidden\">{{ iconLabels[type()] }}</span></span\n >\n <div><ng-content /></div>\n </div>\n `,\n})\nexport class AlertComponent {\n type = input<AlertType>('info');\n protected readonly iconLabels = ICON_LABELS;\n}\n",
|
"sourceCode": "import { Component, input } from '@angular/core';\n\ntype AlertType = 'info' | 'ok' | 'warning' | 'error';\n\n// visually-hidden alternative for the status icon (CIBG a11y requirement).\nconst ICON_LABELS: Record<AlertType, string> = {\n info: $localize`:@@alert.icon.info:Informatie`,\n ok: $localize`:@@alert.icon.ok:Gelukt`,\n warning: $localize`:@@alert.icon.warning:Waarschuwing`,\n error: $localize`:@@alert.icon.error:Foutmelding`,\n};\n\n/** Atom: alert/message banner — the CIBG Huisstijl \"melding\"\n (designsystem.cibg.nl/componenten/meldingen). Thin wrapper over the vendored\n `.feedback feedback-*` classes: the design system owns surface + icon; we add\n only the icon's a11y label and a content wrapper (`.feedback` is a flex row).\n Errors are `role=\"alert\"` (assertive — interrupts) since they need immediate\n attention; other variants stay `role=\"status\"` (polite) so success/info banners\n don't interrupt what the user is doing. */\n@Component({\n selector: 'app-alert',\n styles: [\n `\n .feedback > div {\n flex: 1 1 auto;\n min-width: 0;\n }\n `,\n ],\n template: `\n <div\n class=\"feedback\"\n [class.feedback-info]=\"type() === 'info'\"\n [class.feedback-success]=\"type() === 'ok'\"\n [class.feedback-warning]=\"type() === 'warning'\"\n [class.feedback-error]=\"type() === 'error'\"\n [attr.role]=\"type() === 'error' ? 'alert' : 'status'\"\n aria-atomic=\"true\"\n >\n <span class=\"icon\"\n ><span class=\"visually-hidden\">{{ iconLabels[type()] }}</span></span\n >\n <div><ng-content /></div>\n </div>\n `,\n})\nexport class AlertComponent {\n type = input<AlertType>('info');\n protected readonly iconLabels = ICON_LABELS;\n}\n",
|
||||||
"assetsDirs": [],
|
"assetsDirs": [],
|
||||||
"styleUrlsData": "",
|
"styleUrlsData": "",
|
||||||
"stylesData": "\n .feedback > div {\n flex: 1 1 auto;\n min-width: 0;\n }\n \n",
|
"stylesData": "\n .feedback > div {\n flex: 1 1 auto;\n min-width: 0;\n }\n \n",
|
||||||
@@ -18067,7 +18067,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "LoginFormComponent",
|
"name": "LoginFormComponent",
|
||||||
"id": "component-LoginFormComponent-b3f04d6cf60e4894fa1e6ecad944a0e206bb9db082d3ea89289e34147a4e3cbcb902189c8870b64400784969bc6c43c4fc64248ec4e46cdb553ec228204518ca",
|
"id": "component-LoginFormComponent-b6c5fb92e9b69f8a6f42ec2fc097f8501c87d05930404dcac64a29d3b99a12d69ea004881c9163fae2a94ee69aee48807ae0db75ec0336169eb1ad923e54237a",
|
||||||
"file": "src/app/auth/ui/login-form/login-form.component.ts",
|
"file": "src/app/auth/ui/login-form/login-form.component.ts",
|
||||||
"encapsulation": [],
|
"encapsulation": [],
|
||||||
"entryComponents": [],
|
"entryComponents": [],
|
||||||
@@ -18077,7 +18077,7 @@
|
|||||||
"selector": "app-login-form",
|
"selector": "app-login-form",
|
||||||
"styleUrls": [],
|
"styleUrls": [],
|
||||||
"styles": [],
|
"styles": [],
|
||||||
"template": "<form (ngSubmit)=\"submitted.emit(bsn)\" class=\"form-horizontal\">\n <div class=\"form-header\">\n <div class=\"form-action\">\n <span class=\"meta\" i18n=\"@@form.verplichteVelden\">* verplichte velden</span>\n </div>\n </div>\n\n <app-form-field\n i18n-label=\"@@login.bsnLabel\"\n label=\"BSN\"\n fieldId=\"bsn\"\n required\n i18n-description=\"@@login.bsnDescription\"\n description=\"9 cijfers (demo: vul iets in)\"\n >\n <app-text-input inputId=\"bsn\" [(ngModel)]=\"bsn\" name=\"bsn\" placeholder=\"123456789\" />\n </app-form-field>\n\n <app-form-field i18n-label=\"@@login.wachtwoordLabel\" label=\"Wachtwoord\" fieldId=\"pw\" required>\n <app-text-input inputId=\"pw\" type=\"password\" [(ngModel)]=\"password\" name=\"pw\" />\n </app-form-field>\n\n <app-button type=\"submit\" variant=\"primary\" i18n=\"@@login.submit\"\n >Inloggen met DigiD</app-button\n >\n</form>\n",
|
"template": "<form (ngSubmit)=\"submitted.emit(bsn)\" class=\"form-horizontal\">\n <div class=\"form-header\">\n <div class=\"form-action\">\n <span class=\"meta\" i18n=\"@@form.verplichteVelden\">* verplichte velden</span>\n </div>\n </div>\n\n <app-form-field\n i18n-label=\"@@login.bsnLabel\"\n label=\"BSN\"\n fieldId=\"bsn\"\n required\n i18n-description=\"@@login.bsnDescription\"\n description=\"9 cijfers (demo: vul iets in)\"\n >\n <app-text-input\n inputId=\"bsn\"\n hasDescription\n [(ngModel)]=\"bsn\"\n name=\"bsn\"\n placeholder=\"123456789\"\n />\n </app-form-field>\n\n <app-form-field i18n-label=\"@@login.wachtwoordLabel\" label=\"Wachtwoord\" fieldId=\"pw\" required>\n <app-text-input inputId=\"pw\" type=\"password\" [(ngModel)]=\"password\" name=\"pw\" />\n </app-form-field>\n\n <app-button type=\"submit\" variant=\"primary\" i18n=\"@@login.submit\"\n >Inloggen met DigiD</app-button\n >\n</form>\n",
|
||||||
"templateUrl": [],
|
"templateUrl": [],
|
||||||
"viewProviders": [],
|
"viewProviders": [],
|
||||||
"hostDirectives": [],
|
"hostDirectives": [],
|
||||||
@@ -18091,7 +18091,7 @@
|
|||||||
"indexKey": "",
|
"indexKey": "",
|
||||||
"optional": false,
|
"optional": false,
|
||||||
"description": "",
|
"description": "",
|
||||||
"line": 43,
|
"line": 49,
|
||||||
"required": false
|
"required": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -18105,7 +18105,7 @@
|
|||||||
"indexKey": "",
|
"indexKey": "",
|
||||||
"optional": false,
|
"optional": false,
|
||||||
"description": "",
|
"description": "",
|
||||||
"line": 41
|
"line": 47
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "password",
|
"name": "password",
|
||||||
@@ -18116,7 +18116,7 @@
|
|||||||
"indexKey": "",
|
"indexKey": "",
|
||||||
"optional": false,
|
"optional": false,
|
||||||
"description": "",
|
"description": "",
|
||||||
"line": 42
|
"line": 48
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"methodsClass": [],
|
"methodsClass": [],
|
||||||
@@ -18146,7 +18146,7 @@
|
|||||||
"description": "<p>Organism: DigiD-style mock login. No real auth — just composes atoms/molecules.</p>\n",
|
"description": "<p>Organism: DigiD-style mock login. No real auth — just composes atoms/molecules.</p>\n",
|
||||||
"rawdescription": "\nOrganism: DigiD-style mock login. No real auth — just composes atoms/molecules.",
|
"rawdescription": "\nOrganism: DigiD-style mock login. No real auth — just composes atoms/molecules.",
|
||||||
"type": "component",
|
"type": "component",
|
||||||
"sourceCode": "import { Component, output } from '@angular/core';\nimport { FormsModule } from '@angular/forms';\nimport { FormFieldComponent } from '@shared/ui/form-field/form-field.component';\nimport { TextInputComponent } from '@shared/ui/text-input/text-input.component';\nimport { ButtonComponent } from '@shared/ui/button/button.component';\n\n/** Organism: DigiD-style mock login. No real auth — just composes atoms/molecules. */\n@Component({\n selector: 'app-login-form',\n imports: [FormsModule, FormFieldComponent, TextInputComponent, ButtonComponent],\n template: `\n <form (ngSubmit)=\"submitted.emit(bsn)\" class=\"form-horizontal\">\n <div class=\"form-header\">\n <div class=\"form-action\">\n <span class=\"meta\" i18n=\"@@form.verplichteVelden\">* verplichte velden</span>\n </div>\n </div>\n\n <app-form-field\n i18n-label=\"@@login.bsnLabel\"\n label=\"BSN\"\n fieldId=\"bsn\"\n required\n i18n-description=\"@@login.bsnDescription\"\n description=\"9 cijfers (demo: vul iets in)\"\n >\n <app-text-input inputId=\"bsn\" [(ngModel)]=\"bsn\" name=\"bsn\" placeholder=\"123456789\" />\n </app-form-field>\n\n <app-form-field i18n-label=\"@@login.wachtwoordLabel\" label=\"Wachtwoord\" fieldId=\"pw\" required>\n <app-text-input inputId=\"pw\" type=\"password\" [(ngModel)]=\"password\" name=\"pw\" />\n </app-form-field>\n\n <app-button type=\"submit\" variant=\"primary\" i18n=\"@@login.submit\"\n >Inloggen met DigiD</app-button\n >\n </form>\n `,\n})\nexport class LoginFormComponent {\n bsn = '';\n password = '';\n submitted = output<string>();\n}\n",
|
"sourceCode": "import { Component, output } from '@angular/core';\nimport { FormsModule } from '@angular/forms';\nimport { FormFieldComponent } from '@shared/ui/form-field/form-field.component';\nimport { TextInputComponent } from '@shared/ui/text-input/text-input.component';\nimport { ButtonComponent } from '@shared/ui/button/button.component';\n\n/** Organism: DigiD-style mock login. No real auth — just composes atoms/molecules. */\n@Component({\n selector: 'app-login-form',\n imports: [FormsModule, FormFieldComponent, TextInputComponent, ButtonComponent],\n template: `\n <form (ngSubmit)=\"submitted.emit(bsn)\" class=\"form-horizontal\">\n <div class=\"form-header\">\n <div class=\"form-action\">\n <span class=\"meta\" i18n=\"@@form.verplichteVelden\">* verplichte velden</span>\n </div>\n </div>\n\n <app-form-field\n i18n-label=\"@@login.bsnLabel\"\n label=\"BSN\"\n fieldId=\"bsn\"\n required\n i18n-description=\"@@login.bsnDescription\"\n description=\"9 cijfers (demo: vul iets in)\"\n >\n <app-text-input\n inputId=\"bsn\"\n hasDescription\n [(ngModel)]=\"bsn\"\n name=\"bsn\"\n placeholder=\"123456789\"\n />\n </app-form-field>\n\n <app-form-field i18n-label=\"@@login.wachtwoordLabel\" label=\"Wachtwoord\" fieldId=\"pw\" required>\n <app-text-input inputId=\"pw\" type=\"password\" [(ngModel)]=\"password\" name=\"pw\" />\n </app-form-field>\n\n <app-button type=\"submit\" variant=\"primary\" i18n=\"@@login.submit\"\n >Inloggen met DigiD</app-button\n >\n </form>\n `,\n})\nexport class LoginFormComponent {\n bsn = '';\n password = '';\n submitted = output<string>();\n}\n",
|
||||||
"assetsDirs": [],
|
"assetsDirs": [],
|
||||||
"styleUrlsData": "",
|
"styleUrlsData": "",
|
||||||
"stylesData": "",
|
"stylesData": "",
|
||||||
@@ -21976,7 +21976,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "TextInputComponent",
|
"name": "TextInputComponent",
|
||||||
"id": "component-TextInputComponent-6bb65b77462f371a5f1eef4b78584a11380f10f1ffd7082d2844d9a50d2d6ace877d6e408ef279f4c6057daf08c97ffc6c2fa7c29b6de95c687c34adc9730c94",
|
"id": "component-TextInputComponent-7771b0ade08f68e587e3e3ebd8348dc85ed70f270c17ed6c8ceed88499dc366f926989029f6216b696a7b800e4dd42a55ff9511a7e99ee941fa0ee2c91513ce0",
|
||||||
"file": "src/app/shared/ui/text-input/text-input.component.ts",
|
"file": "src/app/shared/ui/text-input/text-input.component.ts",
|
||||||
"encapsulation": [],
|
"encapsulation": [],
|
||||||
"entryComponents": [],
|
"entryComponents": [],
|
||||||
@@ -21992,11 +21992,23 @@
|
|||||||
"styles": [
|
"styles": [
|
||||||
"\n :host {\n display: block;\n }\n input {\n inline-size: 100%;\n box-sizing: border-box;\n }\n "
|
"\n :host {\n display: block;\n }\n input {\n inline-size: 100%;\n box-sizing: border-box;\n }\n "
|
||||||
],
|
],
|
||||||
"template": "<input\n class=\"form-control\"\n [class.is-invalid]=\"invalid()\"\n [type]=\"type()\"\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"invalid() ? 'true' : null\"\n [attr.aria-describedby]=\"invalid() && inputId() ? inputId() + '-error' : null\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"disabled\"\n [value]=\"value\"\n (input)=\"onInput($event)\"\n (blur)=\"onTouched()\"\n/>\n",
|
"template": "<input\n class=\"form-control\"\n [class.is-invalid]=\"invalid()\"\n [type]=\"type()\"\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"invalid() ? 'true' : null\"\n [attr.aria-describedby]=\"describedBy()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"disabled\"\n [value]=\"value\"\n (input)=\"onInput($event)\"\n (blur)=\"onTouched()\"\n/>\n",
|
||||||
"templateUrl": [],
|
"templateUrl": [],
|
||||||
"viewProviders": [],
|
"viewProviders": [],
|
||||||
"hostDirectives": [],
|
"hostDirectives": [],
|
||||||
"inputsClass": [
|
"inputsClass": [
|
||||||
|
{
|
||||||
|
"name": "hasDescription",
|
||||||
|
"defaultValue": "false, { transform: booleanAttribute }",
|
||||||
|
"deprecated": false,
|
||||||
|
"deprecationMessage": "",
|
||||||
|
"indexKey": "",
|
||||||
|
"optional": false,
|
||||||
|
"description": "<p>Set when the paired form-field renders a <code>-desc</code> hint, so it gets announced.</p>\n",
|
||||||
|
"line": 43,
|
||||||
|
"rawdescription": "\nSet when the paired form-field renders a `-desc` hint, so it gets announced.",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "inputId",
|
"name": "inputId",
|
||||||
"deprecated": false,
|
"deprecated": false,
|
||||||
@@ -22054,7 +22066,7 @@
|
|||||||
"indexKey": "",
|
"indexKey": "",
|
||||||
"optional": false,
|
"optional": false,
|
||||||
"description": "",
|
"description": "",
|
||||||
"line": 44
|
"line": 46
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "onChange",
|
"name": "onChange",
|
||||||
@@ -22065,7 +22077,7 @@
|
|||||||
"indexKey": "",
|
"indexKey": "",
|
||||||
"optional": false,
|
"optional": false,
|
||||||
"description": "",
|
"description": "",
|
||||||
"line": 45
|
"line": 47
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "onTouched",
|
"name": "onTouched",
|
||||||
@@ -22076,7 +22088,7 @@
|
|||||||
"indexKey": "",
|
"indexKey": "",
|
||||||
"optional": false,
|
"optional": false,
|
||||||
"description": "",
|
"description": "",
|
||||||
"line": 46
|
"line": 48
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "value",
|
"name": "value",
|
||||||
@@ -22087,10 +22099,20 @@
|
|||||||
"indexKey": "",
|
"indexKey": "",
|
||||||
"optional": false,
|
"optional": false,
|
||||||
"description": "",
|
"description": "",
|
||||||
"line": 43
|
"line": 45
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"methodsClass": [
|
"methodsClass": [
|
||||||
|
{
|
||||||
|
"name": "describedBy",
|
||||||
|
"args": [],
|
||||||
|
"optional": false,
|
||||||
|
"returnType": "string | null",
|
||||||
|
"typeParameters": [],
|
||||||
|
"line": 50,
|
||||||
|
"deprecated": false,
|
||||||
|
"deprecationMessage": ""
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "onInput",
|
"name": "onInput",
|
||||||
"args": [
|
"args": [
|
||||||
@@ -22106,7 +22128,7 @@
|
|||||||
"optional": false,
|
"optional": false,
|
||||||
"returnType": "void",
|
"returnType": "void",
|
||||||
"typeParameters": [],
|
"typeParameters": [],
|
||||||
"line": 48,
|
"line": 60,
|
||||||
"deprecated": false,
|
"deprecated": false,
|
||||||
"deprecationMessage": "",
|
"deprecationMessage": "",
|
||||||
"jsdoctags": [
|
"jsdoctags": [
|
||||||
@@ -22148,7 +22170,7 @@
|
|||||||
"optional": false,
|
"optional": false,
|
||||||
"returnType": "void",
|
"returnType": "void",
|
||||||
"typeParameters": [],
|
"typeParameters": [],
|
||||||
"line": 55,
|
"line": 67,
|
||||||
"deprecated": false,
|
"deprecated": false,
|
||||||
"deprecationMessage": "",
|
"deprecationMessage": "",
|
||||||
"jsdoctags": [
|
"jsdoctags": [
|
||||||
@@ -22191,7 +22213,7 @@
|
|||||||
"optional": false,
|
"optional": false,
|
||||||
"returnType": "void",
|
"returnType": "void",
|
||||||
"typeParameters": [],
|
"typeParameters": [],
|
||||||
"line": 58,
|
"line": 70,
|
||||||
"deprecated": false,
|
"deprecated": false,
|
||||||
"deprecationMessage": "",
|
"deprecationMessage": "",
|
||||||
"jsdoctags": [
|
"jsdoctags": [
|
||||||
@@ -22224,7 +22246,7 @@
|
|||||||
"optional": false,
|
"optional": false,
|
||||||
"returnType": "void",
|
"returnType": "void",
|
||||||
"typeParameters": [],
|
"typeParameters": [],
|
||||||
"line": 61,
|
"line": 73,
|
||||||
"deprecated": false,
|
"deprecated": false,
|
||||||
"deprecationMessage": "",
|
"deprecationMessage": "",
|
||||||
"jsdoctags": [
|
"jsdoctags": [
|
||||||
@@ -22256,7 +22278,7 @@
|
|||||||
"optional": false,
|
"optional": false,
|
||||||
"returnType": "void",
|
"returnType": "void",
|
||||||
"typeParameters": [],
|
"typeParameters": [],
|
||||||
"line": 52,
|
"line": 64,
|
||||||
"deprecated": false,
|
"deprecated": false,
|
||||||
"deprecationMessage": "",
|
"deprecationMessage": "",
|
||||||
"jsdoctags": [
|
"jsdoctags": [
|
||||||
@@ -22283,7 +22305,7 @@
|
|||||||
"description": "<p>Atom: text input. Utrecht textbox wired up as a form control (ngModel/reactive).</p>\n",
|
"description": "<p>Atom: text input. Utrecht textbox wired up as a form control (ngModel/reactive).</p>\n",
|
||||||
"rawdescription": "\nAtom: text input. Utrecht textbox wired up as a form control (ngModel/reactive).",
|
"rawdescription": "\nAtom: text input. Utrecht textbox wired up as a form control (ngModel/reactive).",
|
||||||
"type": "component",
|
"type": "component",
|
||||||
"sourceCode": "import { Component, forwardRef, input } from '@angular/core';\nimport { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';\n\n/** Atom: text input. Utrecht textbox wired up as a form control (ngModel/reactive). */\n@Component({\n selector: 'app-text-input',\n styles: [\n `\n :host {\n display: block;\n }\n input {\n inline-size: 100%;\n box-sizing: border-box;\n }\n `,\n ],\n template: `\n <input\n class=\"form-control\"\n [class.is-invalid]=\"invalid()\"\n [type]=\"type()\"\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"invalid() ? 'true' : null\"\n [attr.aria-describedby]=\"invalid() && inputId() ? inputId() + '-error' : null\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"disabled\"\n [value]=\"value\"\n (input)=\"onInput($event)\"\n (blur)=\"onTouched()\"\n />\n `,\n providers: [\n { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => TextInputComponent), multi: true },\n ],\n})\nexport class TextInputComponent implements ControlValueAccessor {\n type = input<'text' | 'password' | 'email'>('text');\n placeholder = input('');\n invalid = input(false);\n inputId = input<string>();\n\n value = '';\n disabled = false;\n onChange: (v: string) => void = () => {};\n onTouched: () => void = () => {};\n\n onInput(e: Event) {\n this.value = (e.target as HTMLInputElement).value;\n this.onChange(this.value);\n }\n writeValue(v: string) {\n this.value = v ?? '';\n }\n registerOnChange(fn: (v: string) => void) {\n this.onChange = fn;\n }\n registerOnTouched(fn: () => void) {\n this.onTouched = fn;\n }\n setDisabledState(d: boolean) {\n this.disabled = d;\n }\n}\n",
|
"sourceCode": "import { Component, booleanAttribute, forwardRef, input } from '@angular/core';\nimport { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';\n\n/** Atom: text input. Utrecht textbox wired up as a form control (ngModel/reactive). */\n@Component({\n selector: 'app-text-input',\n styles: [\n `\n :host {\n display: block;\n }\n input {\n inline-size: 100%;\n box-sizing: border-box;\n }\n `,\n ],\n template: `\n <input\n class=\"form-control\"\n [class.is-invalid]=\"invalid()\"\n [type]=\"type()\"\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"invalid() ? 'true' : null\"\n [attr.aria-describedby]=\"describedBy()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"disabled\"\n [value]=\"value\"\n (input)=\"onInput($event)\"\n (blur)=\"onTouched()\"\n />\n `,\n providers: [\n { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => TextInputComponent), multi: true },\n ],\n})\nexport class TextInputComponent implements ControlValueAccessor {\n type = input<'text' | 'password' | 'email'>('text');\n placeholder = input('');\n invalid = input(false);\n inputId = input<string>();\n /** Set when the paired form-field renders a `-desc` hint, so it gets announced. */\n hasDescription = input(false, { transform: booleanAttribute });\n\n value = '';\n disabled = false;\n onChange: (v: string) => void = () => {};\n onTouched: () => void = () => {};\n\n describedBy(): string | null {\n const id = this.inputId();\n if (!id) return null;\n const ids = [\n ...(this.hasDescription() ? [`${id}-desc`] : []),\n ...(this.invalid() ? [`${id}-error`] : []),\n ];\n return ids.length ? ids.join(' ') : null;\n }\n\n onInput(e: Event) {\n this.value = (e.target as HTMLInputElement).value;\n this.onChange(this.value);\n }\n writeValue(v: string) {\n this.value = v ?? '';\n }\n registerOnChange(fn: (v: string) => void) {\n this.onChange = fn;\n }\n registerOnTouched(fn: () => void) {\n this.onTouched = fn;\n }\n setDisabledState(d: boolean) {\n this.disabled = d;\n }\n}\n",
|
||||||
"assetsDirs": [],
|
"assetsDirs": [],
|
||||||
"styleUrlsData": "",
|
"styleUrlsData": "",
|
||||||
"stylesData": "\n :host {\n display: block;\n }\n input {\n inline-size: 100%;\n box-sizing: border-box;\n }\n \n",
|
"stylesData": "\n :host {\n display: block;\n }\n input {\n inline-size: 100%;\n box-sizing: border-box;\n }\n \n",
|
||||||
@@ -22948,6 +22970,16 @@
|
|||||||
"rawdescription": "Used by the shell to find what to poll on return: still-in-flight uploads.",
|
"rawdescription": "Used by the shell to find what to poll on return: still-in-flight uploads.",
|
||||||
"description": "<p>Used by the shell to find what to poll on return: still-in-flight uploads.</p>\n"
|
"description": "<p>Used by the shell to find what to poll on return: still-in-flight uploads.</p>\n"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "initial",
|
||||||
|
"ctype": "miscellaneous",
|
||||||
|
"subtype": "variable",
|
||||||
|
"file": "src/app/brief/domain/brief.machine.ts",
|
||||||
|
"deprecated": false,
|
||||||
|
"deprecationMessage": "",
|
||||||
|
"type": "BriefState",
|
||||||
|
"defaultValue": "{ tag: 'loading' }"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "initial",
|
"name": "initial",
|
||||||
"ctype": "miscellaneous",
|
"ctype": "miscellaneous",
|
||||||
@@ -22988,16 +23020,6 @@
|
|||||||
"type": "RegistratieState",
|
"type": "RegistratieState",
|
||||||
"defaultValue": "{\n tag: 'Invullen',\n draft: emptyDraft,\n cursor: 0,\n errors: {},\n upload: initialUpload,\n}"
|
"defaultValue": "{\n tag: 'Invullen',\n draft: emptyDraft,\n cursor: 0,\n errors: {},\n upload: initialUpload,\n}"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "initial",
|
|
||||||
"ctype": "miscellaneous",
|
|
||||||
"subtype": "variable",
|
|
||||||
"file": "src/app/brief/domain/brief.machine.ts",
|
|
||||||
"deprecated": false,
|
|
||||||
"deprecationMessage": "",
|
|
||||||
"type": "BriefState",
|
|
||||||
"defaultValue": "{ tag: 'loading' }"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "initialUpload",
|
"name": "initialUpload",
|
||||||
"ctype": "miscellaneous",
|
"ctype": "miscellaneous",
|
||||||
@@ -27258,6 +27280,50 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "reduce",
|
||||||
|
"file": "src/app/brief/domain/brief.machine.ts",
|
||||||
|
"ctype": "miscellaneous",
|
||||||
|
"subtype": "function",
|
||||||
|
"deprecated": false,
|
||||||
|
"deprecationMessage": "",
|
||||||
|
"description": "",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "s",
|
||||||
|
"type": "BriefState",
|
||||||
|
"deprecated": false,
|
||||||
|
"deprecationMessage": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "m",
|
||||||
|
"type": "BriefMsg",
|
||||||
|
"deprecated": false,
|
||||||
|
"deprecationMessage": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"returnType": "BriefState",
|
||||||
|
"jsdoctags": [
|
||||||
|
{
|
||||||
|
"name": "s",
|
||||||
|
"type": "BriefState",
|
||||||
|
"deprecated": false,
|
||||||
|
"deprecationMessage": "",
|
||||||
|
"tagName": {
|
||||||
|
"text": "param"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "m",
|
||||||
|
"type": "BriefMsg",
|
||||||
|
"deprecated": false,
|
||||||
|
"deprecationMessage": "",
|
||||||
|
"tagName": {
|
||||||
|
"text": "param"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "reduce",
|
"name": "reduce",
|
||||||
"file": "src/app/herregistratie/domain/herregistratie.machine.ts",
|
"file": "src/app/herregistratie/domain/herregistratie.machine.ts",
|
||||||
@@ -27434,50 +27500,6 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "reduce",
|
|
||||||
"file": "src/app/brief/domain/brief.machine.ts",
|
|
||||||
"ctype": "miscellaneous",
|
|
||||||
"subtype": "function",
|
|
||||||
"deprecated": false,
|
|
||||||
"deprecationMessage": "",
|
|
||||||
"description": "",
|
|
||||||
"args": [
|
|
||||||
{
|
|
||||||
"name": "s",
|
|
||||||
"type": "BriefState",
|
|
||||||
"deprecated": false,
|
|
||||||
"deprecationMessage": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "m",
|
|
||||||
"type": "BriefMsg",
|
|
||||||
"deprecated": false,
|
|
||||||
"deprecationMessage": ""
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"returnType": "BriefState",
|
|
||||||
"jsdoctags": [
|
|
||||||
{
|
|
||||||
"name": "s",
|
|
||||||
"type": "BriefState",
|
|
||||||
"deprecated": false,
|
|
||||||
"deprecationMessage": "",
|
|
||||||
"tagName": {
|
|
||||||
"text": "param"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "m",
|
|
||||||
"type": "BriefMsg",
|
|
||||||
"deprecated": false,
|
|
||||||
"deprecationMessage": "",
|
|
||||||
"tagName": {
|
|
||||||
"text": "param"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "reduceUpload",
|
"name": "reduceUpload",
|
||||||
"file": "src/app/shared/upload/upload.machine.ts",
|
"file": "src/app/shared/upload/upload.machine.ts",
|
||||||
@@ -30414,6 +30436,18 @@
|
|||||||
"defaultValue": "{\n info: $localize`:@@alert.icon.info:Informatie`,\n ok: $localize`:@@alert.icon.ok:Gelukt`,\n warning: $localize`:@@alert.icon.warning:Waarschuwing`,\n error: $localize`:@@alert.icon.error:Foutmelding`,\n}"
|
"defaultValue": "{\n info: $localize`:@@alert.icon.info:Informatie`,\n ok: $localize`:@@alert.icon.ok:Gelukt`,\n warning: $localize`:@@alert.icon.warning:Waarschuwing`,\n error: $localize`:@@alert.icon.error:Foutmelding`,\n}"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"src/app/brief/domain/brief.machine.ts": [
|
||||||
|
{
|
||||||
|
"name": "initial",
|
||||||
|
"ctype": "miscellaneous",
|
||||||
|
"subtype": "variable",
|
||||||
|
"file": "src/app/brief/domain/brief.machine.ts",
|
||||||
|
"deprecated": false,
|
||||||
|
"deprecationMessage": "",
|
||||||
|
"type": "BriefState",
|
||||||
|
"defaultValue": "{ tag: 'loading' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
"src/app/herregistratie/domain/herregistratie.machine.ts": [
|
"src/app/herregistratie/domain/herregistratie.machine.ts": [
|
||||||
{
|
{
|
||||||
"name": "initial",
|
"name": "initial",
|
||||||
@@ -30474,18 +30508,6 @@
|
|||||||
"defaultValue": "{\n tag: 'Editing',\n draft: { straat: '', postcode: '', woonplaats: '' },\n errors: {},\n}"
|
"defaultValue": "{\n tag: 'Editing',\n draft: { straat: '', postcode: '', woonplaats: '' },\n errors: {},\n}"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"src/app/brief/domain/brief.machine.ts": [
|
|
||||||
{
|
|
||||||
"name": "initial",
|
|
||||||
"ctype": "miscellaneous",
|
|
||||||
"subtype": "variable",
|
|
||||||
"file": "src/app/brief/domain/brief.machine.ts",
|
|
||||||
"deprecated": false,
|
|
||||||
"deprecationMessage": "",
|
|
||||||
"type": "BriefState",
|
|
||||||
"defaultValue": "{ tag: 'loading' }"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"src/app/shared/ui/radio-group/radio-group.component.ts": [
|
"src/app/shared/ui/radio-group/radio-group.component.ts": [
|
||||||
{
|
{
|
||||||
"name": "JA_NEE",
|
"name": "JA_NEE",
|
||||||
@@ -41803,8 +41825,8 @@
|
|||||||
"type": "component",
|
"type": "component",
|
||||||
"linktype": "component",
|
"linktype": "component",
|
||||||
"name": "TextInputComponent",
|
"name": "TextInputComponent",
|
||||||
"coveragePercent": 7,
|
"coveragePercent": 12,
|
||||||
"coverageCount": "1/14",
|
"coverageCount": "2/16",
|
||||||
"status": "low"
|
"status": "low"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -24,7 +24,13 @@ import { ButtonComponent } from '@shared/ui/button/button.component';
|
|||||||
i18n-description="@@login.bsnDescription"
|
i18n-description="@@login.bsnDescription"
|
||||||
description="9 cijfers (demo: vul iets in)"
|
description="9 cijfers (demo: vul iets in)"
|
||||||
>
|
>
|
||||||
<app-text-input inputId="bsn" [(ngModel)]="bsn" name="bsn" placeholder="123456789" />
|
<app-text-input
|
||||||
|
inputId="bsn"
|
||||||
|
hasDescription
|
||||||
|
[(ngModel)]="bsn"
|
||||||
|
name="bsn"
|
||||||
|
placeholder="123456789"
|
||||||
|
/>
|
||||||
</app-form-field>
|
</app-form-field>
|
||||||
|
|
||||||
<app-form-field i18n-label="@@login.wachtwoordLabel" label="Wachtwoord" fieldId="pw" required>
|
<app-form-field i18n-label="@@login.wachtwoordLabel" label="Wachtwoord" fieldId="pw" required>
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ const ICON_LABELS: Record<AlertType, string> = {
|
|||||||
/** Atom: alert/message banner — the CIBG Huisstijl "melding"
|
/** Atom: alert/message banner — the CIBG Huisstijl "melding"
|
||||||
(designsystem.cibg.nl/componenten/meldingen). Thin wrapper over the vendored
|
(designsystem.cibg.nl/componenten/meldingen). Thin wrapper over the vendored
|
||||||
`.feedback feedback-*` classes: the design system owns surface + icon; we add
|
`.feedback feedback-*` classes: the design system owns surface + icon; we add
|
||||||
only the icon's a11y label and a content wrapper (`.feedback` is a flex row). */
|
only the icon's a11y label and a content wrapper (`.feedback` is a flex row).
|
||||||
|
Errors are `role="alert"` (assertive — interrupts) since they need immediate
|
||||||
|
attention; other variants stay `role="status"` (polite) so success/info banners
|
||||||
|
don't interrupt what the user is doing. */
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-alert',
|
selector: 'app-alert',
|
||||||
styles: [
|
styles: [
|
||||||
@@ -31,7 +34,7 @@ const ICON_LABELS: Record<AlertType, string> = {
|
|||||||
[class.feedback-success]="type() === 'ok'"
|
[class.feedback-success]="type() === 'ok'"
|
||||||
[class.feedback-warning]="type() === 'warning'"
|
[class.feedback-warning]="type() === 'warning'"
|
||||||
[class.feedback-error]="type() === 'error'"
|
[class.feedback-error]="type() === 'error'"
|
||||||
role="status"
|
[attr.role]="type() === 'error' ? 'alert' : 'status'"
|
||||||
aria-atomic="true"
|
aria-atomic="true"
|
||||||
>
|
>
|
||||||
<span class="icon"
|
<span class="icon"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { Meta, StoryObj } from '@storybook/angular';
|
import type { Meta, StoryObj } from '@storybook/angular';
|
||||||
|
import { expect, within } from 'storybook/test';
|
||||||
import { AlertComponent } from './alert.component';
|
import { AlertComponent } from './alert.component';
|
||||||
|
|
||||||
const meta: Meta<AlertComponent> = {
|
const meta: Meta<AlertComponent> = {
|
||||||
@@ -12,7 +13,28 @@ const meta: Meta<AlertComponent> = {
|
|||||||
export default meta;
|
export default meta;
|
||||||
type Story = StoryObj<AlertComponent>;
|
type Story = StoryObj<AlertComponent>;
|
||||||
|
|
||||||
export const Info: Story = { args: { type: 'info' } };
|
// role assertions guard the polite/assertive split (WP-16): errors interrupt, others don't.
|
||||||
export const Ok: Story = { args: { type: 'ok' } };
|
export const Info: Story = {
|
||||||
export const Warning: Story = { args: { type: 'warning' } };
|
args: { type: 'info' },
|
||||||
export const Error: Story = { args: { type: 'error' } };
|
play: async ({ canvasElement }) => {
|
||||||
|
await expect(within(canvasElement).getByRole('status')).toBeInTheDocument();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export const Ok: Story = {
|
||||||
|
args: { type: 'ok' },
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
await expect(within(canvasElement).getByRole('status')).toBeInTheDocument();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export const Warning: Story = {
|
||||||
|
args: { type: 'warning' },
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
await expect(within(canvasElement).getByRole('status')).toBeInTheDocument();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export const Error: Story = {
|
||||||
|
args: { type: 'error' },
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
await expect(within(canvasElement).getByRole('alert')).toBeInTheDocument();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Meta, StoryObj } from '@storybook/angular';
|
import type { Meta, StoryObj } from '@storybook/angular';
|
||||||
import { moduleMetadata } from '@storybook/angular';
|
import { moduleMetadata } from '@storybook/angular';
|
||||||
|
import { expect, within } from 'storybook/test';
|
||||||
import { FormFieldComponent } from './form-field.component';
|
import { FormFieldComponent } from './form-field.component';
|
||||||
import { TextInputComponent } from '@shared/ui/text-input/text-input.component';
|
import { TextInputComponent } from '@shared/ui/text-input/text-input.component';
|
||||||
|
|
||||||
@@ -13,7 +14,7 @@ const meta: Meta<FormFieldComponent> = {
|
|||||||
template: `
|
template: `
|
||||||
<form class="form-horizontal">
|
<form class="form-horizontal">
|
||||||
<app-form-field [label]="label" [fieldId]="fieldId" [description]="description" [error]="error" [required]="required">
|
<app-form-field [label]="label" [fieldId]="fieldId" [description]="description" [error]="error" [required]="required">
|
||||||
<app-text-input [inputId]="fieldId" [invalid]="!!error" placeholder="Vul in" />
|
<app-text-input [inputId]="fieldId" [hasDescription]="!!description" [invalid]="!!error" placeholder="Vul in" />
|
||||||
</app-form-field>
|
</app-form-field>
|
||||||
</form>`,
|
</form>`,
|
||||||
}),
|
}),
|
||||||
@@ -23,6 +24,13 @@ type Story = StoryObj<FormFieldComponent>;
|
|||||||
|
|
||||||
export const Default: Story = {
|
export const Default: Story = {
|
||||||
args: { label: 'BSN', fieldId: 'bsn', description: '9 cijfers', required: true },
|
args: { label: 'BSN', fieldId: 'bsn', description: '9 cijfers', required: true },
|
||||||
|
// Composition contract: fieldId must equal the input's id — enforced here, not by DI
|
||||||
|
// (see WP-16). Catches drift in the description→aria-describedby wiring.
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
const input = canvas.getByRole('textbox');
|
||||||
|
await expect(input).toHaveAttribute('aria-describedby', 'bsn-desc');
|
||||||
|
},
|
||||||
};
|
};
|
||||||
export const WithError: Story = {
|
export const WithError: Story = {
|
||||||
args: {
|
args: {
|
||||||
@@ -31,4 +39,23 @@ export const WithError: Story = {
|
|||||||
error: 'Dit veld is verplicht.',
|
error: 'Dit veld is verplicht.',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
const input = canvas.getByRole('textbox');
|
||||||
|
await expect(input).toHaveAttribute('aria-describedby', 'street-error');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export const WithDescriptionAndError: Story = {
|
||||||
|
args: {
|
||||||
|
label: 'BSN',
|
||||||
|
fieldId: 'bsn',
|
||||||
|
description: '9 cijfers',
|
||||||
|
error: 'Ongeldig BSN.',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
const input = canvas.getByRole('textbox');
|
||||||
|
await expect(input).toHaveAttribute('aria-describedby', 'bsn-desc bsn-error');
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component, forwardRef, input } from '@angular/core';
|
import { Component, booleanAttribute, forwardRef, input } from '@angular/core';
|
||||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||||
|
|
||||||
/** Atom: text input. Utrecht textbox wired up as a form control (ngModel/reactive). */
|
/** Atom: text input. Utrecht textbox wired up as a form control (ngModel/reactive). */
|
||||||
@@ -22,7 +22,7 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
|||||||
[type]="type()"
|
[type]="type()"
|
||||||
[id]="inputId()"
|
[id]="inputId()"
|
||||||
[attr.aria-invalid]="invalid() ? 'true' : null"
|
[attr.aria-invalid]="invalid() ? 'true' : null"
|
||||||
[attr.aria-describedby]="invalid() && inputId() ? inputId() + '-error' : null"
|
[attr.aria-describedby]="describedBy()"
|
||||||
[placeholder]="placeholder()"
|
[placeholder]="placeholder()"
|
||||||
[disabled]="disabled"
|
[disabled]="disabled"
|
||||||
[value]="value"
|
[value]="value"
|
||||||
@@ -39,12 +39,24 @@ export class TextInputComponent implements ControlValueAccessor {
|
|||||||
placeholder = input('');
|
placeholder = input('');
|
||||||
invalid = input(false);
|
invalid = input(false);
|
||||||
inputId = input<string>();
|
inputId = input<string>();
|
||||||
|
/** Set when the paired form-field renders a `-desc` hint, so it gets announced. */
|
||||||
|
hasDescription = input(false, { transform: booleanAttribute });
|
||||||
|
|
||||||
value = '';
|
value = '';
|
||||||
disabled = false;
|
disabled = false;
|
||||||
onChange: (v: string) => void = () => {};
|
onChange: (v: string) => void = () => {};
|
||||||
onTouched: () => 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) {
|
onInput(e: Event) {
|
||||||
this.value = (e.target as HTMLInputElement).value;
|
this.value = (e.target as HTMLInputElement).value;
|
||||||
this.onChange(this.value);
|
this.onChange(this.value);
|
||||||
|
|||||||
Reference in New Issue
Block a user