Skip to content

Commit 150dd9c

Browse files
authored
Merge pull request DSpace#5447 from 4Science/task/dspace-9_x/DURACOM-347
[Port dspace-9_x] Fix accessibility issues with placeholder text in submission forms by removing placeholders
2 parents 556d0f6 + 9279b3d commit 150dd9c

11 files changed

Lines changed: 101 additions & 22 deletions

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

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
<div [class.mb-2]="hasLabel || model.type === 'DATE' || (model.type !== 'GROUP' && asBootstrapFormGroup) || getClass('element', 'container').includes('mb-2')"
22
[class.d-none]="model.hidden"
3-
[formGroup]="group"
4-
[ngClass]="[getClass('element', 'container'), getClass('grid', 'container')]">
5-
@if (!isCheckbox && hasLabel) {
3+
[formGroup]="group"
4+
[ngClass]="[getClass('element', 'container'), getClass('grid', 'container')]">
5+
@if (!isCheckbox && hasLabel && !isDateField) {
66
<label
77
[id]="'label_' + model.id"
88
[for]="id"
99
class="form-label"
1010
[innerHTML]="(model.required && model.label) ? (model.label | translate) + ' *' : (model.label | translate)"
11-
[ngClass]="[getClass('element', 'label'), getClass('grid', 'label')]"></label>
11+
[ngClass]="[getClass('element', 'label'), getClass('grid', 'label')]"></label>
1212
}
1313
<ng-container *ngTemplateOutlet="startTemplate?.templateRef; context: { $implicit: model };"></ng-container>
1414
<!-- Should be *ngIf instead of class d-none, but that breaks the #componentViewContainer reference-->
@@ -20,8 +20,7 @@
2020
</div>
2121

2222
@if (hasHint && (formBuilderService.hasArrayGroupValue(model) || (!model.repeatable && (isRelationship === false || value?.value === null)) || (model.repeatable === true && context?.index === context?.context?.groups?.length - 1)) && (!showErrorMessages || errorMessages.length === 0)) {
23-
<small
24-
class="text-muted ds-hint" [innerHTML]="model.hint | translate" [ngClass]="getClass('element', 'hint')"></small>
23+
<small class="text-muted ds-hint" [innerHTML]="model.hint | translate" [ngClass]="getClass('element', 'hint')"></small>
2524
}
2625
<!-- In case of repeatable fields show empty space for all elements except the first -->
2726
@if (context?.parent?.groups?.length > 1 && (!showErrorMessages || errorMessages.length === 0)) {

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
@@ -223,6 +223,8 @@ describe('DsDynamicFormControlContainerComponent test suite', () => {
223223
testWSI.item = of(createSuccessfulRemoteDataObject(testItem));
224224
const actions$: ReplaySubject<any> = new ReplaySubject<any>(1);
225225

226+
const renderer = jasmine.createSpyObj('Renderer2', ['setAttribute']);
227+
226228
beforeEach(waitForAsync(() => {
227229

228230
TestBed.configureTestingModule({
@@ -286,6 +288,7 @@ describe('DsDynamicFormControlContainerComponent test suite', () => {
286288
});
287289

288290
fixture.detectChanges();
291+
renderer.setAttribute.calls.reset();
289292
testElement = debugElement.query(By.css(`input[id='${testModel.id}']`));
290293
}));
291294

@@ -434,4 +437,43 @@ describe('DsDynamicFormControlContainerComponent test suite', () => {
434437
});
435438
});
436439

440+
it('should not show a label if is a checkbox or a date field', () => {
441+
const checkboxLabel = fixture.debugElement.query(By.css('#label_' + formModel[0].id));
442+
const dsDatePickerLabel = fixture.debugElement.query(By.css('#label_' + formModel[22].id));
443+
444+
expect(checkboxLabel).toBeNull();
445+
expect(dsDatePickerLabel).toBeNull();
446+
});
447+
448+
it('should not call handleAriaLabelForLibraryComponents if is SSR', () => {
449+
(component as any).platformId = 'server';
450+
(component as any).componentRef = {
451+
instance: new DynamicNGBootstrapInputComponent(null, null),
452+
location: { nativeElement: document.createElement('div') },
453+
} as any;
454+
fixture.detectChanges();
455+
456+
(component as any).handleAriaLabelForLibraryComponents();
457+
458+
expect(renderer.setAttribute).not.toHaveBeenCalled();
459+
});
460+
461+
it('should set aria-label when valid input and additional property ariaLabel exist and is on browser', () => {
462+
(component as any).platformId = 'browser';
463+
const inputEl = document.createElement('input');
464+
const hostEl = {
465+
querySelector: jasmine.createSpy('querySelector').and.returnValue(inputEl),
466+
};
467+
468+
(component as any).componentRef = {
469+
instance: new DynamicNGBootstrapInputComponent(null, null),
470+
location: { nativeElement: hostEl },
471+
} as any;
472+
(component as any).renderer = renderer;
473+
component.model = { additional: { ariaLabel: 'Accessible Label' } } as any;
474+
fixture.detectChanges();
475+
(component as any).handleAriaLabelForLibraryComponents();
476+
expect(renderer.setAttribute).toHaveBeenCalledWith(inputEl, 'aria-label', 'Accessible Label');
477+
});
478+
437479
});

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

Lines changed: 31 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,
@@ -40,10 +43,12 @@ import {
4043
import {
4144
DYNAMIC_FORM_CONTROL_MAP_FN,
4245
DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX,
46+
DynamicFormArrayComponent,
4347
DynamicFormArrayGroupModel,
4448
DynamicFormArrayModel,
4549
DynamicFormComponentService,
4650
DynamicFormControl,
51+
DynamicFormControlComponent,
4752
DynamicFormControlContainerComponent,
4853
DynamicFormControlEvent,
4954
DynamicFormControlEventType,
@@ -130,6 +135,7 @@ import {
130135
} from './existing-metadata-list-element/existing-metadata-list-element.component';
131136
import { ExistingRelationListElementComponent } from './existing-relation-list-element/existing-relation-list-element.component';
132137
import { DYNAMIC_FORM_CONTROL_TYPE_CUSTOM_SWITCH } from './models/custom-switch/custom-switch.model';
138+
import { DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER } from './models/date-picker/date-picker.model';
133139
import { DsDynamicLookupRelationModalComponent } from './relation-lookup-modal/dynamic-lookup-relation-modal.component';
134140

135141
@Component({
@@ -223,6 +229,8 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
223229
@Inject(APP_CONFIG) protected appConfig: AppConfig,
224230
@Inject(DYNAMIC_FORM_CONTROL_MAP_FN) protected dynamicFormControlFn: DynamicFormControlMapFn,
225231
private actions$: Actions,
232+
protected renderer: Renderer2,
233+
@Inject(PLATFORM_ID) protected platformId: string,
226234
) {
227235
super(ref, componentFactoryResolver, layoutService, validationService, dynamicFormComponentService, relationService);
228236
this.fetchThumbnail = this.appConfig.browseBy.showThumbnails;
@@ -324,6 +332,10 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
324332
return this.model.type === DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX || this.model.type === DYNAMIC_FORM_CONTROL_TYPE_CUSTOM_SWITCH;
325333
}
326334

335+
get isDateField(): boolean {
336+
return this.model.type === DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER;
337+
}
338+
327339
ngOnChanges(changes: SimpleChanges) {
328340
if (changes && !this.isRelationship && hasValue(this.group.get(this.model.id))) {
329341
super.ngOnChanges(changes);
@@ -345,6 +357,7 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
345357

346358
ngAfterViewInit() {
347359
this.showErrorMessagesPreviousStage = this.showErrorMessages;
360+
this.handleAriaLabelForLibraryComponents();
348361
}
349362

350363
protected createFormControlComponent(): void {
@@ -516,4 +529,22 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
516529
this.subs.push(collection$.subscribe((collection) => this.collection = collection));
517530

518531
}
532+
533+
private handleAriaLabelForLibraryComponents(): void {
534+
if (!isPlatformBrowser(this.platformId)) {
535+
return;
536+
}
537+
538+
if ((this.componentRef.instance instanceof DynamicFormControlComponent) &&
539+
!(this.componentRef.instance instanceof DynamicFormArrayComponent) &&
540+
this.componentRef.location.nativeElement) {
541+
const inputEl: HTMLElement | null =
542+
this.componentRef.location.nativeElement.querySelector('input,textarea,select,[role="textbox"]');
543+
544+
545+
if (inputEl && this.model?.additional?.ariaLabel) {
546+
this.renderer.setAttribute(inputEl, 'aria-label', this.model.additional.ariaLabel);
547+
}
548+
}
549+
}
519550
}

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 & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
class="form-control"
4242
[attr.aria-labelledby]="'label_' + model.id"
4343
[attr.autoComplete]="model.autoComplete"
44-
[attr.aria-label]="model.label | translate"
44+
[attr.aria-label]="(model.label || model?.additional?.ariaLabel) | translate"
4545
[class.is-invalid]="showErrorMessages"
4646
[id]="model.id"
4747
[inputFormatter]="formatter"
@@ -71,7 +71,7 @@
7171
<input class="form-control"
7272
[attr.aria-labelledby]="'label_' + model.id"
7373
[attr.autoComplete]="model.autoComplete"
74-
[attr.aria-label]="model.label | translate"
74+
[attr.aria-label]="(model.label || model?.additional?.ariaLabel) | translate"
7575
[class.is-invalid]="showErrorMessages"
7676
[class.tree-input]="!model.readOnly"
7777
[id]="id"

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
@@ -92,18 +92,18 @@ export class ConcatFieldParser extends FieldParser {
9292
concatGroup.disabled = input1ModelConfig.readOnly;
9393

9494
if (isNotEmpty(this.firstPlaceholder)) {
95-
input1ModelConfig.placeholder = this.firstPlaceholder;
95+
input1ModelConfig.label = this.firstPlaceholder;
9696
}
9797

9898
if (isNotEmpty(this.secondPlaceholder)) {
99-
input2ModelConfig.placeholder = this.secondPlaceholder;
99+
input2ModelConfig.label = this.secondPlaceholder;
100100
}
101101

102102
// Split placeholder if is like 'placeholder1/placeholder2'
103103
const placeholder = this.configData.label.split('/');
104104
if (placeholder.length === 2) {
105-
input1ModelConfig.placeholder = placeholder[0];
106-
input2ModelConfig.placeholder = placeholder[1];
105+
input1ModelConfig.label = placeholder[0];
106+
input2ModelConfig.label = placeholder[1];
107107
}
108108

109109
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
@@ -67,4 +67,11 @@ describe('DateFieldParser test suite', () => {
6767

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

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

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

1212
public modelFactory(fieldValue?: FormFieldMetadataValueObject, label?: boolean): any {
1313
let malformedDate = false;
14-
const inputDateModelConfig: DynamicDsDateControlModelConfig = this.initModel(null, false, true);
15-
inputDateModelConfig.legend = this.configData.label;
14+
const inputDateModelConfig: DynamicDsDateControlModelConfig = this.initModel(null, label, true);
15+
inputDateModelConfig.legend = this.configData.repeatable ? null : this.configData.label;
1616
inputDateModelConfig.disabled = inputDateModelConfig.readOnly;
1717
inputDateModelConfig.toggleIcon = 'fas fa-calendar';
1818
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
@@ -306,7 +306,8 @@ export abstract class FieldParser {
306306
if (hint) {
307307
controlModel.hint = this.configData.hints || '&nbsp;';
308308
}
309-
controlModel.placeholder = this.configData.label;
309+
310+
controlModel.additional = { ...controlModel.additional, ariaLabel: this.configData.label };
310311

311312
if (this.configData.mandatory && setErrors) {
312313
this.markAsRequired(controlModel);

0 commit comments

Comments
 (0)