Skip to content

Commit 464758f

Browse files
author
Andrea Barbasso
committed
[CST-16756] fix submission form's "serious" accessibility issues
1 parent 1fa00ac commit 464758f

18 files changed

+180
-66
lines changed

src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.spec.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import { DragService } from '../../core/drag.service';
2727
import { CookieService } from '../../core/services/cookie.service';
2828
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
2929
import { HostWindowService } from '../../shared/host-window.service';
30+
import { LiveRegionService } from '../../shared/live-region/live-region.service';
31+
import { getLiveRegionServiceStub } from '../../shared/live-region/live-region.service.stub';
3032
import { CookieServiceMock } from '../../shared/mocks/cookie.service.mock';
3133
import { HttpXsrfTokenExtractorMock } from '../../shared/mocks/http-xsrf-token-extractor.mock';
3234
import { getMockScrollToService } from '../../shared/mocks/scroll-to-service.mock';
@@ -76,6 +78,7 @@ describe('MyDSpaceNewSubmissionComponent test', () => {
7678
{ provide: CookieService, useValue: new CookieServiceMock() },
7779
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
7880
{ provide: EntityTypeDataService, useValue: getMockEntityTypeService() },
81+
{ provide: LiveRegionService, useValue: getLiveRegionServiceStub() },
7982
],
8083
schemas: [NO_ERRORS_SCHEMA],
8184
}).compileComponents();

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,12 @@
3232
<div [id]="id + '_errors'"
3333
[ngClass]="[getClass('element', 'errors'), getClass('grid', 'errors')]">
3434
@for (message of errorMessages; track message) {
35-
<small class="invalid-feedback d-block">{{ message | translate: model.validators }}</small>
35+
<small class="invalid-feedback d-block"
36+
aria-required="true"
37+
aria-invalid="true"
38+
[attr.aria-describedby]="'label_' + model.id"
39+
aria-live="assertive"
40+
>{{ message | translate: model.validators }}</small>
3641
}
3742
</div>
3843
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ import { WorkspaceItem } from '../../../../core/submission/models/workspaceitem.
6868
import { SubmissionObjectDataService } from '../../../../core/submission/submission-object-data.service';
6969
import { VocabularyOptions } from '../../../../core/submission/vocabularies/models/vocabulary-options.model';
7070
import { SubmissionService } from '../../../../submission/submission.service';
71+
import { LiveRegionService } from '../../../live-region/live-region.service';
72+
import { getLiveRegionServiceStub } from '../../../live-region/live-region.service.stub';
7173
import { SelectableListService } from '../../../object-list/selectable-list/selectable-list.service';
7274
import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils';
7375
import { FormBuilderService } from '../form-builder.service';
@@ -240,6 +242,7 @@ describe('DsDynamicFormControlContainerComponent test suite', () => {
240242
{ provide: APP_CONFIG, useValue: environment },
241243
{ provide: APP_DATA_SERVICES_MAP, useValue: {} },
242244
{ provide: DYNAMIC_FORM_CONTROL_MAP_FN, useValue: dsDynamicFormControlMapFn },
245+
{ provide: LiveRegionService, useValue: getLiveRegionServiceStub() },
243246
],
244247
schemas: [CUSTOM_ELEMENTS_SCHEMA],
245248
}).compileComponents().then(() => {

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
DoCheck,
1414
EventEmitter,
1515
Inject,
16+
inject,
1617
Input,
1718
OnChanges,
1819
OnDestroy,
@@ -25,6 +26,7 @@ import {
2526
ViewContainerRef,
2627
} from '@angular/core';
2728
import {
29+
AbstractControl,
2830
FormsModule,
2931
ReactiveFormsModule,
3032
UntypedFormArray,
@@ -107,6 +109,7 @@ import {
107109
isNotEmpty,
108110
isNotUndefined,
109111
} from '../../../empty.util';
112+
import { LiveRegionService } from '../../../live-region/live-region.service';
110113
import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model';
111114
import { SelectableListState } from '../../../object-list/selectable-list/selectable-list.reducer';
112115
import { SelectableListService } from '../../../object-list/selectable-list/selectable-list.service';
@@ -194,6 +197,8 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
194197
return this.dynamicFormControlFn(this.model);
195198
}
196199

200+
private readonly liveRegionService = inject(LiveRegionService);
201+
197202
constructor(
198203
protected componentFactoryResolver: ComponentFactoryResolver,
199204
protected dynamicFormComponentService: DynamicFormComponentService,
@@ -349,6 +354,31 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
349354
if (this.showErrorMessages) {
350355
this.destroyFormControlComponent();
351356
this.createFormControlComponent();
357+
this.announceErrorMessages();
358+
}
359+
}
360+
361+
/**
362+
* Announce error messages to the user
363+
*/
364+
announceErrorMessages() {
365+
const numberOfInvalidInputs = this.getNumberOfInvalidInputs() ?? 1;
366+
setTimeout(() => {
367+
this.errorMessages.forEach((errorMsg) => {
368+
// set timer based on the number of the invalid inputs
369+
this.liveRegionService.setMessageTimeOutMs(numberOfInvalidInputs * 3500);
370+
const message = this.translateService.instant(errorMsg);
371+
this.liveRegionService.addMessage(message);
372+
});
373+
}, 14000);// wait for the general deposit alert to be announced
374+
}
375+
376+
/**
377+
* Get the number of invalid inputs in the formGroup
378+
*/
379+
private getNumberOfInvalidInputs(): number {
380+
if (this.formGroup && this.formGroup.controls) {
381+
return Object.values(this.formGroup.controls).filter((control: AbstractControl) => control.invalid).length;
352382
}
353383
}
354384

src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.scss

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
}
3030

3131
.drag-icon {
32-
visibility: hidden;
3332
width: calc(2 * var(--bs-spacer));
3433
color: var(--bs-gray-600);
3534
margin: var(--bs-btn-padding-y) 0;

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

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
<div>
2-
<fieldset class="d-flex">
2+
<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}} @if (model.required) {
6-
<span>*</span>
7-
}
8-
</legend>
9-
}
5+
{{ model.placeholder }}
6+
@if (model.required) {
7+
<span>*</span>
8+
}
9+
</legend>
10+
}
1011
<ds-number-picker
11-
tabindex="0"
12+
tabindex="1"
1213
[id]="model.id + '_year'"
1314
[disabled]="model.disabled"
1415
[min]="minYear"
@@ -19,29 +20,31 @@
1920
[value]="year"
2021
[invalid]="showErrorMessages"
2122
[placeholder]="'form.date-picker.placeholder.year' | translate"
23+
[widthClass]="'four-digits'"
2224
(blur)="onBlur($event)"
2325
(change)="onChange($event)"
2426
(focus)="onFocus($event)"
2527
></ds-number-picker>
2628

