From 5f80201bc6e861f02f2524ce2746e7b1f8ab8cdc Mon Sep 17 00:00:00 2001 From: PioBar <72926984+Pio-Bar@users.noreply.github.com> Date: Mon, 16 Sep 2024 15:14:28 +0200 Subject: [PATCH] fix: (CXSPA-965) - SearchBox focus managment (#19181) --- .../feature-toggles/config/feature-toggles.ts | 6 ++++ .../spartacus/spartacus-features.module.ts | 1 + .../search-box/search-box.component.spec.ts | 33 ++++++++++++++++++- .../search-box/search-box.component.ts | 19 +++++++++-- 4 files changed, 56 insertions(+), 3 deletions(-) diff --git a/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts b/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts index 07b0ec14607..6a62678480d 100644 --- a/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts +++ b/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts @@ -520,6 +520,11 @@ export interface FeatureTogglesInterface { */ a11yDialogsHeading?: boolean; + /** + * `SearchBoxComponent` should no longer lose focus after closing the popup the esc key. + */ + a11ySearchBoxFocusOnEscape?: boolean; + /** * In OCC cart requests, it puts parameters of a cart name and cart description * into a request body, instead of query params. @@ -624,6 +629,7 @@ export const defaultFeatureToggles: Required = { a11yQuickOrderAriaControls: false, a11yRemoveStatusLoadedRole: false, a11yDialogsHeading: false, + a11ySearchBoxFocusOnEscape: false, occCartNameAndDescriptionInHttpRequestBody: false, cmsBottomHeaderSlotUsingFlexStyles: false, useSiteThemeService: false, diff --git a/projects/storefrontapp/src/app/spartacus/spartacus-features.module.ts b/projects/storefrontapp/src/app/spartacus/spartacus-features.module.ts index d7bad363850..e3dd1c9fcbe 100644 --- a/projects/storefrontapp/src/app/spartacus/spartacus-features.module.ts +++ b/projects/storefrontapp/src/app/spartacus/spartacus-features.module.ts @@ -366,6 +366,7 @@ if (environment.cpq) { a11yQuickOrderAriaControls: true, a11yRemoveStatusLoadedRole: true, a11yDialogsHeading: true, + a11ySearchBoxFocusOnEscape: true, cmsBottomHeaderSlotUsingFlexStyles: true, useSiteThemeService: false, }; diff --git a/projects/storefrontlib/cms-components/navigation/search-box/search-box.component.spec.ts b/projects/storefrontlib/cms-components/navigation/search-box/search-box.component.spec.ts index cc6b5f36720..2fd11d5de96 100644 --- a/projects/storefrontlib/cms-components/navigation/search-box/search-box.component.spec.ts +++ b/projects/storefrontlib/cms-components/navigation/search-box/search-box.component.spec.ts @@ -1,10 +1,17 @@ import { Component, Input, Pipe, PipeTransform } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { RouterModule } from '@angular/router'; import { CmsSearchBoxComponent, + FeatureConfigService, I18nTestingModule, PageType, ProductSearchService, @@ -98,6 +105,12 @@ class MockRoutingService implements Partial { getRouterState = () => routerState$.asObservable(); } +class MockFeatureConfigService { + isEnabled() { + return true; + } +} + describe('SearchBoxComponent', () => { let searchBoxComponent: SearchBoxComponent; let fixture: ComponentFixture; @@ -169,6 +182,10 @@ describe('SearchBoxComponent', () => { provide: RoutingService, useClass: MockRoutingService, }, + { + provide: FeatureConfigService, + useClass: MockFeatureConfigService, + }, ], }).compileComponents(); })); @@ -287,6 +304,20 @@ describe('SearchBoxComponent', () => { expect(searchBoxComponent.getTabIndex(true)).toBe(0); expect(searchBoxComponent.getTabIndex(false)).toBe(0); }); + + it('should focus the search input if search box is closed with the escape key press', fakeAsync(() => { + fixture.detectChanges(); + searchBoxComponent.searchBoxActive = true; + const mockSearchInput = fixture.debugElement.query( + By.css('.searchbox > input') + ).nativeElement; + spyOn(mockSearchInput, 'focus'); + + searchBoxComponent.onEscape(); + tick(); + + expect(mockSearchInput.focus).toHaveBeenCalled(); + })); }); it('should contain 1 product after search', () => { diff --git a/projects/storefrontlib/cms-components/navigation/search-box/search-box.component.ts b/projects/storefrontlib/cms-components/navigation/search-box/search-box.component.ts index 4d46c4d36f6..4c39db57e02 100644 --- a/projects/storefrontlib/cms-components/navigation/search-box/search-box.component.ts +++ b/projects/storefrontlib/cms-components/navigation/search-box/search-box.component.ts @@ -9,6 +9,7 @@ import { ChangeDetectorRef, Component, ElementRef, + HostListener, Input, OnDestroy, OnInit, @@ -73,6 +74,20 @@ export class SearchBoxComponent implements OnInit, OnDestroy { @ViewChild('searchButton') searchButton: ElementRef; + @HostListener('keydown.escape') + onEscape() { + if ( + (this.featureConfigService?.isEnabled('a11ySearchBoxFocusOnEscape') && + this.winRef.document.activeElement !== + this.searchInput.nativeElement) || + this.searchBoxActive + ) { + setTimeout(() => { + this.searchInput.nativeElement.focus(); + }); + } + } + iconTypes = ICON_TYPE; searchBoxActive: boolean = false; @@ -212,7 +227,7 @@ export class SearchBoxComponent implements OnInit, OnDestroy { true ); this.searchBoxActive = true; - this.searchInput.nativeElement.focus(); + this.searchInput?.nativeElement.focus(); } } else { this.searchBoxComponentService.toggleBodyClass(SEARCHBOX_IS_ACTIVE, true); @@ -256,7 +271,7 @@ export class SearchBoxComponent implements OnInit, OnDestroy { // TODO: (CXSPA-6929) - Remove feature flag next major release if (this.a11ySearchBoxMobileFocusEnabled) { this.changeDetecorRef?.detectChanges(); - this.searchButton.nativeElement.focus(); + this.searchButton?.nativeElement.focus(); } else { if (event && event.target) { (event.target).blur();