Skip to content

Commit 15079ce

Browse files
authored
Merge pull request DSpace#4199 from 4Science/task/main/DURACOM-347
Fix accessibility issues with placeholder text in submission forms by removing placeholders
2 parents ac2c970 + a448157 commit 15079ce

11 files changed

+99
-20
lines changed

src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77
@let showClearfix = shouldShowClearfix();
88
@let showErrors = shouldShowErrorMessages();
99

10-
@if (!isCheckbox && hasLabel) {
10+
@if (!isCheckbox && hasLabel && !isDateField) {
1111
<label
1212
[id]="'label_' + model.id"
1313
[for]="id"
1414
class="form-label"
1515
[innerHTML]="(model.required && model.label) ? (model.label | translate) + ' *' : (model.label | translate)"
16-
[ngClass]="[getClass('element', 'label'), getClass('grid', 'label')]"></label>
16+
[ngClass]="[getClass('element', 'label'), getClass('grid', 'label')]"></label>
1717
}
1818
<ng-container *ngTemplateOutlet="startTemplate?.templateRef; context: { $implicit: model };"></ng-container>
1919
<!-- Should be *ngIf instead of class d-none, but that breaks the #componentViewContainer reference-->

src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,8 @@ describe('DsDynamicFormControlContainerComponent test suite', () => {
238238
const testItem: Item = new Item();
239239
const testWSI: WorkspaceItem = new WorkspaceItem();
240240
testWSI.item = of(createSuccessfulRemoteDataObject(testItem));
241+
const renderer = jasmine.createSpyObj('Renderer2', ['setAttribute']);
242+
241243
const actions$: ReplaySubject<any> = new ReplaySubject<any>(1);
242244

243245
beforeEach(waitForAsync(() => {
@@ -303,6 +305,7 @@ describe('DsDynamicFormControlContainerComponent test suite', () => {
303305
});
304306

305307
fixture.detectChanges();
308+
renderer.setAttribute.calls.reset();
306309
testElement = debugElement.query(By.css(`input[id='${testModel.id}']`));
307310
}));
308311

@@ -452,4 +455,43 @@ describe('DsDynamicFormControlContainerComponent test suite', () => {
452455
});
453456
});
454457

458+
it('should not show a label if is a checkbox or a date field', () => {
459+
const checkboxLabel = fixture.debugElement.query(By.css('#label_' + formModel[0].id));
460+
const dsDatePickerLabel = fixture.debugElement.query(By.css('#label_' + formModel[22].id));
461+
462+
expect(checkboxLabel).toBeNull();
463+
expect(dsDatePickerLabel).toBeNull();
464+
});
465+
466+
it('should not call handleAriaLabelForLibraryComponents if is SSR', () => {
467+
(component as any).platformId = 'server';
468+
(component as any).componentRef = {
469+
instance: new DynamicNGBootstrapInputComponent(null, null),
470+
location: { nativeElement: document.createElement('div') },
471+
} as any;
472+
fixture.detectChanges();
473+
474+
(component as any).handleAriaLabelForLibraryComponents();
475+
476+
expect(renderer.setAttribute).not.toHaveBeenCalled();
477+
});
478+
479+
it('should set aria-label when valid input and additional property ariaLabel exist and is on browser', () => {
480+
(component as any).platformId = 'browser';
481+
const inputEl = document.createElement('input');
482+
const hostEl = {
483+
querySelector: jasmine.createSpy('querySelector').and.returnValue(inputEl),
484+
};
485+
486+
(component as any).componentRef = {
487+
instance: new DynamicNGBootstrapInputComponent(null, null),
488+
location: { nativeElement: hostEl },
489+
} as any;
490+
(component as any).renderer = renderer;
491+
component.model = { additional: { ariaLabel: 'Accessible Label' } } as any;
492+
fixture.detectChanges();
493+
(component as any).handleAriaLabelForLibraryComponents();
494+
expect(renderer.setAttribute).toHaveBeenCalledWith(inputEl, 'aria-label', 'Accessible Label');
495+
});
496+
455497
});

