[ACS-10232][ACS-10326] Fix focus trap for search filters (#11447)

* [ACS-10232][ACS-10326] Fix focus trap for search filters

* sonar issue fix
This commit is contained in:
Mykyta Maliarchuk
2025-12-12 09:52:30 +01:00
committed by GitHub
parent 8a7359c769
commit 7dddbf7d91
4 changed files with 81 additions and 20 deletions
@@ -26,15 +26,20 @@ import { HarnessLoader, TestKey } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatChipHarness } from '@angular/material/chips/testing'; import { MatChipHarness } from '@angular/material/chips/testing';
import { MatIconHarness } from '@angular/material/icon/testing'; import { MatIconHarness } from '@angular/material/icon/testing';
import { ConfigurableFocusTrapFactory } from '@angular/cdk/a11y';
describe('SearchFacetChipTabbedComponent', () => { describe('SearchFacetChipTabbedComponent', () => {
let loader: HarnessLoader; let loader: HarnessLoader;
let component: SearchFacetChipTabbedComponent; let component: SearchFacetChipTabbedComponent;
let fixture: ComponentFixture<SearchFacetChipTabbedComponent>; let fixture: ComponentFixture<SearchFacetChipTabbedComponent>;
const focusTrapFactory = jasmine.createSpyObj('ConfigurableFocusTrapFactory', ['create']);
const focusTrap = jasmine.createSpyObj('ConfigurableFocusTrap', ['focusInitialElement', 'destroy']);
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ContentTestingModule], imports: [ContentTestingModule],
providers: [{ provide: ConfigurableFocusTrapFactory, useValue: focusTrapFactory }],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}); });
fixture = TestBed.createComponent(SearchFacetChipTabbedComponent); fixture = TestBed.createComponent(SearchFacetChipTabbedComponent);
@@ -53,6 +58,7 @@ describe('SearchFacetChipTabbedComponent', () => {
}; };
fixture.detectChanges(); fixture.detectChanges();
loader = TestbedHarnessEnvironment.loader(fixture); loader = TestbedHarnessEnvironment.loader(fixture);
focusTrapFactory.create.and.returnValue(focusTrap);
}); });
/** /**
@@ -76,6 +82,14 @@ describe('SearchFacetChipTabbedComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
} }
/**
* Get chip harness and click it
*/
async function clickChip(): Promise<void> {
const chip = await loader.getHarness(MatChipHarness);
await (await chip.host()).click();
}
it('should display correct label for tabbed facet', () => { it('should display correct label for tabbed facet', () => {
const label = fixture.debugElement.query(By.css('.adf-search-filter-placeholder')).nativeElement.innerText; const label = fixture.debugElement.query(By.css('.adf-search-filter-placeholder')).nativeElement.innerText;
expect(label).toBe(component.tabbedFacet.label + ':'); expect(label).toBe(component.tabbedFacet.label + ':');
@@ -105,16 +119,14 @@ describe('SearchFacetChipTabbedComponent', () => {
}); });
it('should display correct title when facet is opened', async () => { it('should display correct title when facet is opened', async () => {
const chip = await loader.getHarness(MatChipHarness); await clickChip();
await (await chip.host()).click();
const title = fixture.debugElement.query(By.css('.adf-search-filter-title')).nativeElement.innerText.split('\n')[0]; const title = fixture.debugElement.query(By.css('.adf-search-filter-title')).nativeElement.innerText.split('\n')[0];
expect(title).toBe(component.tabbedFacet.label); expect(title).toBe(component.tabbedFacet.label);
}); });
it('should display adf-search-facet-tabbed-content component', async () => { it('should display adf-search-facet-tabbed-content component', async () => {
const chip = await loader.getHarness(MatChipHarness); await clickChip();
await (await chip.host()).click();
const activeTabLabel = fixture.debugElement.query(By.css('adf-search-facet-tabbed-content')); const activeTabLabel = fixture.debugElement.query(By.css('adf-search-facet-tabbed-content'));
expect(activeTabLabel).toBeTruthy(); expect(activeTabLabel).toBeTruthy();
@@ -156,8 +168,7 @@ describe('SearchFacetChipTabbedComponent', () => {
it('should update display value when new displayValue$ emitted', async () => { it('should update display value when new displayValue$ emitted', async () => {
const displayValue = 'field_LABEL: test, test2'; const displayValue = 'field_LABEL: test, test2';
const chip = await loader.getHarness(MatChipHarness); await clickChip();
await (await chip.host()).click();
emitChildEvent('displayValue$', displayValue); emitChildEvent('displayValue$', displayValue);
fixture.detectChanges(); fixture.detectChanges();
@@ -168,8 +179,7 @@ describe('SearchFacetChipTabbedComponent', () => {
spyOn(component.menuTrigger, 'closeMenu').and.callThrough(); spyOn(component.menuTrigger, 'closeMenu').and.callThrough();
spyOn(component, 'onApply').and.callThrough(); spyOn(component, 'onApply').and.callThrough();
const chip = await loader.getHarness(MatChipHarness); await clickChip();
await (await chip.host()).click();
const applyButton = fixture.debugElement.query(By.css('#apply-filter-button')); const applyButton = fixture.debugElement.query(By.css('#apply-filter-button'));
applyButton.triggerEventHandler('click', {}); applyButton.triggerEventHandler('click', {});
@@ -181,12 +191,26 @@ describe('SearchFacetChipTabbedComponent', () => {
spyOn(component.menuTrigger, 'closeMenu').and.callThrough(); spyOn(component.menuTrigger, 'closeMenu').and.callThrough();
spyOn(component, 'onRemove').and.callThrough(); spyOn(component, 'onRemove').and.callThrough();
const chip = await loader.getHarness(MatChipHarness); await clickChip();
await (await chip.host()).click();
const applyButton = fixture.debugElement.query(By.css('#cancel-filter-button')); const applyButton = fixture.debugElement.query(By.css('#cancel-filter-button'));
applyButton.triggerEventHandler('click', {}); applyButton.triggerEventHandler('click', {});
expect(component.menuTrigger.closeMenu).toHaveBeenCalled(); expect(component.menuTrigger.closeMenu).toHaveBeenCalled();
expect(component.onRemove).toHaveBeenCalled(); expect(component.onRemove).toHaveBeenCalled();
}); });
it('should create focus trap and focus initial element when menu opens', async () => {
await clickChip();
expect(focusTrapFactory.create).toHaveBeenCalledWith(component.menuContainer.nativeElement);
expect(focusTrap.focusInitialElement).toHaveBeenCalled();
});
it('should destroy focus trap on menu closed', () => {
component.focusTrap = focusTrap;
component.onClosed();
expect(focusTrap.destroy).toHaveBeenCalled();
expect(component.focusTrap).toBeNull();
});
}); });
@@ -64,17 +64,23 @@ export class SearchFacetChipTabbedComponent {
chipIcon = 'keyboard_arrow_down'; chipIcon = 'keyboard_arrow_down';
isPopulated = false; isPopulated = false;
constructor(private focusTrapFactory: ConfigurableFocusTrapFactory, private changeDetectorRef: ChangeDetectorRef) {} constructor(
private readonly focusTrapFactory: ConfigurableFocusTrapFactory,
private readonly changeDetectorRef: ChangeDetectorRef
) {}
onMenuOpen() { onMenuOpen() {
if (this.menuContainer && !this.focusTrap) { setTimeout(() => {
this.focusTrap = this.focusTrapFactory.create(this.menuContainer.nativeElement); if (this.menuContainer && !this.focusTrap) {
} this.focusTrap = this.focusTrapFactory.create(this.menuContainer.nativeElement);
this.focusTrap.focusInitialElement();
}
});
this.chipIcon = 'keyboard_arrow_up'; this.chipIcon = 'keyboard_arrow_up';
} }
onClosed() { onClosed() {
this.focusTrap.destroy(); this.focusTrap?.destroy();
this.focusTrap = null; this.focusTrap = null;
this.chipIcon = 'keyboard_arrow_down'; this.chipIcon = 'keyboard_arrow_down';
} }
@@ -26,6 +26,7 @@ import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatChipHarness } from '@angular/material/chips/testing'; import { MatChipHarness } from '@angular/material/chips/testing';
import { MatIconHarness } from '@angular/material/icon/testing'; import { MatIconHarness } from '@angular/material/icon/testing';
import { ConfigurableFocusTrapFactory } from '@angular/cdk/a11y';
describe('SearchWidgetChipComponent', () => { describe('SearchWidgetChipComponent', () => {
let loader: HarnessLoader; let loader: HarnessLoader;
@@ -33,9 +34,13 @@ describe('SearchWidgetChipComponent', () => {
let fixture: ComponentFixture<SearchWidgetChipComponent>; let fixture: ComponentFixture<SearchWidgetChipComponent>;
let queryBuilder: SearchQueryBuilderService; let queryBuilder: SearchQueryBuilderService;
const focusTrapFactory = jasmine.createSpyObj('ConfigurableFocusTrapFactory', ['create']);
const focusTrap = jasmine.createSpyObj('ConfigurableFocusTrap', ['focusInitialElement', 'destroy']);
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [MatMenuModule, ContentTestingModule] imports: [MatMenuModule, ContentTestingModule],
providers: [{ provide: ConfigurableFocusTrapFactory, useValue: focusTrapFactory }]
}); });
queryBuilder = TestBed.inject(SearchQueryBuilderService); queryBuilder = TestBed.inject(SearchQueryBuilderService);
fixture = TestBed.createComponent(SearchWidgetChipComponent); fixture = TestBed.createComponent(SearchWidgetChipComponent);
@@ -45,6 +50,7 @@ describe('SearchWidgetChipComponent', () => {
component.category = simpleCategories[1]; component.category = simpleCategories[1];
fixture.detectChanges(); fixture.detectChanges();
loader = TestbedHarnessEnvironment.loader(fixture); loader = TestbedHarnessEnvironment.loader(fixture);
focusTrapFactory.create.and.returnValue(focusTrap);
}); });
it('should update search query on apply click', async () => { it('should update search query on apply click', async () => {
@@ -80,4 +86,23 @@ describe('SearchWidgetChipComponent', () => {
const icon = await chip.getHarness(MatIconHarness); const icon = await chip.getHarness(MatIconHarness);
expect(await icon.getName()).toBe('keyboard_arrow_up'); expect(await icon.getName()).toBe('keyboard_arrow_up');
}); });
it('should create focus trap and focus initial element when menu opens', async () => {
const chip = await loader.getHarness(MatChipHarness);
await (await chip.host()).click();
fixture.detectChanges();
await fixture.whenStable();
expect(focusTrapFactory.create).toHaveBeenCalledWith(component.menuContainer.nativeElement);
expect(focusTrap.focusInitialElement).toHaveBeenCalled();
});
it('should destroy focus trap on main menu closed', () => {
component.focusTrap = focusTrap;
component.onClosed();
expect(focusTrap.destroy).toHaveBeenCalled();
expect(component.focusTrap).toBeNull();
});
}); });
@@ -66,7 +66,10 @@ export class SearchWidgetChipComponent implements AfterViewInit {
focusTrap: ConfigurableFocusTrap; focusTrap: ConfigurableFocusTrap;
chipIcon = 'keyboard_arrow_down'; chipIcon = 'keyboard_arrow_down';
constructor(private readonly cd: ChangeDetectorRef, private readonly focusTrapFactory: ConfigurableFocusTrapFactory) {} constructor(
private readonly cd: ChangeDetectorRef,
private readonly focusTrapFactory: ConfigurableFocusTrapFactory
) {}
ngAfterViewInit(): void { ngAfterViewInit(): void {
this.widgetContainerComponent this.widgetContainerComponent
@@ -78,9 +81,12 @@ export class SearchWidgetChipComponent implements AfterViewInit {
} }
onMenuOpen() { onMenuOpen() {
if (this.menuContainer && !this.focusTrap) { setTimeout(() => {
this.focusTrap = this.focusTrapFactory.create(this.menuContainer.nativeElement); if (this.menuContainer && !this.focusTrap) {
} this.focusTrap = this.focusTrapFactory.create(this.menuContainer.nativeElement);
this.focusTrap.focusInitialElement();
}
});
this.chipIcon = 'keyboard_arrow_up'; this.chipIcon = 'keyboard_arrow_up';
} }