Skip to content

Commit 2a0c78f

Browse files
authored
Merge pull request #4777 from 4Science/task/main/CST-16756_squashed
Fix submission form's "serious" accessibility issues
2 parents de031f3 + fa79c1e commit 2a0c78f

25 files changed

+310
-78
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
@@ -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
@@ -37,6 +37,8 @@ import {
3737
import { ScrollToService } from '@nicky-lenaers/ngx-scroll-to';
3838

3939
import { HostWindowService } from '../../shared/host-window.service';
40+
import { LiveRegionService } from '../../shared/live-region/live-region.service';
41+
import { getLiveRegionServiceStub } from '../../shared/live-region/live-region.service.stub';
4042
import { UploaderComponent } from '../../shared/upload/uploader/uploader.component';
4143
import { MyDSpaceNewSubmissionComponent } from './my-dspace-new-submission.component';
4244
import { getMockEntityTypeService } from './my-dspace-new-submission-dropdown/my-dspace-new-submission-dropdown.component.spec';
@@ -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
@@ -59,14 +59,27 @@ import {
5959
DynamicNGBootstrapTextAreaComponent,
6060
DynamicNGBootstrapTimePickerComponent,
6161
} from '@ng-dynamic-forms/ui-ng-bootstrap';
62+
import { Actions } from '@ngrx/effects';
6263
import { Store } from '@ngrx/store';
6364
import { TranslateModule } from '@ngx-translate/core';
6465
import { provideEnvironmentNgxMask } from 'ngx-mask';
65-
import { of } from 'rxjs';
66+
import {
67+
of,
68+
ReplaySubject,
69+
} from 'rxjs';
6670

6771
import { environment } from '../../../../../environments/environment';
72+
import {
73+
SaveForLaterSubmissionFormErrorAction,
74+
SaveSubmissionFormErrorAction,
75+
SaveSubmissionFormSuccessAction,
76+
SaveSubmissionSectionFormErrorAction,
77+
SaveSubmissionSectionFormSuccessAction,
78+
} from '../../../../submission/objects/submission-objects.actions';
6879
import { SubmissionService } from '../../../../submission/submission.service';
6980
import { SubmissionObjectService } from '../../../../submission/submission-object.service';
81+
import { LiveRegionService } from '../../../live-region/live-region.service';
82+
import { getLiveRegionServiceStub } from '../../../live-region/live-region.service.stub';
7083
import { SelectableListService } from '../../../object-list/selectable-list/selectable-list.service';
7184
import { FormBuilderService } from '../form-builder.service';
7285
import { DsDynamicFormControlContainerComponent } from './ds-dynamic-form-control-container.component';
@@ -206,6 +219,8 @@ describe('DsDynamicFormControlContainerComponent test suite', () => {
206219
const testItem: Item = new Item();
207220
const testWSI: WorkspaceItem = new WorkspaceItem();
208221
testWSI.item = of(createSuccessfulRemoteDataObject(testItem));
222+
const actions$: ReplaySubject<any> = new ReplaySubject<any>(1);
223+
209224
beforeEach(waitForAsync(() => {
210225

211226
TestBed.configureTestingModule({
@@ -238,6 +253,8 @@ describe('DsDynamicFormControlContainerComponent test suite', () => {
238253
{ provide: APP_CONFIG, useValue: environment },
239254
{ provide: APP_DATA_SERVICES_MAP, useValue: {} },
240255
{ provide: DYNAMIC_FORM_CONTROL_MAP_FN, useValue: dsDynamicFormControlMapFn },
256+
{ provide: LiveRegionService, useValue: getLiveRegionServiceStub() },
257+
{ provide: Actions, useValue: actions$ },
241258
],
242259
schemas: [CUSTOM_ELEMENTS_SCHEMA],
243260
}).compileComponents().then(() => {
@@ -379,4 +396,40 @@ describe('DsDynamicFormControlContainerComponent test suite', () => {
379396
expect(testFn(formModel[25])).toEqual(DsDynamicFormGroupComponent);
380397
});
381398

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

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,
@@ -92,6 +94,10 @@ import {
9294
DynamicFormValidationService,
9395
DynamicTemplateDirective,
9496
} from '@ng-dynamic-forms/core';
97+
import {
98+
Actions,
99+
ofType,
100+
} from '@ngrx/effects';
95101
import { Store } from '@ngrx/store';
96102
import {
97103
TranslateModule,
@@ -111,8 +117,10 @@ import {
111117
} from 'rxjs/operators';
112118

113119
import { AppState } from '../../../../app.reducer';
120+
import { SubmissionObjectActionTypes } from '../../../../submission/objects/submission-objects.actions';
114121
import { SubmissionService } from '../../../../submission/submission.service';
115122
import { SubmissionObjectService } from '../../../../submission/submission-object.service';
123+
import { LiveRegionService } from '../../../live-region/live-region.service';
116124
import { SelectableListState } from '../../../object-list/selectable-list/selectable-list.reducer';
117125
import { SelectableListService } from '../../../object-list/selectable-list/selectable-list.service';
118126
import { FormBuilderService } from '../form-builder.service';
@@ -171,6 +179,8 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
171179
*/
172180
private subs: Subscription[] = [];
173181

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

203+
private readonly liveRegionService = inject(LiveRegionService);
204+
193205
constructor(
194206
protected componentFactoryResolver: ComponentFactoryResolver,
195207
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
@@ -16,13 +16,17 @@ import {
1616
DynamicFormValidationService,
1717
DynamicInputModel,
1818
} from '@ng-dynamic-forms/core';
19+
import { provideMockActions } from '@ngrx/effects/testing';
1920
import { provideMockStore } from '@ngrx/store/testing';
2021
import {
2122
TranslateModule,
2223
TranslateService,
2324
} from '@ngx-translate/core';
2425
import { provideEnvironmentNgxMask } from 'ngx-mask';
25-
import { of } from 'rxjs';
26+
import {
27+
Observable,
28+
of,
29+
} from 'rxjs';
2630
import { LiveRegionService } from 'src/app/shared/live-region/live-region.service';
2731

2832
import { environment } from '../../../../../../../environments/environment.test';
@@ -61,6 +65,7 @@ describe('DsDynamicFormArrayComponent', () => {
6165
{ provide: TranslateService, useValue: translateServiceStub },
6266
{ provide: HttpClient, useValue: {} },
6367
{ provide: SubmissionService, useValue: {} },
68+
provideMockActions(() => new Observable<any>()),
6469
{ provide: APP_CONFIG, useValue: environment },
6570
{ provide: DYNAMIC_FORM_CONTROL_MAP_FN, useValue: dsDynamicFormControlMapFn },
6671
{ 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)