Skip to content

Commit df3fb98

Browse files
author
Andrea Barbasso
committed
[CST-24622] fix submission form's "serious" accessibility issues
(cherry picked from commit 85f1dbc)
1 parent 09c00d1 commit df3fb98

25 files changed

+310
-74
lines changed

src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@
102102
cdkDragHandle [cdkDragHandleDisabled]="disabled" [ngClass]="{'disabled': disabled}" [dsBtnDisabled]="disabled"
103103
[title]="dsoType + '.edit.metadata.edit.buttons.drag' | translate"
104104
ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.drag' | translate }}">
105-
<i class="fas fa-grip-vertical fa-fw"></i>
105+
<i class="drag-icon"></i>
106106
</button>
107107
</div>
108108
</div>

src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,7 @@
8484
scope="row" id="{{ entry.nameStripped }}" headers="{{ bundleName }} name">
8585
<div class="drag-handle text-muted float-left p-1 mr-2" tabindex="0" cdkDragHandle
8686
(keydown.enter)="select($event, entry)" (keydown.space)="select($event, entry)" (click)="select($event, entry)">
87-
<i class="fas fa-grip-vertical fa-fw"
88-
[title]="'item.edit.bitstreams.edit.buttons.drag' | translate"></i>
87+
<i class="drag-icon" [title]="'item.edit.bitstreams.edit.buttons.drag' | translate"></i>
8988
</div>
9089
{{ entry.name }}
9190
</th>

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
@@ -23,7 +23,12 @@
2323

2424
<div *ngIf="!model.hideErrorMessages && showErrorMessages" [id]="id + '_errors'"
2525
[ngClass]="[getClass('element', 'errors'), getClass('grid', 'errors')]">
26-
<small *ngFor="let message of errorMessages" class="invalid-feedback d-block">{{ message | translate: model.validators }}</small>
26+
<small *ngFor="let message of errorMessages" class="invalid-feedback d-block"
27+
aria-required="true"
28+
aria-invalid="true"
29+
[attr.aria-describedby]="'label_' + model.id"
30+
aria-live="assertive"
31+
>{{ message | translate: model.validators }}</small>
2732
</div>
2833

2934
</div>

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

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,14 @@ import {
5252
DynamicNGBootstrapTextAreaComponent,
5353
DynamicNGBootstrapTimePickerComponent,
5454
} from '@ng-dynamic-forms/ui-ng-bootstrap';
55+
import { Actions } from '@ngrx/effects';
5556
import { Store } from '@ngrx/store';
5657
import { TranslateModule } from '@ngx-translate/core';
5758
import { NgxMaskModule } from 'ngx-mask';
58-
import { of as observableOf } from 'rxjs';
59+
import {
60+
of as observableOf,
61+
ReplaySubject,
62+
} from 'rxjs';
5963

