From 3680b412db398b2d96958257b4db79f99820cc78 Mon Sep 17 00:00:00 2001 From: Sofia Kurilova Date: Fri, 28 Jun 2024 13:03:26 +0000 Subject: [PATCH] Return focus on create button when new profile created; return focus on profile is profile was edited --- .../risk-assessment.component.html | 6 ++- .../risk-assessment.component.spec.ts | 14 ++---- .../risk-assessment.component.ts | 11 +++-- .../risk-assessment.store.spec.ts | 46 ++++++++++++++++++- .../risk-assessment/risk-assessment.store.ts | 33 ++++++++++++- .../src/app/services/focus-manager.service.ts | 3 +- 6 files changed, 95 insertions(+), 18 deletions(-) diff --git a/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.html b/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.html index 0e19b944b..053add44f 100644 --- a/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.html +++ b/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.html @@ -43,7 +43,11 @@

Saved profiles

{ let component: RiskAssessmentComponent; let fixture: ComponentFixture; let mockService: SpyObj; - let mockFocusManagerService: SpyObj; let mockRiskAssessmentStore: SpyObj; const mockLiveAnnouncer: SpyObj = jasmine.createSpyObj([ @@ -52,16 +50,15 @@ describe('RiskAssessmentComponent', () => { mockService = jasmine.createSpyObj(['fetchProfiles', 'deleteProfile']); mockService.deleteProfile.and.returnValue(of(true)); - mockFocusManagerService = jasmine.createSpyObj('mockFocusManagerService', [ - 'focusFirstElementInContainer', - ]); - mockRiskAssessmentStore = jasmine.createSpyObj('RiskAssessmentStore', [ 'deleteProfile', 'setFocus', 'getProfilesFormat', 'saveProfile', 'updateSelectedProfile', + 'setFocusOnCreateButton', + 'setFocusOnSelectedProfile', + 'setFocusOnProfileForm', ]); await TestBed.configureTestingModule({ @@ -73,7 +70,6 @@ describe('RiskAssessmentComponent', () => { imports: [MatToolbarModule, MatSidenavModule, BrowserAnimationsModule], providers: [ { provide: TestRunService, useValue: mockService }, - { provide: FocusManagerService, useValue: mockFocusManagerService }, { provide: RiskAssessmentStore, useValue: mockRiskAssessmentStore }, { provide: LiveAnnouncer, useValue: mockLiveAnnouncer }, ], @@ -213,10 +209,10 @@ describe('RiskAssessmentComponent', () => { ); }); - it('should focus first element in container', async () => { + it('should focus first element in profile form', async () => { await component.openForm(); expect( - mockFocusManagerService.focusFirstElementInContainer + mockRiskAssessmentStore.setFocusOnProfileForm ).toHaveBeenCalled(); }); }); diff --git a/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.ts b/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.ts index 1336a67de..503d87a52 100644 --- a/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.ts +++ b/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.ts @@ -23,7 +23,6 @@ import { RiskAssessmentStore } from './risk-assessment.store'; import { SimpleDialogComponent } from '../../components/simple-dialog/simple-dialog.component'; import { Subject, takeUntil } from 'rxjs'; import { MatDialog } from '@angular/material/dialog'; -import { FocusManagerService } from '../../services/focus-manager.service'; import { LiveAnnouncer } from '@angular/cdk/a11y'; import { Profile } from '../../model/profile'; import { Observable } from 'rxjs/internal/Observable'; @@ -42,7 +41,6 @@ export class RiskAssessmentComponent implements OnInit, OnDestroy { constructor( private store: RiskAssessmentStore, public dialog: MatDialog, - private focusManagerService: FocusManagerService, private liveAnnouncer: LiveAnnouncer ) {} @@ -59,8 +57,7 @@ export class RiskAssessmentComponent implements OnInit, OnDestroy { this.isOpenProfileForm = true; this.store.updateSelectedProfile(profile); await this.liveAnnouncer.announce('Risk assessment questionnaire'); - const profileForm = window.document.querySelector('app-profile-form'); - this.focusManagerService.focusFirstElementInContainer(profileForm); + this.store.setFocusOnProfileForm(); } deleteProfile( @@ -95,17 +92,23 @@ export class RiskAssessmentComponent implements OnInit, OnDestroy { saveProfileClicked(profile: Profile, selectedProfile: Profile | null): void { if (!selectedProfile) { this.saveProfile(profile); + this.store.setFocusOnCreateButton(); } else { this.openSaveDialog(selectedProfile.name) .pipe(takeUntil(this.destroy$)) .subscribe(saveProfile => { if (saveProfile) { this.saveProfile(profile); + this.store.setFocusOnSelectedProfile(); } }); } } + trackByIndex = (index: number): number => { + return index; + }; + private closeFormAfterDelete(name: string, selectedProfile: Profile | null) { if (selectedProfile?.name === name) { this.isOpenProfileForm = false; diff --git a/modules/ui/src/app/pages/risk-assessment/risk-assessment.store.spec.ts b/modules/ui/src/app/pages/risk-assessment/risk-assessment.store.spec.ts index ab8fdf4be..3bc123929 100644 --- a/modules/ui/src/app/pages/risk-assessment/risk-assessment.store.spec.ts +++ b/modules/ui/src/app/pages/risk-assessment/risk-assessment.store.spec.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { TestBed } from '@angular/core/testing'; +import { fakeAsync, TestBed, tick } from '@angular/core/testing'; import { of, skip, take } from 'rxjs'; import { MockStore, provideMockStore } from '@ngrx/store/testing'; import { TestRunService } from '../../services/test-run.service'; @@ -69,6 +69,7 @@ describe('RiskAssessmentStore', () => { store = TestBed.inject(MockStore); riskAssessmentStore = TestBed.inject(RiskAssessmentStore); + mockFocusManagerService.focusFirstElementInContainer.calls.reset(); spyOn(store, 'dispatch').and.callFake(() => {}); }); @@ -170,6 +171,49 @@ describe('RiskAssessmentStore', () => { }); }); + describe('setFocusOnCreateButton', () => { + const container = document.createElement('div') as HTMLElement; + container.classList.add('risk-assessment-content-empty'); + document.querySelector('body')?.appendChild(container); + + it('should call focusFirstElementInContainer', fakeAsync(() => { + riskAssessmentStore.setFocusOnCreateButton(); + + tick(11); + expect( + mockFocusManagerService.focusFirstElementInContainer + ).toHaveBeenCalledWith(container); + })); + }); + + describe('setFocusOnSelectedProfile', () => { + const container = document.createElement('div') as HTMLElement; + container.classList.add('profiles-drawer-content'); + const inner = document.createElement('div') as HTMLElement; + inner.classList.add('selected'); + container.appendChild(inner); + document.querySelector('body')?.appendChild(container); + + it('should call focusFirstElementInContainer', () => { + riskAssessmentStore.setFocusOnSelectedProfile(); + + expect( + mockFocusManagerService.focusFirstElementInContainer + ).toHaveBeenCalledWith(inner); + }); + }); + + describe('setFocusOnProfileForm', () => { + const profileForm = window.document.querySelector('app-profile-form'); + it('should call focusFirstElementInContainer', () => { + riskAssessmentStore.setFocusOnProfileForm(); + + expect( + mockFocusManagerService.focusFirstElementInContainer + ).toHaveBeenCalledWith(profileForm); + }); + }); + describe('getProfilesFormat', () => { it('should update store', done => { mockService.fetchProfilesFormat.and.returnValue(of(PROFILE_FORM)); diff --git a/modules/ui/src/app/pages/risk-assessment/risk-assessment.store.ts b/modules/ui/src/app/pages/risk-assessment/risk-assessment.store.ts index b544fdf31..93e89b434 100644 --- a/modules/ui/src/app/pages/risk-assessment/risk-assessment.store.ts +++ b/modules/ui/src/app/pages/risk-assessment/risk-assessment.store.ts @@ -17,7 +17,7 @@ import { Injectable } from '@angular/core'; import { ComponentStore } from '@ngrx/component-store'; import { tap, withLatestFrom } from 'rxjs/operators'; -import { exhaustMap } from 'rxjs'; +import { delay, exhaustMap } from 'rxjs'; import { TestRunService } from '../../services/test-run.service'; import { Profile, ProfileFormat } from '../../model/profile'; import { FocusManagerService } from '../../services/focus-manager.service'; @@ -88,6 +88,37 @@ export class RiskAssessmentStore extends ComponentStore { } ); + setFocusOnCreateButton = this.effect(trigger$ => { + return trigger$.pipe( + delay(10), + tap(() => { + this.focusManagerService.focusFirstElementInContainer( + window.document.querySelector('.risk-assessment-content-empty') + ); + }) + ); + }); + + setFocusOnSelectedProfile = this.effect(trigger$ => { + return trigger$.pipe( + tap(() => { + this.focusManagerService.focusFirstElementInContainer( + window.document.querySelector('.profiles-drawer-content .selected') + ); + }) + ); + }); + + setFocusOnProfileForm = this.effect(trigger$ => { + return trigger$.pipe( + tap(() => { + this.focusManagerService.focusFirstElementInContainer( + window.document.querySelector('app-profile-form') + ); + }) + ); + }); + getProfilesFormat = this.effect(trigger$ => { return trigger$.pipe( exhaustMap(() => { diff --git a/modules/ui/src/app/services/focus-manager.service.ts b/modules/ui/src/app/services/focus-manager.service.ts index c145fc71f..ebe3ad939 100644 --- a/modules/ui/src/app/services/focus-manager.service.ts +++ b/modules/ui/src/app/services/focus-manager.service.ts @@ -12,7 +12,6 @@ export class FocusManagerService { const dialogOpened = window.document.querySelector('.mdc-dialog--open'); const parentElem = dialogOpened ? dialogOpened : container; const firstInteractiveElem = this.findFirstInteractiveElem(parentElem); - if (firstInteractiveElem) { firstInteractiveElem.focus(); } @@ -22,7 +21,7 @@ export class FocusManagerService { parentEl: Document | Element | null ): HTMLElement | undefined | null { return parentEl?.querySelector( - 'button:not([disabled="true"]):not([tabindex="-1"]), a:not([disabled="true"]), input:not([disabled="true"]), table' + 'button:not([disabled="true"]):not([tabindex="-1"]), a:not([disabled="true"]), input:not([disabled="true"]), table, [tabindex="0"]' ); } }