27-
<ds-number-picker
28-
tabindex="0"
29+
<ds-number-picker class="date-month"
30+
tabindex="2"
2931
[id]="model.id + '_month'"
3032
[min]="minMonth"
3133
[max]="maxMonth"
3234
[name]="'month'"
33-
[size]="6"
35+
[size]="2"
3436
[(ngModel)]="initialMonth"
3537
[value]="month"
3638
[placeholder]="'form.date-picker.placeholder.month' | translate"
3739
[disabled]="!year || model.disabled"
40+
[widthClass]="'two-digits'"
3841
(blur)="onBlur($event)"
3942
(change)="onChange($event)"
4043
(focus)="onFocus($event)"
4144
></ds-number-picker>
4245

43-
<ds-number-picker
44-
tabindex="0"
46+
<ds-number-picker class="date-day"
47+
tabindex="3"
4548
[id]="model.id + '_day'"
4649
[min]="minDay"
4750
[max]="maxDay"
@@ -51,6 +54,7 @@
5154
[value]="day"
5255
[placeholder]="'form.date-picker.placeholder.day' | translate"
5356
[disabled]="!month || model.disabled"
57+
[widthClass]="'two-digits'"
5458
(blur)="onBlur($event)"
5559
(change)="onChange($event)"
5660
(focus)="onFocus($event)"