6064
import {
6165
APP_CONFIG,
@@ -67,7 +71,16 @@ import { Item } from '../../../../core/shared/item.model';
6771
import { WorkspaceItem } from '../../../../core/submission/models/workspaceitem.model';
6872
import { SubmissionObjectDataService } from '../../../../core/submission/submission-object-data.service';
6973
import { VocabularyOptions } from '../../../../core/submission/vocabularies/models/vocabulary-options.model';
74+
import {
75+
SaveForLaterSubmissionFormErrorAction,
76+
SaveSubmissionFormErrorAction,
77+
SaveSubmissionFormSuccessAction,
78+
SaveSubmissionSectionFormErrorAction,
79+
SaveSubmissionSectionFormSuccessAction,
80+
} from '../../../../submission/objects/submission-objects.actions';
7081
import { SubmissionService } from '../../../../submission/submission.service';
82+
import { LiveRegionService } from '../../../live-region/live-region.service';
83+
import { getLiveRegionServiceStub } from '../../../live-region/live-region.service.stub';
7184
import { SelectableListService } from '../../../object-list/selectable-list/selectable-list.service';
7285
import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils';
7386
import { FormBuilderService } from '../form-builder.service';
@@ -208,6 +221,7 @@ describe('DsDynamicFormControlContainerComponent test suite', () => {
208221
const testItem: Item = new Item();
209222
const testWSI: WorkspaceItem = new WorkspaceItem();
210223
testWSI.item = observableOf(createSuccessfulRemoteDataObject(testItem));
224+
const actions$: ReplaySubject<any> = new ReplaySubject<any>(1);
211225
beforeEach(waitForAsync(() => {
212226

213227
TestBed.configureTestingModule({
@@ -240,6 +254,8 @@ describe('DsDynamicFormControlContainerComponent test suite', () => {
240254
{ provide: APP_CONFIG, useValue: environment },
241255
{ provide: APP_DATA_SERVICES_MAP, useValue: {} },
242256
{ provide: DYNAMIC_FORM_CONTROL_MAP_FN, useValue: dsDynamicFormControlMapFn },
257+
{ provide: LiveRegionService, useValue: getLiveRegionServiceStub() },
258+
{ provide: Actions, useValue: actions$ },
243259
],
244260
schemas: [CUSTOM_ELEMENTS_SCHEMA],
245261
}).compileComponents().then(() => {
@@ -382,4 +398,40 @@ describe('DsDynamicFormControlContainerComponent test suite', () => {
382398
expect(testFn(formModel[25])).toEqual(DsDynamicFormGroupComponent);
383399
});
384400

401+
describe('store action subscriptions', () => {
402+
beforeEach(() => {
403+
fixture.detectChanges();
404+
});
405+
406+
it('should call announceErrorMessages on SAVE_SUBMISSION_FORM_SUCCESS', () => {
407+
spyOn(component, 'announceErrorMessages');
408+
actions$.next(new SaveSubmissionFormSuccessAction('1234', [] as any));
409+
expect(component.announceErrorMessages).toHaveBeenCalled();
410+
});
411+
412+
it('should call announceErrorMessages on SAVE_SUBMISSION_SECTION_FORM_SUCCESS', () => {
413+
spyOn(component, 'announceErrorMessages');
414+
actions$.next(new SaveSubmissionSectionFormSuccessAction('1234', [] as any));
415+
expect(component.announceErrorMessages).toHaveBeenCalled();
416+
});
417+
418+
it('should call announceErrorMessages on SAVE_SUBMISSION_FORM_ERROR', () => {
419+
spyOn(component, 'announceErrorMessages');
420+
actions$.next(new SaveSubmissionFormErrorAction('1234'));
421+
expect(component.announceErrorMessages).toHaveBeenCalled();
422+
});
423+
424+
it('should call announceErrorMessages on SAVE_FOR_LATER_SUBMISSION_FORM_ERROR', () => {
425+
spyOn(component, 'announceErrorMessages');
426+
actions$.next(new SaveForLaterSubmissionFormErrorAction('1234'));
427+
expect(component.announceErrorMessages).toHaveBeenCalled();
428+
});
429+
430+
it('should call announceErrorMessages on SAVE_SUBMISSION_SECTION_FORM_ERROR', () => {
431+
spyOn(component, 'announceErrorMessages');
432+
actions$.next(new SaveSubmissionSectionFormErrorAction('1234'));
433+
expect(component.announceErrorMessages).toHaveBeenCalled();
434+
});
435+
});
436+
385437
});

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

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
DoCheck,
1616
EventEmitter,
1717
Inject,
18+
inject,
1819
Input,
1920
OnChanges,
2021
OnDestroy,
@@ -27,6 +28,7 @@ import {
2728
ViewContainerRef,
2829
} from '@angular/core';
2930
import {
31+
AbstractControl,
3032
FormsModule,
3133
ReactiveFormsModule,
3234
UntypedFormArray,
@@ -55,6 +57,10 @@ import {
5557
DynamicTemplateDirective,
5658
} from '@ng-dynamic-forms/core';
5759
import { DynamicFormControlMapFn } from '@ng-dynamic-forms/core/lib/service/dynamic-form-component.service';
60+
import {
61+
Actions,
62+
ofType,
63+
} from '@ngrx/effects';
5864
import { Store } from '@ngrx/store';
5965
import {
6066
TranslateModule,
@@ -100,6 +106,7 @@ import {
100106
import { SubmissionObject } from '../../../../core/submission/models/submission-object.model';
101107
import { SubmissionObjectDataService } from '../../../../core/submission/submission-object-data.service';
102108
import { paginatedRelationsToItems } from '../../../../item-page/simple/item-types/shared/item-relationships-utils';
109+
import { SubmissionObjectActionTypes } from '../../../../submission/objects/submission-objects.actions';
103110
import { SubmissionService } from '../../../../submission/submission.service';
104111
import { BtnDisabledDirective } from '../../../btn-disabled.directive';
105112
import {
@@ -108,6 +115,7 @@ import {
108115
isNotEmpty,
109116
isNotUndefined,
110117
} from '../../../empty.util';
118+
import { LiveRegionService } from '../../../live-region/live-region.service';
111119
import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model';
112120
import { SelectableListState } from '../../../object-list/selectable-list/selectable-list.reducer';
113121
import { SelectableListService } from '../../../object-list/selectable-list/selectable-list.service';
@@ -178,6 +186,8 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
178186
*/
179187
private subs: Subscription[] = [];
180188

189+
private liveRegionErrorMessagesShownAlready = false;
190+
181191
/* eslint-disable @angular-eslint/no-output-rename */
182192
@Output('dfBlur') blur: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
183193
@Output('dfChange') change: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
@@ -197,6 +207,8 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
197207
return this.dynamicFormControlFn(this.model);
198208
}
199209

210+
private readonly liveRegionService = inject(LiveRegionService);
211+
200212
constructor(
201213
protected componentFactoryResolver: ComponentFactoryResolver,
202214
protected dynamicFormComponentService: DynamicFormComponentService,
@@ -216,6 +228,7 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
216228
protected metadataService: MetadataService,
217229
@Inject(APP_CONFIG) protected appConfig: AppConfig,
218230
@Inject(DYNAMIC_FORM_CONTROL_MAP_FN) protected dynamicFormControlFn: DynamicFormControlMapFn,
231+
private actions$: Actions,
219232
) {
220233
super(ref, componentFactoryResolver, layoutService, validationService, dynamicFormComponentService, relationService);
221234
this.fetchThumbnail = this.appConfig.browseBy.showThumbnails;
@@ -228,6 +241,18 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
228241
this.isRelationship = hasValue(this.model.relationship);
229242
const isWrapperAroundRelationshipList = hasValue(this.model.relationshipConfig);
230243

244+
// Subscribe to specified submission actions to announce error messages
245+
const errorAnnounceActionsSub = this.actions$.pipe(
246+
ofType(
247+
SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM_SUCCESS,
248+
SubmissionObjectActionTypes.SAVE_SUBMISSION_SECTION_FORM_SUCCESS,
249+
SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM_ERROR,
250+
SubmissionObjectActionTypes.SAVE_FOR_LATER_SUBMISSION_FORM_ERROR,
251+
SubmissionObjectActionTypes.SAVE_SUBMISSION_SECTION_FORM_ERROR,
252+
),
253+
).subscribe(() => this.announceErrorMessages());
254+
this.subs.push(errorAnnounceActionsSub);
255+
231256
if (this.isRelationship || isWrapperAroundRelationshipList) {
232257
const config = this.model.relationshipConfig || this.model.relationship;
233258
const relationshipOptions = Object.assign(new RelationshipOptions(), config);
@@ -352,6 +377,36 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
352377
if (this.showErrorMessages) {
353378
this.destroyFormControlComponent();
354379
this.createFormControlComponent();
380+
this.announceErrorMessages();
381+
}
382+
}
383+
384+
/**
385+
* Announce error messages to the user
386+
*/
387+
announceErrorMessages() {
388+
if (!this.liveRegionErrorMessagesShownAlready) {
389+
this.liveRegionErrorMessagesShownAlready = true;
390+
const numberOfInvalidInputs = this.getNumberOfInvalidInputs() ?? 1;
391+
const timeoutMs = numberOfInvalidInputs * 3500;
392+
this.errorMessages.forEach((errorMsg) => {
393+
// set timer based on the number of the invalid inputs
394+
this.liveRegionService.setMessageTimeOutMs(timeoutMs);
395+
const message = this.translateService.instant(errorMsg);
396+
this.liveRegionService.addMessage(message);
397+
});
398+
setTimeout(() => {
399+
this.liveRegionErrorMessagesShownAlready = false;
400+
}, timeoutMs);
401+
}
402+
}
403+
404+
/**
405+
* Get the number of invalid inputs in the formGroup
406+
*/
407+
private getNumberOfInvalidInputs(): number {
408+
if (this.formGroup && this.formGroup.controls) {
409+
return Object.values(this.formGroup.controls).filter((control: AbstractControl) => control.invalid).length;
355410
}
356411
}
357412

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
(keydown.escape)="cancelKeyboardDragAndDrop(sortableElement, idx, length)"
2626
(keydown.arrowUp)="handleArrowPress($event, dropList, length, idx, 'up')"
2727
(keydown.arrowDown)="handleArrowPress($event, dropList, length, idx, 'down')">
28-
<i class="drag-icon fas fa-grip-vertical fa-fw" [class.drag-disable]="dragDisabled" ></i>
28+
<i class="drag-icon" [class.drag-disable]="dragDisabled" aria-hidden="true"></i>
2929
</div>
3030
<ng-container *ngTemplateOutlet="startTemplate?.templateRef; context: groupModel"></ng-container>
3131
<ds-dynamic-form-control-container *ngFor="let _model of groupModel.group"

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@
1616
margin-left: calc(-2.3 * var(--bs-spacer));
1717
margin-right: calc(-0.5 * var(--bs-spacer));
1818
padding-right: calc(0.5 * var(--bs-spacer));
19+
> .col {
20+
padding-left: 5px;
21+
padding-right: 5px;
22+
}
23+
.cdk-drag-handle {
24+
width: calc(2 * var(--bs-spacer));
25+
}
26+
1927
.drag-icon {
2028
width: calc(2 * var(--bs-spacer));
2129
color: var(--bs-gray-600);

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,17 @@ import {
1414
DynamicFormValidationService,
1515
DynamicInputModel,
1616
} from '@ng-dynamic-forms/core';
17+
import { provideMockActions } from '@ngrx/effects/testing';
1718
import { provideMockStore } from '@ngrx/store/testing';
1819
import {
1920
TranslateModule,
2021
TranslateService,
2122
} from '@ngx-translate/core';
2223
import { NgxMaskModule } from 'ngx-mask';
23-
import { of } from 'rxjs';
24+
import {
25+
Observable,
26+
of,
27+
} from 'rxjs';
2428

2529
import {
2630
APP_CONFIG,
@@ -63,6 +67,7 @@ describe('DsDynamicFormArrayComponent', () => {
6367
{ provide: TranslateService, useValue: translateServiceStub },
6468
{ provide: HttpClient, useValue: {} },
6569
{ provide: SubmissionService, useValue: {} },
70+
provideMockActions(() => new Observable<any>()),
6671
{ provide: APP_CONFIG, useValue: environment },
6772
{ provide: DYNAMIC_FORM_CONTROL_MAP_FN, useValue: dsDynamicFormControlMapFn },
6873
{ provide: LiveRegionService, useValue: getLiveRegionServiceStub() },

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

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
<div>
2-
<fieldset class="d-flex">
2+
<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')]">
44
{{model.placeholder}} <span *ngIf="model.required">*</span>
5-
</legend>
5+
</legend>
66
<ds-number-picker
77
tabindex="0"
88
[id]="model.id + '_year'"
@@ -15,28 +15,30 @@
1515
[value]="year"
1616
[invalid]="showErrorMessages"
1717
[placeholder]="'form.date-picker.placeholder.year' | translate"
18+
[widthClass]="'four-digits'"
1819
(blur)="onBlur($event)"
1920
(change)="onChange($event)"
2021
(focus)="onFocus($event)"
2122
></ds-number-picker>
2223

23-
<ds-number-picker
24+
<ds-number-picker class="date-month"
2425
tabindex="0"
2526
[id]="model.id + '_month'"
2627
[min]="minMonth"
2728
[max]="maxMonth"
2829
[name]="'month'"
29-
[size]="6"
30+
[size]="2"
3031
[(ngModel)]="initialMonth"
3132
[value]="month"
3233
[placeholder]="'form.date-picker.placeholder.month' | translate"
3334
[disabled]="!year || model.disabled"
35+
[widthClass]="'two-digits'"
3436
(blur)="onBlur($event)"
3537
(change)="onChange($event)"
3638
(focus)="onFocus($event)"
3739
></ds-number-picker>
3840

39-
<ds-number-picker
41+
<ds-number-picker class="date-day"
4042
tabindex="0"
4143
[id]="model.id + '_day'"
4244
[min]="minDay"
@@ -47,6 +49,7 @@
4749
[value]="day"
4850
[placeholder]="'form.date-picker.placeholder.day' | translate"
4951
[disabled]="!month || model.disabled"
52+
[widthClass]="'two-digits'"
5053
(blur)="onBlur($event)"
5154
(change)="onChange($event)"
5255
(focus)="onFocus($event)"

0 commit comments

Comments
 (0)