Skip to content

Commit 875b912

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

25 files changed

Lines changed: 312 additions & 84 deletions

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
@@ -89,7 +89,7 @@
8989
[class.disabled]="isOnlyValue || saving" [dsBtnDisabled]="isOnlyValue || saving"
9090
[title]="dsoType + '.edit.metadata.edit.buttons.drag' | translate"
9191
ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.drag' | translate }}">
92-
<i class="fas fa-grip-vertical fa-fw"></i>
92+
<i class="drag-icon"></i>
9393
</button>
9494
</div>
9595
</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
@@ -81,8 +81,7 @@
8181
scope="row" id="{{ entry.nameStripped }}" headers="{{ bundleName }} name">
8282
<div class="drag-handle text-muted float-start p-1 me-2 d-inline" tabindex="0" cdkDragHandle
8383
(keydown.enter)="select($event, entry)" (keydown.space)="select($event, entry)" (click)="select($event, entry)">
84-
<i class="fas fa-grip-vertical fa-fw"
85-
[title]="'item.edit.bitstreams.edit.buttons.drag' | translate"></i>
84+
<i class="drag-icon" [title]="'item.edit.bitstreams.edit.buttons.drag' | translate"></i>
8685
</div>
8786
<span class="dont-break-out">{{ entry.name }}</span>
8887
</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
@@ -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: 54 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 { provideEnvironmentNgxMask } from 'ngx-mask';
58-
import { of } from 'rxjs';
59+
import {
60+
of,
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,8 @@ describe('DsDynamicFormControlContainerComponent test suite', () => {
208221
const testItem: Item = new Item();
209222
const testWSI: WorkspaceItem = new WorkspaceItem();
210223
testWSI.item = of(createSuccessfulRemoteDataObject(testItem));
224+
const actions$: ReplaySubject<any> = new ReplaySubject<any>(1);
225+
211226
beforeEach(waitForAsync(() => {
212227

213228
TestBed.configureTestingModule({
@@ -240,6 +255,8 @@ describe('DsDynamicFormControlContainerComponent test suite', () => {
240255
{ provide: APP_CONFIG, useValue: environment },
241256
{ provide: APP_DATA_SERVICES_MAP, useValue: {} },
242257
{ provide: DYNAMIC_FORM_CONTROL_MAP_FN, useValue: dsDynamicFormControlMapFn },
258+
{ provide: LiveRegionService, useValue: getLiveRegionServiceStub() },
259+
{ provide: Actions, useValue: actions$ },
243260
],
244261
schemas: [CUSTOM_ELEMENTS_SCHEMA],
245262
}).compileComponents().then(() => {
@@ -381,4 +398,40 @@ describe('DsDynamicFormControlContainerComponent test suite', () => {
381398
expect(testFn(formModel[25])).toEqual(DsDynamicFormGroupComponent);
382399
});
383400

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+
384437
});

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
@@ -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,
@@ -53,6 +55,10 @@ import {
5355
DynamicFormValidationService,
5456
DynamicTemplateDirective,
5557
} from '@ng-dynamic-forms/core';
58+
import {
59+
Actions,
60+
ofType,
61+
} from '@ngrx/effects';
5662
import { Store } from '@ngrx/store';
5763
import {
5864
TranslateModule,
@@ -99,13 +105,15 @@ import { SubmissionObject } from '../../../../core/submission/models/submission-
99105
import { SUBMISSION_LINKS_TO_FOLLOW } from '../../../../core/submission/resolver/submission-links-to-follow';
100106
import { SubmissionObjectDataService } from '../../../../core/submission/submission-object-data.service';
101107
import { paginatedRelationsToItems } from '../../../../item-page/simple/item-types/shared/item-relationships-utils';
108+
import { SubmissionObjectActionTypes } from '../../../../submission/objects/submission-objects.actions';
102109
import { SubmissionService } from '../../../../submission/submission.service';
103110
import {
104111
hasNoValue,
105112
hasValue,
106113
isNotEmpty,
107114
isNotUndefined,
108115
} from '../../../empty.util';
116+
import { LiveRegionService } from '../../../live-region/live-region.service';
109117
import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model';
110118
import { SelectableListState } from '../../../object-list/selectable-list/selectable-list.reducer';
111119
import { SelectableListService } from '../../../object-list/selectable-list/selectable-list.service';
@@ -172,6 +180,8 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
172180
*/
173181
private subs: Subscription[] = [];
174182

183+
private liveRegionErrorMessagesShownAlready = false;
184+
175185
/* eslint-disable @angular-eslint/no-output-rename */
176186
@Output('dfBlur') blur: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
177187
@Output('dfChange') change: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
@@ -191,6 +201,8 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
191201
return this.dynamicFormControlFn(this.model);
192202
}
193203

204+
private readonly liveRegionService = inject(LiveRegionService);
205+
194206
constructor(
195207
protected componentFactoryResolver: ComponentFactoryResolver,
196208
protected dynamicFormComponentService: DynamicFormComponentService,
@@ -210,6 +222,7 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
210222
protected metadataService: MetadataService,
211223
@Inject(APP_CONFIG) protected appConfig: AppConfig,
212224
@Inject(DYNAMIC_FORM_CONTROL_MAP_FN) protected dynamicFormControlFn: DynamicFormControlMapFn,
225+
private actions$: Actions,
213226
) {
214227
super(ref, componentFactoryResolver, layoutService, validationService, dynamicFormComponentService, relationService);
215228
this.fetchThumbnail = this.appConfig.browseBy.showThumbnails;
@@ -222,6 +235,18 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
222235
this.isRelationship = hasValue(this.model.relationship);
223236
const isWrapperAroundRelationshipList = hasValue(this.model.relationshipConfig);
224237

238+
// Subscribe to specified submission actions to announce error messages
239+
const errorAnnounceActionsSub = this.actions$.pipe(
240+
ofType(
241+
SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM_SUCCESS,
242+
SubmissionObjectActionTypes.SAVE_SUBMISSION_SECTION_FORM_SUCCESS,
243+
SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM_ERROR,
244+
SubmissionObjectActionTypes.SAVE_FOR_LATER_SUBMISSION_FORM_ERROR,
245+
SubmissionObjectActionTypes.SAVE_SUBMISSION_SECTION_FORM_ERROR,
246+
),
247+
).subscribe(() => this.announceErrorMessages());
248+
this.subs.push(errorAnnounceActionsSub);
249+
225250
if (this.isRelationship || isWrapperAroundRelationshipList) {
226251
const config = this.model.relationshipConfig || this.model.relationship;
227252
const relationshipOptions = Object.assign(new RelationshipOptions(), config);
@@ -346,6 +371,36 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
346371
if (this.showErrorMessages) {
347372
this.destroyFormControlComponent();
348373
this.createFormControlComponent();
374+
this.announceErrorMessages();
375+
}
376+
}
377+
378+
/**
379+
* Announce error messages to the user
380+
*/
381+
announceErrorMessages() {
382+
if (!this.liveRegionErrorMessagesShownAlready) {
383+
this.liveRegionErrorMessagesShownAlready = true;
384+
const numberOfInvalidInputs = this.getNumberOfInvalidInputs() ?? 1;
385+
const timeoutMs = numberOfInvalidInputs * 3500;
386+
this.errorMessages.forEach((errorMsg) => {
387+
// set timer based on the number of the invalid inputs
388+
this.liveRegionService.setMessageTimeOutMs(timeoutMs);
389+
const message = this.translateService.instant(errorMsg);
390+
this.liveRegionService.addMessage(message);
391+
});
392+
setTimeout(() => {
393+
this.liveRegionErrorMessagesShownAlready = false;
394+
}, timeoutMs);
395+
}
396+
}
397+
398+
/**
399+
* Get the number of invalid inputs in the formGroup
400+
*/
401+
private getNumberOfInvalidInputs(): number {
402+
if (this.formGroup && this.formGroup.controls) {
403+
return Object.values(this.formGroup.controls).filter((control: AbstractControl) => control.invalid).length;
349404
}
350405
}
351406

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
@@ -26,7 +26,7 @@
2626
(keydown.escape)="cancelKeyboardDragAndDrop(sortableElement, idx, length)"
2727
(keydown.arrowUp)="handleArrowPress($event, dropList, length, idx, 'up')"
2828
(keydown.arrowDown)="handleArrowPress($event, dropList, length, idx, 'down')">
29-
<i class="drag-icon fas fa-grip-vertical fa-fw" [class.drag-disable]="dragDisabled" ></i>
29+
<i class="drag-icon" [class.drag-disable]="dragDisabled" aria-hidden="true"></i>
3030
</div>
3131
<ng-container *ngTemplateOutlet="startTemplate?.templateRef; context: groupModel"></ng-container>
3232
@for (_model of groupModel.group; track _model) {

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

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,6 @@
2828
width: calc(2 * var(--bs-spacer));
2929
}
3030

31-
.drag-icon {
32-
visibility: hidden;
33-
width: calc(2 * var(--bs-spacer));
34-
color: var(--bs-gray-600);
35-
margin: var(--bs-btn-padding-y) 0;
36-
line-height: var(--bs-btn-line-height);
37-
text-indent: calc(0.5 * var(--bs-spacer))
38-
}
39-
4031
&:hover, &:focus {
4132
cursor: grab;
4233

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 { provideEnvironmentNgxMask } from 'ngx-mask';
23-
import { of } from 'rxjs';
24+
import {
25+
Observable,
26+
of,
27+
} from 'rxjs';
2428
import { LiveRegionService } from 'src/app/shared/live-region/live-region.service';
2529

2630
import {
@@ -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: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
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
1112
tabindex="0"
1213
[id]="model.id + '_year'"
@@ -19,28 +20,30 @@
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
29+
<ds-number-picker class="date-month"
2830
tabindex="0"
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
46+
<ds-number-picker class="date-day"
4447
tabindex="0"
4548
[id]="model.id + '_day'"
4649
[min]="minDay"
@@ -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)"

0 commit comments

Comments
 (0)