src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.component.spec.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ import { SubmissionObjectDataService } from '../../../../../../core/submission/s
4444
import { VocabularyService } from '../../../../../../core/submission/vocabularies/vocabulary.service';
4545
import { XSRFService } from '../../../../../../core/xsrf/xsrf.service';
4646
import { SubmissionService } from '../../../../../../submission/submission.service';
47+
import { LiveRegionService } from '../../../../../live-region/live-region.service';
48+
import { getLiveRegionServiceStub } from '../../../../../live-region/live-region.service.stub';
4749
import { createTestComponent } from '../../../../../testing/utils.test';
4850
import { VocabularyServiceStub } from '../../../../../testing/vocabulary-service.stub';
4951
import { Chips } from '../../../../chips/models/chips.model';
@@ -184,6 +186,7 @@ describe('DsDynamicRelationGroupComponent test suite', () => {
184186
{ provide: APP_CONFIG, useValue: environment },
185187
{ provide: APP_DATA_SERVICES_MAP, useValue: {} },
186188
{ provide: DYNAMIC_FORM_CONTROL_MAP_FN, useValue: dsDynamicFormControlMapFn },
189+
{ provide: LiveRegionService, useValue: getLiveRegionServiceStub() },
187190
],
188191
schemas: [CUSTOM_ELEMENTS_SCHEMA],
189192
})

