From df3fb98ec7ae7494f9bfe9aee94e2f99e32af133 Mon Sep 17 00:00:00 2001 From: Andrea Barbasso <´andrea.barbasso@4science.com´> Date: Thu, 16 Oct 2025 16:12:46 +0200 Subject: [PATCH] [CST-24622] fix submission form's "serious" accessibility issues (cherry picked from commit 85f1dbc38d7695cfcba584c5f2ba18486b302d7d) --- .../dso-edit-metadata-value.component.html | 2 +- .../item-edit-bitstream-bundle.component.html | 3 +- ...my-dspace-new-submission.component.spec.ts | 3 + ...amic-form-control-container.component.html | 7 +- ...c-form-control-container.component.spec.ts | 54 +++++++++++++- ...ynamic-form-control-container.component.ts | 55 ++++++++++++++ .../dynamic-form-array.component.html | 2 +- .../dynamic-form-array.component.scss | 8 +++ .../dynamic-form-array.component.spec.ts | 7 +- .../date-picker/date-picker.component.html | 13 ++-- .../dynamic-relation-group.component.spec.ts | 11 +++ src/app/shared/form/form.component.html | 8 +-- .../number-picker.component.html | 72 ++++++++++--------- .../number-picker.component.scss | 20 ++++-- .../number-picker/number-picker.component.ts | 1 + .../resource-policy-form.component.spec.ts | 10 ++- .../uploader/uploader.component.spec.ts | 5 +- .../upload/uploader/uploader.component.ts | 37 +++++++++- .../submission-form-footer.component.html | 8 +-- .../section-accesses.component.spec.ts | 15 ++-- .../license/section-license.component.spec.ts | 10 ++- .../file/section-upload-file.component.html | 8 +-- src/assets/i18n/en.json5 | 6 ++ src/assets/images/grip-vertical.svg | 5 ++ src/styles/_global-styles.scss | 14 ++++ 25 files changed, 310 insertions(+), 74 deletions(-) create mode 100644 src/assets/images/grip-vertical.svg diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.html b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.html index 22627826874..6e540d51a4d 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.html +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.html @@ -102,7 +102,7 @@ cdkDragHandle [cdkDragHandleDisabled]="disabled" [ngClass]="{'disabled': disabled}" [dsBtnDisabled]="disabled" [title]="dsoType + '.edit.metadata.edit.buttons.drag' | translate" ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.drag' | translate }}"> - + diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html index 348118a05c6..e73fefd6bb8 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html @@ -84,8 +84,7 @@ scope="row" id="{{ entry.nameStripped }}" headers="{{ bundleName }} name">
- +
{{ entry.name }} diff --git a/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.spec.ts b/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.spec.ts index ad2ef073d84..6474ea57ccc 100644 --- a/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.spec.ts +++ b/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.spec.ts @@ -27,6 +27,8 @@ import { DragService } from '../../core/drag.service'; import { CookieService } from '../../core/services/cookie.service'; import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; import { HostWindowService } from '../../shared/host-window.service'; +import { LiveRegionService } from '../../shared/live-region/live-region.service'; +import { getLiveRegionServiceStub } from '../../shared/live-region/live-region.service.stub'; import { CookieServiceMock } from '../../shared/mocks/cookie.service.mock'; import { HttpXsrfTokenExtractorMock } from '../../shared/mocks/http-xsrf-token-extractor.mock'; import { getMockScrollToService } from '../../shared/mocks/scroll-to-service.mock'; @@ -76,6 +78,7 @@ describe('MyDSpaceNewSubmissionComponent test', () => { { provide: CookieService, useValue: new CookieServiceMock() }, { provide: HostWindowService, useValue: new HostWindowServiceStub(800) }, { provide: EntityTypeDataService, useValue: getMockEntityTypeService() }, + { provide: LiveRegionService, useValue: getLiveRegionServiceStub() }, ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html index e54705bb19a..9bd372eb0f3 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html @@ -23,7 +23,12 @@
- {{ message | translate: model.validators }} + {{ message | translate: model.validators }}
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts index 7600e25d894..2d176d9566d 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts @@ -52,10 +52,14 @@ import { DynamicNGBootstrapTextAreaComponent, DynamicNGBootstrapTimePickerComponent, } from '@ng-dynamic-forms/ui-ng-bootstrap'; +import { Actions } from '@ngrx/effects'; import { Store } from '@ngrx/store'; import { TranslateModule } from '@ngx-translate/core'; import { NgxMaskModule } from 'ngx-mask'; -import { of as observableOf } from 'rxjs'; +import { + of as observableOf, + ReplaySubject, +} from 'rxjs'; import { APP_CONFIG, @@ -67,7 +71,16 @@ import { Item } from '../../../../core/shared/item.model'; import { WorkspaceItem } from '../../../../core/submission/models/workspaceitem.model'; import { SubmissionObjectDataService } from '../../../../core/submission/submission-object-data.service'; import { VocabularyOptions } from '../../../../core/submission/vocabularies/models/vocabulary-options.model'; +import { + SaveForLaterSubmissionFormErrorAction, + SaveSubmissionFormErrorAction, + SaveSubmissionFormSuccessAction, + SaveSubmissionSectionFormErrorAction, + SaveSubmissionSectionFormSuccessAction, +} from '../../../../submission/objects/submission-objects.actions'; import { SubmissionService } from '../../../../submission/submission.service'; +import { LiveRegionService } from '../../../live-region/live-region.service'; +import { getLiveRegionServiceStub } from '../../../live-region/live-region.service.stub'; import { SelectableListService } from '../../../object-list/selectable-list/selectable-list.service'; import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils'; import { FormBuilderService } from '../form-builder.service'; @@ -208,6 +221,7 @@ describe('DsDynamicFormControlContainerComponent test suite', () => { const testItem: Item = new Item(); const testWSI: WorkspaceItem = new WorkspaceItem(); testWSI.item = observableOf(createSuccessfulRemoteDataObject(testItem)); + const actions$: ReplaySubject = new ReplaySubject(1); beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ @@ -240,6 +254,8 @@ describe('DsDynamicFormControlContainerComponent test suite', () => { { provide: APP_CONFIG, useValue: environment }, { provide: APP_DATA_SERVICES_MAP, useValue: {} }, { provide: DYNAMIC_FORM_CONTROL_MAP_FN, useValue: dsDynamicFormControlMapFn }, + { provide: LiveRegionService, useValue: getLiveRegionServiceStub() }, + { provide: Actions, useValue: actions$ }, ], schemas: [CUSTOM_ELEMENTS_SCHEMA], }).compileComponents().then(() => { @@ -382,4 +398,40 @@ describe('DsDynamicFormControlContainerComponent test suite', () => { expect(testFn(formModel[25])).toEqual(DsDynamicFormGroupComponent); }); + describe('store action subscriptions', () => { + beforeEach(() => { + fixture.detectChanges(); + }); + + it('should call announceErrorMessages on SAVE_SUBMISSION_FORM_SUCCESS', () => { + spyOn(component, 'announceErrorMessages'); + actions$.next(new SaveSubmissionFormSuccessAction('1234', [] as any)); + expect(component.announceErrorMessages).toHaveBeenCalled(); + }); + + it('should call announceErrorMessages on SAVE_SUBMISSION_SECTION_FORM_SUCCESS', () => { + spyOn(component, 'announceErrorMessages'); + actions$.next(new SaveSubmissionSectionFormSuccessAction('1234', [] as any)); + expect(component.announceErrorMessages).toHaveBeenCalled(); + }); + + it('should call announceErrorMessages on SAVE_SUBMISSION_FORM_ERROR', () => { + spyOn(component, 'announceErrorMessages'); + actions$.next(new SaveSubmissionFormErrorAction('1234')); + expect(component.announceErrorMessages).toHaveBeenCalled(); + }); + + it('should call announceErrorMessages on SAVE_FOR_LATER_SUBMISSION_FORM_ERROR', () => { + spyOn(component, 'announceErrorMessages'); + actions$.next(new SaveForLaterSubmissionFormErrorAction('1234')); + expect(component.announceErrorMessages).toHaveBeenCalled(); + }); + + it('should call announceErrorMessages on SAVE_SUBMISSION_SECTION_FORM_ERROR', () => { + spyOn(component, 'announceErrorMessages'); + actions$.next(new SaveSubmissionSectionFormErrorAction('1234')); + expect(component.announceErrorMessages).toHaveBeenCalled(); + }); + }); + }); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts index 48a8ebbc0cc..cb2ddd9a91c 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts @@ -15,6 +15,7 @@ import { DoCheck, EventEmitter, Inject, + inject, Input, OnChanges, OnDestroy, @@ -27,6 +28,7 @@ import { ViewContainerRef, } from '@angular/core'; import { + AbstractControl, FormsModule, ReactiveFormsModule, UntypedFormArray, @@ -55,6 +57,10 @@ import { DynamicTemplateDirective, } from '@ng-dynamic-forms/core'; import { DynamicFormControlMapFn } from '@ng-dynamic-forms/core/lib/service/dynamic-form-component.service'; +import { + Actions, + ofType, +} from '@ngrx/effects'; import { Store } from '@ngrx/store'; import { TranslateModule, @@ -100,6 +106,7 @@ import { import { SubmissionObject } from '../../../../core/submission/models/submission-object.model'; import { SubmissionObjectDataService } from '../../../../core/submission/submission-object-data.service'; import { paginatedRelationsToItems } from '../../../../item-page/simple/item-types/shared/item-relationships-utils'; +import { SubmissionObjectActionTypes } from '../../../../submission/objects/submission-objects.actions'; import { SubmissionService } from '../../../../submission/submission.service'; import { BtnDisabledDirective } from '../../../btn-disabled.directive'; import { @@ -108,6 +115,7 @@ import { isNotEmpty, isNotUndefined, } from '../../../empty.util'; +import { LiveRegionService } from '../../../live-region/live-region.service'; import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model'; import { SelectableListState } from '../../../object-list/selectable-list/selectable-list.reducer'; import { SelectableListService } from '../../../object-list/selectable-list/selectable-list.service'; @@ -178,6 +186,8 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo */ private subs: Subscription[] = []; + private liveRegionErrorMessagesShownAlready = false; + /* eslint-disable @angular-eslint/no-output-rename */ @Output('dfBlur') blur: EventEmitter = new EventEmitter(); @Output('dfChange') change: EventEmitter = new EventEmitter(); @@ -197,6 +207,8 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo return this.dynamicFormControlFn(this.model); } + private readonly liveRegionService = inject(LiveRegionService); + constructor( protected componentFactoryResolver: ComponentFactoryResolver, protected dynamicFormComponentService: DynamicFormComponentService, @@ -216,6 +228,7 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo protected metadataService: MetadataService, @Inject(APP_CONFIG) protected appConfig: AppConfig, @Inject(DYNAMIC_FORM_CONTROL_MAP_FN) protected dynamicFormControlFn: DynamicFormControlMapFn, + private actions$: Actions, ) { super(ref, componentFactoryResolver, layoutService, validationService, dynamicFormComponentService, relationService); this.fetchThumbnail = this.appConfig.browseBy.showThumbnails; @@ -228,6 +241,18 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo this.isRelationship = hasValue(this.model.relationship); const isWrapperAroundRelationshipList = hasValue(this.model.relationshipConfig); + // Subscribe to specified submission actions to announce error messages + const errorAnnounceActionsSub = this.actions$.pipe( + ofType( + SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM_SUCCESS, + SubmissionObjectActionTypes.SAVE_SUBMISSION_SECTION_FORM_SUCCESS, + SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM_ERROR, + SubmissionObjectActionTypes.SAVE_FOR_LATER_SUBMISSION_FORM_ERROR, + SubmissionObjectActionTypes.SAVE_SUBMISSION_SECTION_FORM_ERROR, + ), + ).subscribe(() => this.announceErrorMessages()); + this.subs.push(errorAnnounceActionsSub); + if (this.isRelationship || isWrapperAroundRelationshipList) { const config = this.model.relationshipConfig || this.model.relationship; const relationshipOptions = Object.assign(new RelationshipOptions(), config); @@ -352,6 +377,36 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo if (this.showErrorMessages) { this.destroyFormControlComponent(); this.createFormControlComponent(); + this.announceErrorMessages(); + } + } + + /** + * Announce error messages to the user + */ + announceErrorMessages() { + if (!this.liveRegionErrorMessagesShownAlready) { + this.liveRegionErrorMessagesShownAlready = true; + const numberOfInvalidInputs = this.getNumberOfInvalidInputs() ?? 1; + const timeoutMs = numberOfInvalidInputs * 3500; + this.errorMessages.forEach((errorMsg) => { + // set timer based on the number of the invalid inputs + this.liveRegionService.setMessageTimeOutMs(timeoutMs); + const message = this.translateService.instant(errorMsg); + this.liveRegionService.addMessage(message); + }); + setTimeout(() => { + this.liveRegionErrorMessagesShownAlready = false; + }, timeoutMs); + } + } + + /** + * Get the number of invalid inputs in the formGroup + */ + private getNumberOfInvalidInputs(): number { + if (this.formGroup && this.formGroup.controls) { + return Object.values(this.formGroup.controls).filter((control: AbstractControl) => control.invalid).length; } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.html index 3ad70632564..6c1edcbdee1 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.html @@ -25,7 +25,7 @@ (keydown.escape)="cancelKeyboardDragAndDrop(sortableElement, idx, length)" (keydown.arrowUp)="handleArrowPress($event, dropList, length, idx, 'up')" (keydown.arrowDown)="handleArrowPress($event, dropList, length, idx, 'down')"> - + .col { + padding-left: 5px; + padding-right: 5px; + } + .cdk-drag-handle { + width: calc(2 * var(--bs-spacer)); + } + .drag-icon { width: calc(2 * var(--bs-spacer)); color: var(--bs-gray-600); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.spec.ts index 707ea485236..3375fd14b40 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.spec.ts @@ -14,13 +14,17 @@ import { DynamicFormValidationService, DynamicInputModel, } from '@ng-dynamic-forms/core'; +import { provideMockActions } from '@ngrx/effects/testing'; import { provideMockStore } from '@ngrx/store/testing'; import { TranslateModule, TranslateService, } from '@ngx-translate/core'; import { NgxMaskModule } from 'ngx-mask'; -import { of } from 'rxjs'; +import { + Observable, + of, +} from 'rxjs'; import { APP_CONFIG, @@ -63,6 +67,7 @@ describe('DsDynamicFormArrayComponent', () => { { provide: TranslateService, useValue: translateServiceStub }, { provide: HttpClient, useValue: {} }, { provide: SubmissionService, useValue: {} }, + provideMockActions(() => new Observable()), { provide: APP_CONFIG, useValue: environment }, { provide: DYNAMIC_FORM_CONTROL_MAP_FN, useValue: dsDynamicFormControlMapFn }, { provide: LiveRegionService, useValue: getLiveRegionServiceStub() }, diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.html index 3dab2d214a7..2ae2316f817 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.html @@ -1,8 +1,8 @@
-
+
{{model.placeholder}} * - + - - { FormComponent, FormService, provideMockStore({ initialState }), + provideMockActions(() => new Observable()), { provide: VocabularyService, useValue: vocabularyServiceStub }, { provide: DsDynamicTypeBindRelationService, useClass: DsDynamicTypeBindRelationService }, { provide: SubmissionObjectDataService, useValue: {} }, @@ -185,9 +190,15 @@ describe('DsDynamicRelationGroupComponent test suite', () => { { provide: APP_CONFIG, useValue: environment }, { provide: APP_DATA_SERVICES_MAP, useValue: {} }, { provide: DYNAMIC_FORM_CONTROL_MAP_FN, useValue: dsDynamicFormControlMapFn }, + { provide: LiveRegionService, useValue: getLiveRegionServiceStub() }, ], schemas: [CUSTOM_ELEMENTS_SCHEMA], }) + .overrideComponent(DsDynamicRelationGroupComponent, { + remove: { + imports: [FormComponent], + }, + }) .compileComponents(); })); diff --git a/src/app/shared/form/form.component.html b/src/app/shared/form/form.component.html index 85e32e66ea8..9cb9328088d 100644 --- a/src/app/shared/form/form.component.html +++ b/src/app/shared/form/form.component.html @@ -16,8 +16,8 @@
@@ -25,8 +25,8 @@
diff --git a/src/app/shared/form/number-picker/number-picker.component.html b/src/app/shared/form/number-picker/number-picker.component.html index 9b9d038e868..807bb0acf13 100644 --- a/src/app/shared/form/number-picker/number-picker.component.html +++ b/src/app/shared/form/number-picker/number-picker.component.html @@ -1,38 +1,46 @@ -
- - + + +
diff --git a/src/app/shared/form/number-picker/number-picker.component.scss b/src/app/shared/form/number-picker/number-picker.component.scss index 94f7f38ef01..95ad3e8e3d8 100644 --- a/src/app/shared/form/number-picker/number-picker.component.scss +++ b/src/app/shared/form/number-picker/number-picker.component.scss @@ -4,24 +4,32 @@ .chevron::before { border-style: solid; - border-width: 0.29em 0.29em 0 0; + border-width: 0.19em 0.19em 0 0; content: ''; display: inline-block; height: 0.69em; - left: 0.05em; position: relative; - top: 0.15em; + top: -0.15rem; transform: rotate(-45deg); vertical-align: middle; width: 0.71em; } .chevron.bottom:before { - top: -.3em; + top: -.45em; transform: rotate(135deg); } -input { - max-width: 80px !important; +.btn-date { + max-height: 1.1rem; + padding: 0; +} + +.four-digits { + width: 90px; +} + +.two-digits { + width: 80px; } .btn-link-focus { diff --git a/src/app/shared/form/number-picker/number-picker.component.ts b/src/app/shared/form/number-picker/number-picker.component.ts index ef35e1ee796..672a68e8016 100644 --- a/src/app/shared/form/number-picker/number-picker.component.ts +++ b/src/app/shared/form/number-picker/number-picker.component.ts @@ -50,6 +50,7 @@ export class NumberPickerComponent implements OnChanges, OnInit, ControlValueAcc @Input() disabled: boolean; @Input() invalid: boolean; @Input() value: number; + @Input() widthClass: 'four-digits' | 'two-digits' | undefined; @Output() selected = new EventEmitter(); @Output() remove = new EventEmitter(); diff --git a/src/app/shared/resource-policies/form/resource-policy-form.component.spec.ts b/src/app/shared/resource-policies/form/resource-policy-form.component.spec.ts index deaef6c611f..9136ccfaee6 100644 --- a/src/app/shared/resource-policies/form/resource-policy-form.component.spec.ts +++ b/src/app/shared/resource-policies/form/resource-policy-form.component.spec.ts @@ -21,11 +21,15 @@ import { } from '@angular/router'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { DYNAMIC_FORM_CONTROL_MAP_FN } from '@ng-dynamic-forms/core'; +import { provideMockActions } from '@ngrx/effects/testing'; import { provideMockStore } from '@ngrx/store/testing'; import { TranslateModule } from '@ngx-translate/core'; import { getTestScheduler } from 'jasmine-marbles'; import { NgxMaskModule } from 'ngx-mask'; -import { of as observableOf } from 'rxjs'; +import { + Observable, + of as observableOf, +} from 'rxjs'; import { delay } from 'rxjs/operators'; import { TestScheduler } from 'rxjs/testing'; import { @@ -57,6 +61,8 @@ import { DsDynamicTypeBindRelationService } from '../../form/builder/ds-dynamic- import { FormBuilderService } from '../../form/builder/form-builder.service'; import { FormComponent } from '../../form/form.component'; import { FormService } from '../../form/form.service'; +import { LiveRegionService } from '../../live-region/live-region.service'; +import { getLiveRegionServiceStub } from '../../live-region/live-region.service.stub'; import { getMockFormService } from '../../mocks/form-service.mock'; import { getMockRequestService } from '../../mocks/request.service.mock'; import { RouterMock } from '../../mocks/router.mock'; @@ -237,6 +243,8 @@ describe('ResourcePolicyFormComponent test suite', () => { { provide: APP_DATA_SERVICES_MAP, useValue: {} }, { provide: DYNAMIC_FORM_CONTROL_MAP_FN, useValue: dsDynamicFormControlMapFn }, provideMockStore({}), + provideMockActions(() => new Observable()), + { provide: LiveRegionService, useValue: getLiveRegionServiceStub() }, ], schemas: [ NO_ERRORS_SCHEMA, diff --git a/src/app/shared/upload/uploader/uploader.component.spec.ts b/src/app/shared/upload/uploader/uploader.component.spec.ts index 90563f10802..55ae023f67f 100644 --- a/src/app/shared/upload/uploader/uploader.component.spec.ts +++ b/src/app/shared/upload/uploader/uploader.component.spec.ts @@ -16,13 +16,15 @@ import { FileUploadModule } from 'ng2-file-upload'; import { DragService } from '../../../core/drag.service'; import { CookieService } from '../../../core/services/cookie.service'; +import { LiveRegionService } from '../../live-region/live-region.service'; +import { getLiveRegionServiceStub } from '../../live-region/live-region.service.stub'; import { CookieServiceMock } from '../../mocks/cookie.service.mock'; import { HttpXsrfTokenExtractorMock } from '../../mocks/http-xsrf-token-extractor.mock'; import { createTestComponent } from '../../testing/utils.test'; import { UploaderComponent } from './uploader.component'; import { UploaderOptions } from './uploader-options.model'; -describe('Chips component', () => { +describe('Uploader component', () => { let testComp: TestComponent; let testFixture: ComponentFixture; @@ -44,6 +46,7 @@ describe('Chips component', () => { DragService, { provide: HttpXsrfTokenExtractor, useValue: new HttpXsrfTokenExtractorMock('mock-token') }, { provide: CookieService, useValue: new CookieServiceMock() }, + { provide: LiveRegionService, useValue: getLiveRegionServiceStub() }, ], schemas: [CUSTOM_ELEMENTS_SCHEMA], }); diff --git a/src/app/shared/upload/uploader/uploader.component.ts b/src/app/shared/upload/uploader/uploader.component.ts index 804200d220b..b4e82ecde1f 100644 --- a/src/app/shared/upload/uploader/uploader.component.ts +++ b/src/app/shared/upload/uploader/uploader.component.ts @@ -15,6 +15,7 @@ import { import { TranslateModule } from '@ngx-translate/core'; import uniqueId from 'lodash/uniqueId'; import { + FileItem, FileUploader, FileUploadModule, } from 'ng2-file-upload'; @@ -33,6 +34,7 @@ import { isNotEmpty, isUndefined, } from '../../empty.util'; +import { LiveRegionService } from '../../live-region/live-region.service'; import { UploaderOptions } from './uploader-options.model'; import { UploaderProperties } from './uploader-properties.model'; @@ -107,6 +109,17 @@ export class UploaderComponent implements OnInit, AfterViewInit { public isOverBaseDropZone = observableOf(false); public isOverDocumentDropZone = observableOf(false); + /** + * Set of progress values that have been announced to screen readers + */ + private announcedProgress: Set = new Set(); + + /** + * The uuid of the last progress message announced to screen readers + * @private + */ + private lastProgressMessageUuid: string; + @HostListener('window:dragover', ['$event']) onDragOver(event: any) { @@ -124,6 +137,7 @@ export class UploaderComponent implements OnInit, AfterViewInit { private dragService: DragService, private tokenExtractor: HttpXsrfTokenExtractor, private cookieService: CookieService, + private liveRegionService: LiveRegionService, ) { } @@ -211,7 +225,28 @@ export class UploaderComponent implements OnInit, AfterViewInit { this.uploader.cancelAll(); }; this.uploader.onProgressAll = () => this.onProgress(); - this.uploader.onProgressItem = () => this.onProgress(); + // Live region service setup + this.liveRegionService.setMessageTimeOutMs(1500); + this.liveRegionService.clear(); + this.uploader.onProgressItem = (fileItem: FileItem, progress: any) => { + this.announceProgress(progress); + this.onProgress(); + }; + } + + /** + * Announce the progress of the upload to screen readers + * @param progress + */ + private announceProgress(progress: any) { + if (!this.announcedProgress.has(progress)) { + this.announcedProgress.add(progress); + const message = progress + '%'; + if (this.lastProgressMessageUuid) { + this.liveRegionService.clearMessageByUUID(this.lastProgressMessageUuid); + } + this.lastProgressMessageUuid = this.liveRegionService.addMessage(message); + } } /** diff --git a/src/app/submission/form/footer/submission-form-footer.component.html b/src/app/submission/form/footer/submission-form-footer.component.html index 5c76b2f9dcc..46f84520cf5 100644 --- a/src/app/submission/form/footer/submission-form-footer.component.html +++ b/src/app/submission/form/footer/submission-form-footer.component.html @@ -1,5 +1,5 @@ -
-
+
+
-
+
{{'submission.general.info.saved' | translate}} @@ -23,7 +23,7 @@
Depositing...
-
+