diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html index 1cb1abb1185..f844fab281f 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html @@ -7,13 +7,13 @@ @let showClearfix = shouldShowClearfix(); @let showErrors = shouldShowErrorMessages(); - @if (!isCheckbox && hasLabel) { + @if (!isCheckbox && hasLabel && !isDateField) { + [ngClass]="[getClass('element', 'label'), getClass('grid', 'label')]"> } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts index a89c7544377..8fb617d0336 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts @@ -238,6 +238,8 @@ describe('DsDynamicFormControlContainerComponent test suite', () => { const testItem: Item = new Item(); const testWSI: WorkspaceItem = new WorkspaceItem(); testWSI.item = of(createSuccessfulRemoteDataObject(testItem)); + const renderer = jasmine.createSpyObj('Renderer2', ['setAttribute']); + const actions$: ReplaySubject = new ReplaySubject(1); beforeEach(waitForAsync(() => { @@ -303,6 +305,7 @@ describe('DsDynamicFormControlContainerComponent test suite', () => { }); fixture.detectChanges(); + renderer.setAttribute.calls.reset(); testElement = debugElement.query(By.css(`input[id='${testModel.id}']`)); })); @@ -452,4 +455,43 @@ describe('DsDynamicFormControlContainerComponent test suite', () => { }); }); + it('should not show a label if is a checkbox or a date field', () => { + const checkboxLabel = fixture.debugElement.query(By.css('#label_' + formModel[0].id)); + const dsDatePickerLabel = fixture.debugElement.query(By.css('#label_' + formModel[22].id)); + + expect(checkboxLabel).toBeNull(); + expect(dsDatePickerLabel).toBeNull(); + }); + + it('should not call handleAriaLabelForLibraryComponents if is SSR', () => { + (component as any).platformId = 'server'; + (component as any).componentRef = { + instance: new DynamicNGBootstrapInputComponent(null, null), + location: { nativeElement: document.createElement('div') }, + } as any; + fixture.detectChanges(); + + (component as any).handleAriaLabelForLibraryComponents(); + + expect(renderer.setAttribute).not.toHaveBeenCalled(); + }); + + it('should set aria-label when valid input and additional property ariaLabel exist and is on browser', () => { + (component as any).platformId = 'browser'; + const inputEl = document.createElement('input'); + const hostEl = { + querySelector: jasmine.createSpy('querySelector').and.returnValue(inputEl), + }; + + (component as any).componentRef = { + instance: new DynamicNGBootstrapInputComponent(null, null), + location: { nativeElement: hostEl }, + } as any; + (component as any).renderer = renderer; + component.model = { additional: { ariaLabel: 'Accessible Label' } } as any; + fixture.detectChanges(); + (component as any).handleAriaLabelForLibraryComponents(); + expect(renderer.setAttribute).toHaveBeenCalledWith(inputEl, 'aria-label', 'Accessible Label'); + }); + }); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts index cf45e8fdab7..3bdd5d67f44 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts @@ -1,5 +1,6 @@ import { AsyncPipe, + isPlatformBrowser, NgClass, NgTemplateOutlet, } from '@angular/common'; @@ -19,7 +20,9 @@ import { OnDestroy, OnInit, Output, + PLATFORM_ID, QueryList, + Renderer2, SimpleChanges, Type, ViewChild, @@ -79,10 +82,12 @@ import { import { DYNAMIC_FORM_CONTROL_MAP_FN, DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX, + DynamicFormArrayComponent, DynamicFormArrayGroupModel, DynamicFormArrayModel, DynamicFormComponentService, DynamicFormControl, + DynamicFormControlComponent, DynamicFormControlContainerComponent, DynamicFormControlEvent, DynamicFormControlEventType, @@ -129,6 +134,7 @@ import { DsDynamicTypeBindRelationService } from './ds-dynamic-type-bind-relatio import { ExistingMetadataListElementComponent } from './existing-metadata-list-element/existing-metadata-list-element.component'; import { ExistingRelationListElementComponent } from './existing-relation-list-element/existing-relation-list-element.component'; import { DYNAMIC_FORM_CONTROL_TYPE_CUSTOM_SWITCH } from './models/custom-switch/custom-switch.model'; +import { DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER } from './models/date-picker/date-picker.model'; import { DynamicConcatModel } from './models/ds-dynamic-concat.model'; import { DsDynamicLookupRelationModalComponent } from './relation-lookup-modal/dynamic-lookup-relation-modal.component'; import { NameVariantService } from './relation-lookup-modal/name-variant.service'; @@ -227,6 +233,8 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo @Inject(APP_CONFIG) protected appConfig: AppConfig, @Inject(DYNAMIC_FORM_CONTROL_MAP_FN) protected dynamicFormControlFn: DynamicFormControlMapFn, private actions$: Actions, + protected renderer: Renderer2, + @Inject(PLATFORM_ID) protected platformId: string, ) { super(ref, componentFactoryResolver, layoutService, validationService, dynamicFormComponentService, relationService); this.fetchThumbnail = this.appConfig.browseBy.showThumbnails; @@ -336,6 +344,11 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo return this.model.type === DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX || this.model.type === DYNAMIC_FORM_CONTROL_TYPE_CUSTOM_SWITCH; } + + get isDateField(): boolean { + return this.model.type === DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER; + } + ngOnChanges(changes: SimpleChanges) { if (changes && !this.isRelationship && hasValue(this.group.get(this.model.id))) { super.ngOnChanges(changes); @@ -357,6 +370,7 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo ngAfterViewInit() { this.showErrorMessagesPreviousStage = this.showErrorMessages; + this.handleAriaLabelForLibraryComponents(); } protected createFormControlComponent(): void { @@ -562,6 +576,24 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo } } + private handleAriaLabelForLibraryComponents(): void { + if (!isPlatformBrowser(this.platformId)) { + return; + } + + if ((this.componentRef.instance instanceof DynamicFormControlComponent) && + !(this.componentRef.instance instanceof DynamicFormArrayComponent) && + this.componentRef.location.nativeElement) { + const inputEl: HTMLElement | null = + this.componentRef.location.nativeElement.querySelector('input,textarea,select,[role="textbox"]'); + + + if (inputEl && this.model?.additional?.ariaLabel) { + this.renderer.setAttribute(inputEl, 'aria-label', this.model.additional.ariaLabel); + } + } + } + /** * Determines whether a form field should be validated based on its parent group's state. * @returns {boolean} True if the field should be validated, false otherwise diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.html index 5d38f9e9084..3e6e05c7f74 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.html @@ -2,12 +2,12 @@
@if (!model.repeatable) { - {{ model.placeholder }} + {{model.label}} @if (model.required) { * } - - } + + } + [attr.aria-label]="model.label | translate">
{ expect(fieldModel.value).toEqual(expectedValue); }); + + it('should skip setting the placeholder', () => { + const parser = new DateFieldParser(submissionId, field, initFormValues, parserOptions, null, translateService); + const fieldModel = parser.parse(); + + expect(fieldModel.placeholder).toBeNull(); + }); }); diff --git a/src/app/shared/form/builder/parsers/date-field-parser.ts b/src/app/shared/form/builder/parsers/date-field-parser.ts index 0e0f3efd5bc..77cd0872ea0 100644 --- a/src/app/shared/form/builder/parsers/date-field-parser.ts +++ b/src/app/shared/form/builder/parsers/date-field-parser.ts @@ -12,8 +12,8 @@ export class DateFieldParser extends FieldParser { public modelFactory(fieldValue?: FormFieldMetadataValueObject, label?: boolean): any { let malformedDate = false; - const inputDateModelConfig: DynamicDsDateControlModelConfig = this.initModel(null, false, true); - inputDateModelConfig.legend = this.configData.label; + const inputDateModelConfig: DynamicDsDateControlModelConfig = this.initModel(null, label, true); + inputDateModelConfig.legend = this.configData.repeatable ? null : this.configData.label; inputDateModelConfig.disabled = inputDateModelConfig.readOnly; inputDateModelConfig.toggleIcon = 'fas fa-calendar'; this.setValues(inputDateModelConfig as any, fieldValue); diff --git a/src/app/shared/form/builder/parsers/field-parser.ts b/src/app/shared/form/builder/parsers/field-parser.ts index eeb3dd1f67f..5c31eafff77 100644 --- a/src/app/shared/form/builder/parsers/field-parser.ts +++ b/src/app/shared/form/builder/parsers/field-parser.ts @@ -340,7 +340,8 @@ export abstract class FieldParser { if (hint) { controlModel.hint = this.configData.hints || ' '; } - controlModel.placeholder = this.configData.label; + + controlModel.additional = { ...controlModel.additional, ariaLabel: this.configData.label }; if (this.configData.mandatory && setErrors) { this.markAsRequired(controlModel); diff --git a/src/app/shared/form/number-picker/number-picker.component.html b/src/app/shared/form/number-picker/number-picker.component.html index 807bb0acf13..e704e278c6a 100644 --- a/src/app/shared/form/number-picker/number-picker.component.html +++ b/src/app/shared/form/number-picker/number-picker.component.html @@ -26,7 +26,6 @@ [readonly]="disabled" [disabled]="disabled" [ngClass]="{'is-invalid': invalid}" - placeholder="{{'form.number-picker.label.' + placeholder | translate }}" title="{{'form.number-picker.label.' + placeholder | translate}}" [attr.aria-labelledby]="id + ' increment-' + id + ' decrement-' + id" >