Skip to content

Commit 12a5276

Browse files
authored
Merge pull request #5445 from 4Science/task/dspace-8_x/DURACOM-347
[Port dspace-8_x] Fix accessibility issues with placeholder text in submission forms by removing placeholders
2 parents ddb3a27 + f77391b commit 12a5276

11 files changed

Lines changed: 102 additions & 23 deletions

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
[class.d-none]="model.hidden"
33
[formGroup]="group"
44
[ngClass]="[getClass('element', 'container'), getClass('grid', 'container')]">
5-
<label *ngIf="!isCheckbox && hasLabel"
5+
<label *ngIf="!isCheckbox && hasLabel && !isDateField"
66
[id]="'label_' + model.id"
77
[for]="id"
88
[innerHTML]="(model.required && model.label) ? (model.label | translate) + ' *' : (model.label | translate)"

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

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,8 +220,10 @@ describe('DsDynamicFormControlContainerComponent test suite', () => {
220220
let testElement: DebugElement;
221221
const testItem: Item = new Item();
222222
const testWSI: WorkspaceItem = new WorkspaceItem();
223-
testWSI.item = observableOf(createSuccessfulRemoteDataObject(testItem));
224223
const actions$: ReplaySubject<any> = new ReplaySubject<any>(1);
224+
testWSI.item = observableOf(createSuccessfulRemoteDataObject(testItem));
225+
const renderer = jasmine.createSpyObj('Renderer2', ['setAttribute']);
226+
225227
beforeEach(waitForAsync(() => {
226228

227229
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: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
AsyncPipe,
3+
isPlatformBrowser,
34
NgClass,
45
NgForOf,
56
NgIf,
@@ -21,7 +22,9 @@ import {
2122
OnDestroy,
2223
OnInit,
2324
Output,
25+
PLATFORM_ID,
2426
QueryList,
27+
Renderer2,
2528
SimpleChanges,
2629
Type,
2730
ViewChild,
@@ -42,10 +45,12 @@ import {
4245
import {
4346
DYNAMIC_FORM_CONTROL_MAP_FN,
4447
DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX,
48+
DynamicFormArrayComponent,
4549
DynamicFormArrayGroupModel,
4650
DynamicFormArrayModel,
4751
DynamicFormComponentService,
4852
DynamicFormControl,
53+
DynamicFormControlComponent,
4954
DynamicFormControlContainerComponent,
5055
DynamicFormControlEvent,
5156
DynamicFormControlEventType,
@@ -108,7 +113,6 @@ import { SubmissionObjectDataService } from '../../../../core/submission/submiss
108113
import { paginatedRelationsToItems } from '../../../../item-page/simple/item-types/shared/item-relationships-utils';
109114
import { SubmissionObjectActionTypes } from '../../../../submission/objects/submission-objects.actions';
110115
import { SubmissionService } from '../../../../submission/submission.service';
111-
import { BtnDisabledDirective } from '../../../btn-disabled.directive';
112116
import {
113117
hasNoValue,
114118
hasValue,
@@ -132,6 +136,7 @@ import {
132136
} from './existing-metadata-list-element/existing-metadata-list-element.component';
133137
import { ExistingRelationListElementComponent } from './existing-relation-list-element/existing-relation-list-element.component';
134138
import { DYNAMIC_FORM_CONTROL_TYPE_CUSTOM_SWITCH } from './models/custom-switch/custom-switch.model';
139+
import { DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER } from './models/date-picker/date-picker.model';
135140
import { DsDynamicLookupRelationModalComponent } from './relation-lookup-modal/dynamic-lookup-relation-modal.component';
136141

137142
@Component({
@@ -151,7 +156,6 @@ import { DsDynamicLookupRelationModalComponent } from './relation-lookup-modal/d
151156
NgbTooltipModule,
152157
NgTemplateOutlet,
153158
ExistingRelationListElementComponent,
154-
BtnDisabledDirective,
155159
],
156160
standalone: true,
157161
})
@@ -229,6 +233,8 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
229233
@Inject(APP_CONFIG) protected appConfig: AppConfig,
230234
@Inject(DYNAMIC_FORM_CONTROL_MAP_FN) protected dynamicFormControlFn: DynamicFormControlMapFn,
231235
private actions$: Actions,
236+
protected renderer: Renderer2,
237+
@Inject(PLATFORM_ID) protected platformId: string,
232238
) {
233239
super(ref, componentFactoryResolver, layoutService, validationService, dynamicFormComponentService, relationService);
234240
this.fetchThumbnail = this.appConfig.browseBy.showThumbnails;
@@ -330,6 +336,11 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
330336
return this.model.type === DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX || this.model.type === DYNAMIC_FORM_CONTROL_TYPE_CUSTOM_SWITCH;
331337
}
332338

339+
340+
get isDateField(): boolean {
341+
return this.model.type === DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER;
342+
}
343+
333344
ngOnChanges(changes: SimpleChanges) {
334345
if (changes && !this.isRelationship && hasValue(this.group.get(this.model.id))) {
335346
super.ngOnChanges(changes);
@@ -351,6 +362,7 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
351362

352363
ngAfterViewInit() {
353364
this.showErrorMessagesPreviousStage = this.showErrorMessages;
365+
this.handleAriaLabelForLibraryComponents();
354366
}
355367

356368
protected createFormControlComponent(): void {
@@ -522,4 +534,22 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
522534
this.subs.push(collection$.subscribe((collection) => this.collection = collection));
523535

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

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
<div>
22
<fieldset class="d-flex justify-content-start flex-wrap gap-2">
33
<legend *ngIf="!model.repeatable" [id]="'legend_' + model.id" [ngClass]="[getClass('element', 'label'), getClass('grid', 'label')]">
4-
{{model.placeholder}} <span *ngIf="model.required">*</span>
5-
</legend>
4+
{{model.label}} <span *ngIf="model.required">*</span>
5+
</legend>
66
<ds-number-picker
77
tabindex="0"
88
[id]="model.id + '_year'"
@@ -15,7 +15,7 @@
1515
[value]="year"
1616
[invalid]="showErrorMessages"
1717
[placeholder]="'form.date-picker.placeholder.year' | translate"
18-
[widthClass]="'four-digits'"
18+
[widthClass]="'four-digits'"
1919
(blur)="onBlur($event)"
2020
(change)="onChange($event)"
2121
(focus)="onFocus($event)"
@@ -27,12 +27,12 @@
2727
[min]="minMonth"
2828
[max]="maxMonth"
2929
[name]="'month'"
30-
[size]="2"
30+
[size]="2"
3131
[(ngModel)]="initialMonth"
3232
[value]="month"
3333
[placeholder]="'form.date-picker.placeholder.month' | translate"
3434
[disabled]="!year || model.disabled"
35-
[widthClass]="'two-digits'"
35+
[widthClass]="'two-digits'"
3636
(blur)="onBlur($event)"
3737
(change)="onChange($event)"
3838
(focus)="onFocus($event)"
@@ -49,7 +49,7 @@
4949
[value]="day"
5050
[placeholder]="'form.date-picker.placeholder.day' | translate"
5151
[disabled]="!month || model.disabled"
52-
[widthClass]="'two-digits'"
52+
[widthClass]="'two-digits'"
5353
(blur)="onBlur($event)"
5454
(change)="onChange($event)"
5555
(focus)="onFocus($event)"

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
@@ -32,7 +32,7 @@
3232
class="form-control"
3333
[attr.aria-labelledby]="'label_' + model.id"
3434
[attr.autoComplete]="model.autoComplete"
35-
[attr.aria-label]="model.label | translate"
35+
[attr.aria-label]="(model.label || model?.additional?.ariaLabel) | translate"
3636
[class.is-invalid]="showErrorMessages"
3737
[id]="model.id"
3838
[inputFormatter]="formatter"
@@ -59,7 +59,7 @@
5959
<input class="form-control"
6060
[attr.aria-labelledby]="'label_' + model.id"
6161
[attr.autoComplete]="model.autoComplete"
62-
[attr.aria-label]="model.label | translate"
62+
[attr.aria-label]="(model.label || model?.additional?.ariaLabel) | translate"
6363
[class.is-invalid]="showErrorMessages"
6464
[class.tree-input]="!model.readOnly"
6565
[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
@@ -11,7 +11,7 @@
1111
aria-hidden="true"></i>
1212
<input class="form-control"
1313
[attr.aria-controls]="'combobox_' + id + '_listbox'"
14-
[attr.aria-label]="model.placeholder"
14+
[attr.aria-label]="model.label"
1515
[attr.autoComplete]="model.autoComplete"
1616
[class.is-invalid]="showErrorMessages"
1717
[class.scrollable-dropdown-input]="!model.readOnly"
@@ -29,11 +29,11 @@
2929

3030
<div #dropdownMenu ngbDropdownMenu
3131
class="dropdown-menu scrollable-dropdown-menu w-100"
32-
[attr.aria-label]="model.placeholder">
32+
[attr.aria-label]="model.label">
3333
<div class="scrollable-menu"
3434
role="listbox"
3535
[id]="'combobox_' + id + '_listbox'"
36-
[attr.aria-label]="model.placeholder"
36+
[attr.aria-label]="model.label"
3737
infiniteScroll
3838
[infiniteScrollDistance]="2"
3939
[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)