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 031e407e459..4ce0e248c1c 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 @@ -89,7 +89,7 @@ [class.disabled]="isOnlyValue || saving" [dsBtnDisabled]="isOnlyValue || saving" [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 607cad0d895..40a5286ffe4 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 @@ -81,8 +81,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 25a5049b217..0b4cf3ae696 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 @@ -37,6 +37,8 @@ import { import { ScrollToService } from '@nicky-lenaers/ngx-scroll-to'; 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 { UploaderComponent } from '../../shared/upload/uploader/uploader.component'; import { MyDSpaceNewSubmissionComponent } from './my-dspace-new-submission.component'; import { getMockEntityTypeService } from './my-dspace-new-submission-dropdown/my-dspace-new-submission-dropdown.component.spec'; @@ -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 c4c1d79c294..1a14469a2e0 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 @@ -32,7 +32,12 @@
@for (message of errorMessages; track message) { - {{ 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 2721374f4e4..0c136605395 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 @@ -59,14 +59,27 @@ 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 } from 'rxjs'; +import { + of, + ReplaySubject, +} from 'rxjs'; import { environment } from '../../../../../environments/environment'; +import { + SaveForLaterSubmissionFormErrorAction, + SaveSubmissionFormErrorAction, + SaveSubmissionFormSuccessAction, + SaveSubmissionSectionFormErrorAction, + SaveSubmissionSectionFormSuccessAction, +} from '../../../../submission/objects/submission-objects.actions'; import { SubmissionService } from '../../../../submission/submission.service'; import { SubmissionObjectService } from '../../../../submission/submission-object.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 { FormBuilderService } from '../form-builder.service'; import { DsDynamicFormControlContainerComponent } from './ds-dynamic-form-control-container.component'; @@ -206,6 +219,8 @@ describe('DsDynamicFormControlContainerComponent test suite', () => { const testItem: Item = new Item(); const testWSI: WorkspaceItem = new WorkspaceItem(); testWSI.item = of(createSuccessfulRemoteDataObject(testItem)); + const actions$: ReplaySubject = new ReplaySubject(1); + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ @@ -238,6 +253,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(() => { @@ -379,4 +396,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 5a7ebcd34d6..922a60c97a1 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 @@ -13,6 +13,7 @@ import { DoCheck, EventEmitter, Inject, + inject, Input, OnChanges, OnDestroy, @@ -25,6 +26,7 @@ import { ViewContainerRef, } from '@angular/core'; import { + AbstractControl, FormsModule, ReactiveFormsModule, UntypedFormArray, @@ -92,6 +94,10 @@ import { DynamicFormValidationService, DynamicTemplateDirective, } from '@ng-dynamic-forms/core'; +import { + Actions, + ofType, +} from '@ngrx/effects'; import { Store } from '@ngrx/store'; import { TranslateModule, @@ -111,8 +117,10 @@ import { } from 'rxjs/operators'; import { AppState } from '../../../../app.reducer'; +import { SubmissionObjectActionTypes } from '../../../../submission/objects/submission-objects.actions'; import { SubmissionService } from '../../../../submission/submission.service'; import { SubmissionObjectService } from '../../../../submission/submission-object.service'; +import { LiveRegionService } from '../../../live-region/live-region.service'; import { SelectableListState } from '../../../object-list/selectable-list/selectable-list.reducer'; import { SelectableListService } from '../../../object-list/selectable-list/selectable-list.service'; import { FormBuilderService } from '../form-builder.service'; @@ -171,6 +179,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(); @@ -190,6 +200,8 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo return this.dynamicFormControlFn(this.model); } + private readonly liveRegionService = inject(LiveRegionService); + constructor( protected componentFactoryResolver: ComponentFactoryResolver, protected dynamicFormComponentService: DynamicFormComponentService, @@ -210,6 +222,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; @@ -222,6 +235,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); @@ -346,6 +371,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 5c6d50d6ab8..73cae52db2e 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 @@ -26,7 +26,7 @@ (keydown.escape)="cancelKeyboardDragAndDrop(sortableElement, idx, length)" (keydown.arrowUp)="handleArrowPress($event, dropList, length, idx, 'up')" (keydown.arrowDown)="handleArrowPress($event, dropList, length, idx, 'down')"> - + @for (_model of groupModel.group; track _model) { diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.scss b/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.scss index 0005f20dfce..b5dca9630e0 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.scss +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.scss @@ -28,15 +28,6 @@ width: calc(2 * var(--bs-spacer)); } - .drag-icon { - visibility: hidden; - width: calc(2 * var(--bs-spacer)); - color: var(--bs-gray-600); - margin: var(--bs-btn-padding-y) 0; - line-height: var(--bs-btn-line-height); - text-indent: calc(0.5 * var(--bs-spacer)) - } - &:hover, &:focus { cursor: grab; 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 576fa609e95..246d67d6060 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 @@ -16,13 +16,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 { LiveRegionService } from 'src/app/shared/live-region/live-region.service'; import { environment } from '../../../../../../../environments/environment.test'; @@ -61,6 +65,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 5faf188165d..5d38f9e9084 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,12 +1,13 @@
-
+
@if (!model.repeatable) { - {{model.placeholder}} @if (model.required) { - * - } - - } + {{ model.placeholder }} + @if (model.required) { + * + } + + } - - { FormComponent, FormService, provideMockStore({ initialState }), + provideMockActions(() => new Observable()), { provide: VocabularyService, useValue: vocabularyServiceStub }, { provide: DsDynamicTypeBindRelationService, useClass: DsDynamicTypeBindRelationService }, { provide: SubmissionObjectService, useValue: {} }, @@ -177,9 +182,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 46623e73d36..304f2cf14c6 100644 --- a/src/app/shared/form/form.component.html +++ b/src/app/shared/form/form.component.html @@ -17,8 +17,8 @@
@@ -28,8 +28,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 6d8aeb7b9d9..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 0b4e7ac5694..6f7ae8d7e0b 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 90452327cf5..7f22886c2b0 100644 --- a/src/app/shared/form/number-picker/number-picker.component.ts +++ b/src/app/shared/form/number-picker/number-picker.component.ts @@ -49,6 +49,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 3bdc92a7458..24fd7012254 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 @@ -44,11 +44,15 @@ import { import { isNotEmptyOperator } from '@dspace/shared/utils/empty.util'; 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 } from 'rxjs'; +import { + Observable, + of, +} from 'rxjs'; import { delay } from 'rxjs/operators'; import { TestScheduler } from 'rxjs/testing'; @@ -63,6 +67,8 @@ import { FormBuilderService } from '../../form/builder/form-builder.service'; import { FormComponent } from '../../form/form.component'; import { FormService } from '../../form/form.service'; import { getMockFormService } from '../../form/testing/form-service.mock'; +import { LiveRegionService } from '../../live-region/live-region.service'; +import { getLiveRegionServiceStub } from '../../live-region/live-region.service.stub'; import { ResourcePolicyEvent, ResourcePolicyFormComponent, @@ -235,6 +241,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 822a8fd9051..2bd66227702 100644 --- a/src/app/shared/upload/uploader/uploader.component.spec.ts +++ b/src/app/shared/upload/uploader/uploader.component.spec.ts @@ -18,6 +18,8 @@ import { createTestComponent } from '@dspace/core/testing/utils.test'; import { TranslateModule } from '@ngx-translate/core'; import { FileUploadModule } from 'ng2-file-upload'; +import { LiveRegionService } from '../../live-region/live-region.service'; +import { getLiveRegionServiceStub } from '../../live-region/live-region.service.stub'; import { UploaderComponent } from './uploader.component'; import { UploaderOptions } from './uploader-options.model'; @@ -43,6 +45,7 @@ describe('UploaderComponent', () => { 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 6f613351ebc..f52513b3a98 100644 --- a/src/app/shared/upload/uploader/uploader.component.ts +++ b/src/app/shared/upload/uploader/uploader.component.ts @@ -27,12 +27,14 @@ import { import { TranslateModule } from '@ngx-translate/core'; import uniqueId from 'lodash/uniqueId'; import { + FileItem, FileUploader, FileUploadModule, } from 'ng2-file-upload'; import { of } from 'rxjs'; import { BtnDisabledDirective } from '../../btn-disabled.directive'; +import { LiveRegionService } from '../../live-region/live-region.service'; import { UploaderOptions } from './uploader-options.model'; import { UploaderProperties } from './uploader-properties.model'; @@ -111,6 +113,17 @@ export class UploaderComponent implements OnInit, AfterViewInit { public isOverBaseDropZone = of(false); public isOverDocumentDropZone = of(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) { @@ -128,6 +141,7 @@ export class UploaderComponent implements OnInit, AfterViewInit { private dragService: DragService, private tokenExtractor: HttpXsrfTokenExtractor, private cookieService: CookieService, + private liveRegionService: LiveRegionService, ) { } @@ -215,7 +229,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 671888e8242..cf22b0c3a48 100644 --- a/src/app/submission/form/footer/submission-form-footer.component.html +++ b/src/app/submission/form/footer/submission-form-footer.component.html @@ -1,6 +1,6 @@ @if (!!submissionId) { -
-
+
+
@if ((showDepositAndDiscard | async)) {
-
+
@if ((hasUnsavedModification | async) !== true && (processingSaveStatus | async) !== true && (processingDepositStatus | async) !== true) { {{'submission.general.info.saved' | translate}} @@ -36,7 +36,7 @@
} -
+