src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
AsyncPipe,
3+
isPlatformBrowser,
34
NgClass,
45
NgTemplateOutlet,
56
} from '@angular/common';
@@ -19,7 +20,9 @@ import {
1920
OnDestroy,
2021
OnInit,
2122
Output,
23+
PLATFORM_ID,
2224
QueryList,
25+
Renderer2,
2326
SimpleChanges,
2427
Type,
2528
ViewChild,
@@ -79,10 +82,12 @@ import {
7982
import {
8083
DYNAMIC_FORM_CONTROL_MAP_FN,
8184
DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX,
85+
DynamicFormArrayComponent,
8286
DynamicFormArrayGroupModel,
8387
DynamicFormArrayModel,
8488
DynamicFormComponentService,
8589
DynamicFormControl,
90+
DynamicFormControlComponent,
8691
DynamicFormControlContainerComponent,
8792
DynamicFormControlEvent,
8893
DynamicFormControlEventType,
@@ -129,6 +134,7 @@ import { DsDynamicTypeBindRelationService } from './ds-dynamic-type-bind-relatio
129134
import { ExistingMetadataListElementComponent } from './existing-metadata-list-element/existing-metadata-list-element.component';
130135
import { ExistingRelationListElementComponent } from './existing-relation-list-element/existing-relation-list-element.component';
131136
import { DYNAMIC_FORM_CONTROL_TYPE_CUSTOM_SWITCH } from './models/custom-switch/custom-switch.model';
137+
import { DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER } from './models/date-picker/date-picker.model';
132138
import { DynamicConcatModel } from './models/ds-dynamic-concat.model';
133139
import { DsDynamicLookupRelationModalComponent } from './relation-lookup-modal/dynamic-lookup-relation-modal.component';
134140
import { NameVariantService } from './relation-lookup-modal/name-variant.service';
@@ -227,6 +233,8 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
227233
@Inject(APP_CONFIG) protected appConfig: AppConfig,
228234
@Inject(DYNAMIC_FORM_CONTROL_MAP_FN) protected dynamicFormControlFn: DynamicFormControlMapFn,
229235
private actions$: Actions,
236+
protected renderer: Renderer2,
237+
@Inject(PLATFORM_ID) protected platformId: string,
230238
) {
231239
super(ref, componentFactoryResolver, layoutService, validationService, dynamicFormComponentService, relationService);
232240
this.fetchThumbnail = this.appConfig.browseBy.showThumbnails;
@@ -336,6 +344,11 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
336344
return this.model.type === DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX || this.model.type === DYNAMIC_FORM_CONTROL_TYPE_CUSTOM_SWITCH;
337345
}
338346

347+
348+
get isDateField(): boolean {
349+
return this.model.type === DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER;
350+
}
351+
339352
ngOnChanges(changes: SimpleChanges) {
340353
if (changes && !this.isRelationship && hasValue(this.group.get(this.model.id))) {
341354
super.ngOnChanges(changes);
@@ -357,6 +370,7 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
357370

358371
ngAfterViewInit() {
359372
this.showErrorMessagesPreviousStage = this.showErrorMessages;
373+
this.handleAriaLabelForLibraryComponents();
360374
}
361375

362376
protected createFormControlComponent(): void {
@@ -562,6 +576,24 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
562576
}
563577
}
564578

579+
private handleAriaLabelForLibraryComponents(): void {
580+
if (!isPlatformBrowser(this.platformId)) {
581+
return;
582+
}
583+
584+
if ((this.componentRef.instance instanceof DynamicFormControlComponent) &&
585+
!(this.componentRef.instance instanceof DynamicFormArrayComponent) &&
586+
this.componentRef.location.nativeElement) {
587+
const inputEl: HTMLElement | null =
588+
this.componentRef.location.nativeElement.querySelector('input,textarea,select,[role="textbox"]');
589+
590+
591+
if (inputEl && this.model?.additional?.ariaLabel) {
592+
this.renderer.setAttribute(inputEl, 'aria-label', this.model.additional.ariaLabel);
593+
}
594+
}
595+
}
596+
565597
/**
566598
* Determines whether a form field should be validated based on its parent group's state.
567599
* @returns {boolean} True if the field should be validated, false otherwise

src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
<fieldset class="d-flex justify-content-start flex-wrap gap-2">
33
@if (!model.repeatable) {
44
<legend [id]="'legend_' + model.id" [ngClass]="[getClass('element', 'label'), getClass('grid', 'label')]">
5-
{{ model.placeholder }}
5+
{{model.label}}
66
@if (model.required) {
77
<span>*</span>
88
}
9-
</legend>
10-
}
9+
</legend>
10+
}
1111
<ds-number-picker
1212
tabindex="0"
1313
[id]="model.id + '_year'"

src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.html

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,7 @@
6565
class="form-control"
6666
[attr.aria-labelledby]="'label_' + model.id"
6767
[attr.autoComplete]="model.autoComplete"
68-
[attr.aria-label]="model.label | translate"
69-
[class.is-invalid]="showErrorMessages"
68+
[attr.aria-label]="(model.label || model?.additional?.ariaLabel) | translate" [class.is-invalid]="showErrorMessages"
7069
[id]="model.id"
7170
[inputFormatter]="formatter"
7271
[name]="model.name"
@@ -96,8 +95,7 @@
9695
<input class="form-control"
9796
[attr.aria-labelledby]="'label_' + model.id"
9897
[attr.autoComplete]="model.autoComplete"
99-
[attr.aria-label]="model.label | translate"
100-
[class.is-invalid]="showErrorMessages"
98+
[attr.aria-label]="(model.label || model?.additional?.ariaLabel) | translate" [class.is-invalid]="showErrorMessages"
10199
[class.tree-input]="!model.readOnly"
102100
[id]="id"
103101
[name]="model.name"

src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
}
1515
<input class="form-control"
1616
[attr.aria-controls]="'combobox_' + id + '_listbox'"
17-
[attr.aria-label]="model.placeholder"
17+
[attr.aria-label]="model.label"
1818
[attr.autoComplete]="model.autoComplete"
1919
[class.is-invalid]="showErrorMessages"
2020
[class.scrollable-dropdown-input]="!model.readOnly"
@@ -32,11 +32,11 @@
3232

3333
<div #dropdownMenu ngbDropdownMenu
3434
class="dropdown-menu scrollable-dropdown-menu w-100"
35-
[attr.aria-label]="model.placeholder | translate">
35+
[attr.aria-label]="model.label | translate">
3636
<div class="scrollable-menu"
3737
role="listbox"
3838
[id]="'combobox_' + id + '_listbox'"
39-
[attr.aria-label]="model.placeholder | translate"
39+
[attr.aria-label]="model.label | translate"
4040
infiniteScroll
4141
[infiniteScrollDistance]="2"
4242
[infiniteScrollThrottle]="50"

src/app/shared/form/builder/parsers/concat-field-parser.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,18 +95,18 @@ export class ConcatFieldParser extends FieldParser {
9595
concatGroup.disabled = input1ModelConfig.readOnly;
9696

9797
if (isNotEmpty(this.firstPlaceholder)) {
98-
input1ModelConfig.placeholder = this.firstPlaceholder;
98+
input1ModelConfig.label = this.firstPlaceholder;
9999
}
100100

101101
if (isNotEmpty(this.secondPlaceholder)) {
102-
input2ModelConfig.placeholder = this.secondPlaceholder;
102+
input2ModelConfig.label = this.secondPlaceholder;
103103
}
104104

105105
// Split placeholder if is like 'placeholder1/placeholder2'
106106
const placeholder = this.configData.label.split('/');
107107
if (placeholder.length === 2) {
108-
input1ModelConfig.placeholder = placeholder[0];
109-
input2ModelConfig.placeholder = placeholder[1];
108+
input1ModelConfig.label = placeholder[0];
109+
input2ModelConfig.label = placeholder[1];
110110
}
111111

112112
const model1 = new DsDynamicInputModel(input1ModelConfig, clsInput);

src/app/shared/form/builder/parsers/date-field-parser.spec.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,11 @@ describe('DateFieldParser test suite', () => {
6868

6969
expect(fieldModel.value).toEqual(expectedValue);
7070
});
71+
72+
it('should skip setting the placeholder', () => {
73+
const parser = new DateFieldParser(submissionId, field, initFormValues, parserOptions, null, translateService);
74+
const fieldModel = parser.parse();
75+
76+
expect(fieldModel.placeholder).toBeNull();
77+
});
7178
});

src/app/shared/form/builder/parsers/date-field-parser.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ export class DateFieldParser extends FieldParser {
1212

1313
public modelFactory(fieldValue?: FormFieldMetadataValueObject, label?: boolean): any {
1414
let malformedDate = false;
15-
const inputDateModelConfig: DynamicDsDateControlModelConfig = this.initModel(null, false, true);
16-
inputDateModelConfig.legend = this.configData.label;
15+
const inputDateModelConfig: DynamicDsDateControlModelConfig = this.initModel(null, label, true);
16+
inputDateModelConfig.legend = this.configData.repeatable ? null : this.configData.label;
1717
inputDateModelConfig.disabled = inputDateModelConfig.readOnly;
1818
inputDateModelConfig.toggleIcon = 'fas fa-calendar';
1919
this.setValues(inputDateModelConfig as any, fieldValue);

src/app/shared/form/builder/parsers/field-parser.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,8 @@ export abstract class FieldParser {
340340
if (hint) {
341341
controlModel.hint = this.configData.hints || '&nbsp;';
342342
}
343-
controlModel.placeholder = this.configData.label;
343+
344+
controlModel.additional = { ...controlModel.additional, ariaLabel: this.configData.label };
344345

345346
if (this.configData.mandatory && setErrors) {
346347
this.markAsRequired(controlModel);

0 commit comments

Comments
 (0)