src/app/shared/form/form.component.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
<div
1818
class="col-2 d-flex flex-column justify-content-sm-start align-items-end">
1919
<button type="button" class="btn btn-secondary" role="button"
20-
title="{{'form.remove' | translate}}"
21-
[attr.aria-label]="'form.remove' | translate"
20+
title="{{('form.remove' | translate) + ' ' + (context.label ?? context.name) + ' ' + index}}"
21+
[attr.aria-label]="('form.remove' | translate) + ' ' + (context.label ?? context.name) + ' ' + index"
2222
(click)="removeItem($event, context, index)">
2323
<span><i class="fas fa-trash" aria-hidden="true"></i></span>
2424
</button>
@@ -28,8 +28,8 @@
2828
<div class="clearfix ps-4 w-100">
2929
<div class="btn-group" role="group">
3030
<button type="button" role="button" class="ds-form-add-more btn btn-link"
31-
title="{{'form.add' | translate}}"
32-
attr.aria-label="{{'form.add' | translate}}"
31+
title="{{('form.add' | translate) + ' ' + (context.label ?? context.name)}}"
32+
[attr.aria-label]="('form.add' | translate ) + ' ' + (context.label ?? context.name)"
3333
(click)="insertItem($event, group.context, group.context.groups.length)">
3434
<span><i class="fas fa-plus"></i> {{'form.add' | translate}}</span>
3535
</button>
Lines changed: 40 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,46 @@
1-
<div class="d-flex flex-column align-items-center justify-content-around me-3">
2-
<button
3-
class="btn btn-link-focus"
4-
type="button"
5-
tabindex="0"
6-
[dsBtnDisabled]="disabled"
7-
(click)="toggleUp()">
1+
<div class="d-flex flex-column align-items-center">
2+
<label class="ml-n3" [for]="id">{{'form.number-picker.label.' + placeholder | translate }}</label>
3+
<div class="d-flex flex-column align-items-stretch justify-content-around position-relative">
4+
<button id="increment-{{id}}"
5+
class="btn btn-link-focus btn-date ml-2"
6+
type="button"
7+
tabindex="0"
8+
[dsBtnDisabled]="disabled"
9+
(click)="toggleUp()"
10+
title="{{'form.number-picker.increment' | translate: {field: placeholder} }}"
11+
[attr.aria-label]="'form.number-picker.increment' | translate: {field: placeholder}">
812
<span class="chevron"></span>
913
<span class="sr-only">{{'form.number-picker.increment' | translate: {field: name} }}</span>
1014
</button>
1115
<input
12-
id="{{id}}"
13-
type="text"
14-
class="form-control d-inline-block text-center"
15-
maxlength="{{size}}"
16-
size="{{size}}"
17-
placeholder="{{placeholder}}"
18-
[name]="name"
19-
[(ngModel)]="value"
20-
(blur)="onBlur($event); $event.stopPropagation();"
21-
(change)="update($event); $event.stopPropagation();"
22-
(focus)="onFocus($event); $event.stopPropagation();"
23-
[readonly]="disabled"
24-
[disabled]="disabled"
25-
[ngClass]="{'is-invalid': invalid}"
26-
title="{{placeholder}}"
27-
[attr.aria-label]="placeholder"
16+
id="{{id}}"
17+
type="text"
18+
class="form-control d-inline-block text-center {{ widthClass }}"
19+
maxlength="{{size}}"
20+
size="{{size}}"
21+
[name]="name"
22+
[(ngModel)]="value"
23+
(blur)="onBlur($event); $event.stopPropagation();"
24+
(change)="update($event); $event.stopPropagation();"
25+
(focus)="onFocus($event); $event.stopPropagation();"
26+
[readonly]="disabled"
27+
[disabled]="disabled"
28+
[ngClass]="{'is-invalid': invalid}"
29+
placeholder="{{'form.number-picker.label.' + placeholder | translate }}"
30+
title="{{'form.number-picker.label.' + placeholder | translate}}"
31+
[attr.aria-labelledby]="id + ' increment-' + id + ' decrement-' + id"
2832
>
29-
<button
30-
class="btn btn-link-focus"
31-
type="button"
32-
tabindex="0"
33-
[dsBtnDisabled]="disabled"
34-
(click)="toggleDown()">
35-
<span class="chevron bottom"></span>
36-
<span class="sr-only">{{'form.number-picker.decrement' | translate: {field: name} }}</span>
37-
</button>
33+
34+
<button id="decrement-{{id}}"
35+
class="btn btn-link-focus btn-date ml-2"
36+
type="button"
37+
tabindex="0"
38+
[dsBtnDisabled]="disabled"
39+
(click)="toggleDown()"
40+
title="{{'form.number-picker.decrement' | translate: {field: placeholder} }}"
41+
[attr.aria-label]="'form.number-picker.decrement' | translate: {field: placeholder}">
42+
<span class="chevron bottom"></span>
43+
<span class="sr-only">{{'form.number-picker.decrement' | translate: {field: name} }}</span>
44+
</button>
45+
</div>
3846
</div>

src/app/shared/form/number-picker/number-picker.component.scss

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,32 @@
44

55
.chevron::before {
66
border-style: solid;
7-
border-width: 0.29em 0.29em 0 0;
7+
border-width: 0.19em 0.19em 0 0;
88
content: '';
99
display: inline-block;
1010
height: 0.69em;
11-
left: 0.05em;
1211
position: relative;
13-
top: 0.15em;
12+
top: -0.15rem;
1413
transform: rotate(-45deg);
1514
vertical-align: middle;
1615
width: 0.71em;
1716
}
1817
.chevron.bottom:before {
19-
top: -.3em;
18+
top: -.45em;
2019
transform: rotate(135deg);
2120
}
2221

23-
input {
24-
max-width: 80px !important;
22+
.btn-date {
23+
max-height: 1.1rem;
24+
padding: 0;
25+
}
26+
27+
.four-digits {
28+
width: 90px;
29+
}
30+
31+
.two-digits {
32+
width: 80px;
2533
}
2634

2735
.btn-link-focus {

0 commit comments

Comments
 (0)