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 38380b3fa11c3c6e7443830e764c94b1a041728c..f410d028556ac30087622f7a09660cdad8e6dadb 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 70a8bdfdccc7b1c6f7b3e55f0fd8cd74d6a2ca55..37336816f4fcff3a8d95ffcfe977c541794a75a8 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 2533dc1ed4b63eec0da6aac3d7969a8b34172740..1cc8b07470d8086854cee7b310342c8697b4eed5 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 dfab859671455c3354bd14297d069ffde5474cde..bbb9f693fc54a9760f0bb33b006848b12aba6554 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 c4c009107436b4b7a8837c0fa9c31c4add1008d6..3c116fb0b27c3f1b3b24807e7a0e26affb514b00 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 45f7c42c8f3f609567e89d3075411ac652eb8882..19ae4d4d435edb97ec43d2512eda5f6ab371c7aa 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 39ac3109643ea6771e5199ba3e9bba49cef69c37..753a2c385a768d890c272847d220b89f8de34c9e 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 c7032ef03b58627369f4857d0b2bda4546d092b5..9203da053f1a13a071244c009c6516c213de5434 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 224dbe8cc5d73df87e45d77700b0ffca1efdb03e..cb566bafe0c5a5a804ae3e10491c0ca19faf4c40 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 8844b8075d1e66c181c05cbddf1da8669c698a06..fca8d2a2ecfbec56310a3ec835f03459edb4ec27 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 ffadf796c9031418fda5463896a1452351f2e109..3b3750bd3174bfaeecf2821d4dc29d9e5c6a4d4e 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 66ed08e7f973dad14e092c899de2880ff7d8f515..8c33dd8452e8f8bc07db1cfff2a2910dbe279da1 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 cd01ba0190c68b4f9fec34f3e94496e6d86f73aa..4a8ddff09949312893bf0faa30b59918d5461ce5 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 f3eb84af9d638567a3bcbdc644b400abf344643e..d3c04b6c82bb491b5f25652bd89e6af37595dd85 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 b0cb78f8a1d59efdddeb25e26c4deda6a746d7fa..7f8f416bf9b8a56b3f17bf41d79572d012bdbdcb 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`; +}