From 10a784c7b63fd198520ea306f52f76ea653dc03b Mon Sep 17 00:00:00 2001 From: Martin <git@mail.de> Date: Tue, 18 Mar 2025 09:49:49 +0100 Subject: [PATCH] OZG-7591 add validation error id for aria-describedby --- .../user-form-roles.component.html | 1 + .../user-form-roles.component.ts | 3 +- .../checkbox-editor.component.html | 2 ++ .../checkbox-editor.component.ts | 4 ++- .../text-editor/text-editor.component.html | 2 ++ .../form/text-editor/text-editor.component.ts | 4 ++- .../textarea-editor.component.html | 2 ++ .../textarea-editor.component.ts | 4 ++- .../validation-error.component.html | 2 +- .../validation-error.component.ts | 1 + .../lib/form/checkbox/checkbox.component.ts | 4 ++- .../error-message/error-message.component.ts | 4 ++- .../form/text-input/text-input.component.ts | 4 ++- .../lib/form/textarea/textarea.component.ts | 4 ++- .../lib/validation/tech.validation.util.ts | 36 ++++++------------- 15 files changed, 43 insertions(+), 34 deletions(-) diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.html b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.html index 38380b3fa1..f410d02855 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.html +++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.html @@ -1,5 +1,6 @@ <h2 class="heading-2 mt-4">Rollen für OZG-Cloud *</h2> <ods-validation-error + [id]="validationErrorId" [invalidParams]="invalidParams$ | async" label="Rollen" data-test-id="rollen-error" diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.ts b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.ts index 70a8bdfdcc..37336816f4 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.ts +++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.ts @@ -1,4 +1,4 @@ -import { InvalidParam } from '@alfa-client/tech-shared'; +import { generateValidationErrorId, InvalidParam } from '@alfa-client/tech-shared'; import { AsyncPipe } from '@angular/common'; import { Component, Input, OnInit } from '@angular/core'; import { AbstractControl, FormControlStatus, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms'; @@ -26,6 +26,7 @@ export class UserFormRolesComponent implements OnInit { public invalidParams$: Observable<InvalidParam[]> = of([]); public readonly UserFormService = UserFormService; + public readonly validationErrorId: string = generateValidationErrorId(); ngOnInit(): void { const control: AbstractControl = this.formGroupParent.controls[UserFormService.CLIENT_ROLES]; diff --git a/alfa-client/libs/design-component/src/lib/form/checkbox-editor/checkbox-editor.component.html b/alfa-client/libs/design-component/src/lib/form/checkbox-editor/checkbox-editor.component.html index 2533dc1ed4..1cc8b07470 100644 --- a/alfa-client/libs/design-component/src/lib/form/checkbox-editor/checkbox-editor.component.html +++ b/alfa-client/libs/design-component/src/lib/form/checkbox-editor/checkbox-editor.component.html @@ -29,9 +29,11 @@ [label]="label" [disabled]="control.disabled" [hasError]="hasError" + [ariaDescribedBy]="validationErrorId" > <ods-validation-error error + [id]="validationErrorId" [invalidParams]="invalidParams" [label]="label" [attr.data-test-id]="(label | convertForDataTest) + '-checkbox-editor-error'" diff --git a/alfa-client/libs/design-component/src/lib/form/checkbox-editor/checkbox-editor.component.ts b/alfa-client/libs/design-component/src/lib/form/checkbox-editor/checkbox-editor.component.ts index dfab859671..bbb9f693fc 100644 --- a/alfa-client/libs/design-component/src/lib/form/checkbox-editor/checkbox-editor.component.ts +++ b/alfa-client/libs/design-component/src/lib/form/checkbox-editor/checkbox-editor.component.ts @@ -21,7 +21,7 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { ConvertForDataTestPipe } from '@alfa-client/tech-shared'; +import { ConvertForDataTestPipe, generateValidationErrorId } from '@alfa-client/tech-shared'; import { Component, Input } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; import { CheckboxComponent } from '@ods/system'; @@ -38,6 +38,8 @@ export class CheckboxEditorComponent extends FormControlEditorAbstractComponent @Input() inputId: string; @Input() label: string; + public readonly validationErrorId: string = generateValidationErrorId(); + get hasError(): boolean { return this.invalidParams.length > 0; } diff --git a/alfa-client/libs/design-component/src/lib/form/text-editor/text-editor.component.html b/alfa-client/libs/design-component/src/lib/form/text-editor/text-editor.component.html index c4c0091074..3c116fb0b2 100644 --- a/alfa-client/libs/design-component/src/lib/form/text-editor/text-editor.component.html +++ b/alfa-client/libs/design-component/src/lib/form/text-editor/text-editor.component.html @@ -34,9 +34,11 @@ [required]="isRequired" [focus]="focus" [showLabel]="showLabel" + [ariaDescribedBy]="validationErrorId" > <ods-validation-error error + [id]="validationErrorId" [invalidParams]="invalidParams" [label]="label" [attr.data-test-id]="dataTestId + '-text-editor-error'" diff --git a/alfa-client/libs/design-component/src/lib/form/text-editor/text-editor.component.ts b/alfa-client/libs/design-component/src/lib/form/text-editor/text-editor.component.ts index 45f7c42c8f..19ae4d4d43 100644 --- a/alfa-client/libs/design-component/src/lib/form/text-editor/text-editor.component.ts +++ b/alfa-client/libs/design-component/src/lib/form/text-editor/text-editor.component.ts @@ -21,7 +21,7 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { ConvertForDataTestPipe } from '@alfa-client/tech-shared'; +import { ConvertForDataTestPipe, generateValidationErrorId } from '@alfa-client/tech-shared'; import { CommonModule } from '@angular/common'; import { Component, Input } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; @@ -44,6 +44,8 @@ export class TextEditorComponent extends FormControlEditorAbstractComponent { @Input() showLabel: boolean = true; @Input() dataTestId: string; + public readonly validationErrorId: string = generateValidationErrorId(); + get variant(): string { return this.invalidParams.length > 0 ? 'error' : 'default'; } diff --git a/alfa-client/libs/design-component/src/lib/form/textarea-editor/textarea-editor.component.html b/alfa-client/libs/design-component/src/lib/form/textarea-editor/textarea-editor.component.html index 39ac310964..753a2c385a 100644 --- a/alfa-client/libs/design-component/src/lib/form/textarea-editor/textarea-editor.component.html +++ b/alfa-client/libs/design-component/src/lib/form/textarea-editor/textarea-editor.component.html @@ -34,9 +34,11 @@ [focus]="focus" [isResizable]="isResizable" [showLabel]="showLabel" + [ariaDescribedBy]="validationErrorId" > <ods-validation-error error + [id]="validationErrorId" [invalidParams]="invalidParams" [label]="label" [attr.data-test-id]="(label | convertForDataTest) + '-textarea-editor-error'" diff --git a/alfa-client/libs/design-component/src/lib/form/textarea-editor/textarea-editor.component.ts b/alfa-client/libs/design-component/src/lib/form/textarea-editor/textarea-editor.component.ts index c7032ef03b..9203da053f 100644 --- a/alfa-client/libs/design-component/src/lib/form/textarea-editor/textarea-editor.component.ts +++ b/alfa-client/libs/design-component/src/lib/form/textarea-editor/textarea-editor.component.ts @@ -21,7 +21,7 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { ConvertForDataTestPipe } from '@alfa-client/tech-shared'; +import { ConvertForDataTestPipe, generateValidationErrorId } from '@alfa-client/tech-shared'; import { CommonModule } from '@angular/common'; import { Component, Input } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; @@ -44,6 +44,8 @@ export class TextareaEditorComponent extends FormControlEditorAbstractComponent @Input() isResizable: boolean = true; @Input() showLabel: boolean = true; + public readonly validationErrorId: string = generateValidationErrorId(); + get variant(): string { return this.invalidParams.length > 0 ? 'error' : 'default'; } diff --git a/alfa-client/libs/design-component/src/lib/form/validation-error/validation-error.component.html b/alfa-client/libs/design-component/src/lib/form/validation-error/validation-error.component.html index 224dbe8cc5..cb566bafe0 100644 --- a/alfa-client/libs/design-component/src/lib/form/validation-error/validation-error.component.html +++ b/alfa-client/libs/design-component/src/lib/form/validation-error/validation-error.component.html @@ -24,5 +24,5 @@ --> <ng-container *ngFor="let invalidParam of invalidParams" - ><ods-error-message [text]="message(invalidParam)"></ods-error-message + ><ods-error-message [id]="id" [text]="message(invalidParam)"></ods-error-message ></ng-container> diff --git a/alfa-client/libs/design-component/src/lib/form/validation-error/validation-error.component.ts b/alfa-client/libs/design-component/src/lib/form/validation-error/validation-error.component.ts index 8844b8075d..fca8d2a2ec 100644 --- a/alfa-client/libs/design-component/src/lib/form/validation-error/validation-error.component.ts +++ b/alfa-client/libs/design-component/src/lib/form/validation-error/validation-error.component.ts @@ -33,6 +33,7 @@ import { ErrorMessageComponent } from '@ods/system'; templateUrl: './validation-error.component.html', }) export class ValidationErrorComponent { + @Input({ required: true }) id: string; @Input() label: string; @Input() invalidParams: InvalidParam[]; diff --git a/alfa-client/libs/design-system/src/lib/form/checkbox/checkbox.component.ts b/alfa-client/libs/design-system/src/lib/form/checkbox/checkbox.component.ts index ffadf796c9..3b3750bd31 100644 --- a/alfa-client/libs/design-system/src/lib/form/checkbox/checkbox.component.ts +++ b/alfa-client/libs/design-system/src/lib/form/checkbox/checkbox.component.ts @@ -21,7 +21,7 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { ConvertForDataTestPipe } from '@alfa-client/tech-shared'; +import { ConvertForDataTestPipe, EMPTY_STRING } from '@alfa-client/tech-shared'; import { CommonModule } from '@angular/common'; import { Component, Input } from '@angular/core'; import { FormControl, ReactiveFormsModule } from '@angular/forms'; @@ -45,6 +45,7 @@ import { FormControl, ReactiveFormsModule } from '@angular/forms'; [attr.id]="inputId" [attr.disabled]="disabled ? true : null" [attr.data-test-id]="(label | convertForDataTest) + '-checkbox-editor'" + [attr.aria-describedby]="ariaDescribedBy" /> <label class="leading-5 text-text" [attr.for]="inputId">{{ label }}</label> <svg @@ -69,4 +70,5 @@ export class CheckboxComponent { @Input() label: string; @Input() disabled: boolean = false; @Input() hasError: boolean = false; + @Input() ariaDescribedBy: string = EMPTY_STRING; } diff --git a/alfa-client/libs/design-system/src/lib/form/error-message/error-message.component.ts b/alfa-client/libs/design-system/src/lib/form/error-message/error-message.component.ts index 66ed08e7f9..8c33dd8452 100644 --- a/alfa-client/libs/design-system/src/lib/form/error-message/error-message.component.ts +++ b/alfa-client/libs/design-system/src/lib/form/error-message/error-message.component.ts @@ -21,6 +21,7 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ +import { EMPTY_STRING } from '@alfa-client/tech-shared'; import { CommonModule } from '@angular/common'; import { Component, Input } from '@angular/core'; import { ExclamationIconComponent } from '../../icons/exclamation-icon/exclamation-icon.component'; @@ -31,13 +32,14 @@ import { ExclamationIconComponent } from '../../icons/exclamation-icon/exclamati imports: [CommonModule, ExclamationIconComponent], styles: [':host {@apply flex text-error my-2 text-sm items-center font-medium}'], template: `<ods-exclamation-icon class="mr-1"></ods-exclamation-icon> - <div class="flex-grow break-all"> + <div class="flex-grow break-all" [id]="id"> {{ text }} <br *ngIf="subText" aria-hidden="true" /> {{ subText }} </div> `, }) export class ErrorMessageComponent { + @Input() id: string = EMPTY_STRING; @Input({ required: true }) text!: string; @Input() subText: string = ''; } diff --git a/alfa-client/libs/design-system/src/lib/form/text-input/text-input.component.ts b/alfa-client/libs/design-system/src/lib/form/text-input/text-input.component.ts index cd01ba0190..4a8ddff099 100644 --- a/alfa-client/libs/design-system/src/lib/form/text-input/text-input.component.ts +++ b/alfa-client/libs/design-system/src/lib/form/text-input/text-input.component.ts @@ -62,6 +62,7 @@ type TextInputVariants = VariantProps<typeof textInputVariants>; <ng-content select="[prefix]" /> </div> <input + #inputElement type="text" [id]="id" [formControl]="fieldControl" @@ -71,8 +72,8 @@ type TextInputVariants = VariantProps<typeof textInputVariants>; [attr.aria-required]="required" [attr.aria-invalid]="variant === 'error'" [attr.data-test-id]="_dataTestId + '-text-input'" + [attr.aria-describedby]="ariaDescribedBy" (click)="clickEmitter.emit()" - #inputElement /> <div *ngIf="withSuffix" class="absolute bottom-2 right-2 flex size-6 items-center justify-center"> <ng-content select="[suffix]" /> @@ -102,6 +103,7 @@ export class TextInputComponent implements AfterViewInit { @Input() set dataTestId(value: string) { if (isNotUndefined(value)) this._dataTestId = value; } + @Input() ariaDescribedBy: string = EMPTY_STRING; @Output() clickEmitter: EventEmitter<MouseEvent> = new EventEmitter<MouseEvent>(); diff --git a/alfa-client/libs/design-system/src/lib/form/textarea/textarea.component.ts b/alfa-client/libs/design-system/src/lib/form/textarea/textarea.component.ts index f3eb84af9d..d3c04b6c82 100644 --- a/alfa-client/libs/design-system/src/lib/form/textarea/textarea.component.ts +++ b/alfa-client/libs/design-system/src/lib/form/textarea/textarea.component.ts @@ -57,6 +57,7 @@ type TextareaVariants = VariantProps<typeof textareaVariants>; {{ inputLabel }}<ng-container *ngIf="required"><i aria-hidden="true">*</i></ng-container> </label> <textarea + #textAreaElement [id]="id" [formControl]="fieldControl" [rows]="rows" @@ -66,7 +67,7 @@ type TextareaVariants = VariantProps<typeof textareaVariants>; [attr.aria-required]="required" [attr.aria-invalid]="variant === 'error'" [attr.data-test-id]="(inputLabel | convertForDataTest) + '-textarea'" - #textAreaElement + [attr.aria-describedby]="ariaDescribedBy" ></textarea> <ng-content select="[error]"></ng-content> </div> @@ -94,6 +95,7 @@ export class TextareaComponent { this.textAreaElement.nativeElement.focus(); } } + @Input() ariaDescribedBy: string = EMPTY_STRING; inputLabel: string; id: string; diff --git a/alfa-client/libs/tech-shared/src/lib/validation/tech.validation.util.ts b/alfa-client/libs/tech-shared/src/lib/validation/tech.validation.util.ts index b0cb78f8a1..7f8f416bf9 100644 --- a/alfa-client/libs/tech-shared/src/lib/validation/tech.validation.util.ts +++ b/alfa-client/libs/tech-shared/src/lib/validation/tech.validation.util.ts @@ -22,7 +22,7 @@ * unter der Lizenz sind dem Lizenztext zu entnehmen. */ import { AbstractControl, UntypedFormGroup } from '@angular/forms'; -import { isEmpty, isNil } from 'lodash-es'; +import { isEmpty, isNil, uniqueId } from 'lodash-es'; import { ApiError, InvalidParam, Issue, IssueParam, ProblemDetail } from '../tech.model'; import { replacePlaceholder } from '../tech.util'; import { VALIDATION_MESSAGES, ValidationMessageCode } from './tech.validation.messages'; @@ -31,28 +31,18 @@ export function isValidationError(issue: Issue): boolean { return issue.messageCode.includes('javax.validation.constraints'); } -export function setIssueValidationError( - form: UntypedFormGroup, - issue: Issue, - pathPrefix?: string, -): void { +export function setIssueValidationError(form: UntypedFormGroup, issue: Issue, pathPrefix?: string): void { const control: AbstractControl = getControlForIssue(form, issue, pathPrefix); control.setErrors({ [issue.messageCode]: issue }); control.markAsTouched(); } -export function getControlForIssue( - form: UntypedFormGroup, - issue: Issue, - pathPrefix?: string, -): AbstractControl { +export function getControlForIssue(form: UntypedFormGroup, issue: Issue, pathPrefix?: string): AbstractControl { const fieldPath: string = getFieldPath(issue.field, pathPrefix); let curControl: AbstractControl = form; - fieldPath - .split('.') - .forEach((field) => (curControl = (<UntypedFormGroup>curControl).controls[field])); + fieldPath.split('.').forEach((field) => (curControl = (<UntypedFormGroup>curControl).controls[field])); return curControl; } @@ -66,9 +56,7 @@ export function getMessageForIssue(label: string, issue: Issue): string { } msg = replacePlaceholder(msg, 'field', label); - issue.parameters.forEach( - (param: IssueParam) => (msg = replacePlaceholder(msg, param.name, param.value)), - ); + issue.parameters.forEach((param: IssueParam) => (msg = replacePlaceholder(msg, param.name, param.value))); return msg; } @@ -84,11 +72,7 @@ export function getMessageCode(apiError: ApiError): string { return apiError.issues[0].messageCode; } -export function setInvalidParamValidationError( - form: UntypedFormGroup, - invalidParam: InvalidParam, - pathPrefix?: string, -): void { +export function setInvalidParamValidationError(form: UntypedFormGroup, invalidParam: InvalidParam, pathPrefix?: string): void { const control: AbstractControl = getControlForInvalidParam(form, invalidParam, pathPrefix); control.setErrors({ [invalidParam.reason]: invalidParam }); @@ -112,9 +96,7 @@ export function getMessageForInvalidParam(label: string, invalidParam: InvalidPa } msg = replacePlaceholder(msg, 'field', label); - invalidParam.constraintParameters.forEach( - (param: IssueParam) => (msg = replacePlaceholder(msg, param.name, param.value)), - ); + invalidParam.constraintParameters.forEach((param: IssueParam) => (msg = replacePlaceholder(msg, param.name, param.value))); return msg; } @@ -126,3 +108,7 @@ export function getFieldPath(name: string, pathPrefix: string): string { const indexOfField = name.lastIndexOf(pathPrefix) + pathPrefix.length + 1; return name.slice(indexOfField); } + +export function generateValidationErrorId(): string { + return `${uniqueId()}-validation-error`; +} -- GitLab