[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 { MatChipHarness } from '@angular/material/chips/testing';
import { MatIconHarness } from '@angular/material/icon/testing';
import { ConfigurableFocusTrapFactory } from '@angular/cdk/a11y';
describe('SearchFacetChipTabbedComponent', () => {
let loader: HarnessLoader;
let component: SearchFacetChipTabbedComponent;
let fixture: ComponentFixture<SearchFacetChipTabbedComponent>;
const focusTrapFactory = jasmine.createSpyObj('ConfigurableFocusTrapFactory', ['create']);
const focusTrap = jasmine.createSpyObj('ConfigurableFocusTrap', ['focusInitialElement', 'destroy']);
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ContentTestingModule],
providers: [{ provide: ConfigurableFocusTrapFactory, useValue: focusTrapFactory }],
schemas: [NO_ERRORS_SCHEMA]
});
fixture = TestBed.createComponent(SearchFacetChipTabbedComponent);
@@ -53,6 +58,7 @@ describe('SearchFacetChipTabbedComponent', () => {
};
fixture.detectChanges();
loader = TestbedHarnessEnvironment.loader(fixture);
focusTrapFactory.create.and.returnValue(focusTrap);
});
/**
@@ -76,6 +82,14 @@ describe('SearchFacetChipTabbedComponent', () => {
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', () => {
const label = fixture.debugElement.query(By.css('.adf-search-filter-placeholder')).nativeElement.innerText;
expect(label).toBe(component.tabbedFacet.label + ':');
@@ -105,16 +119,14 @@ describe('SearchFacetChipTabbedComponent', () => {
});
it('should display correct title when facet is opened', async () => {
const chip = await loader.getHarness(MatChipHarness);
await (await chip.host()).click();
await clickChip();
const title = fixture.debugElement.query(By.css('.adf-search-filter-title')).nativeElement.innerText.split('\n')[0];
expect(title).toBe(component.tabbedFacet.label);
});
it('should display adf-search-facet-tabbed-content component', async () => {
const chip = await loader.getHarness(MatChipHarness);
await (await chip.host()).click();
await clickChip();
const activeTabLabel = fixture.debugElement.query(By.css('adf-search-facet-tabbed-content'));
expect(activeTabLabel).toBeTruthy();
@@ -156,8 +168,7 @@ describe('SearchFacetChipTabbedComponent', () => {
it('should update display value when new displayValue$ emitted', async () => {
const displayValue = 'field_LABEL: test, test2';
const chip = await loader.getHarness(MatChipHarness);
await (await chip.host()).click();
await clickChip();
emitChildEvent('displayValue$', displayValue);
fixture.detectChanges();
@@ -168,8 +179,7 @@ describe('SearchFacetChipTabbedComponent', () => {
spyOn(component.menuTrigger, 'closeMenu').and.callThrough();
spyOn(component, 'onApply').and.callThrough();
const chip = await loader.getHarness(MatChipHarness);
await (await chip.host()).click();
await clickChip();
const applyButton = fixture.debugElement.query(By.css('#apply-filter-button'));
applyButton.triggerEventHandler('click', {});
@@ -181,12 +191,26 @@ describe('SearchFacetChipTabbedComponent', () => {
spyOn(component.menuTrigger, 'closeMenu').and.callThrough();
spyOn(component, 'onRemove').and.callThrough();
const chip = await loader.getHarness(MatChipHarness);
await (await chip.host()).click();
await clickChip();
const applyButton = fixture.debugElement.query(By.css('#cancel-filter-button'));
applyButton.triggerEventHandler('click', {});
expect(component.menuTrigger.closeMenu).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';
isPopulated = false;
constructor(private focusTrapFactory: ConfigurableFocusTrapFactory, private changeDetectorRef: ChangeDetectorRef) {}
constructor(
private readonly focusTrapFactory: ConfigurableFocusTrapFactory,
private readonly changeDetectorRef: ChangeDetectorRef
) {}
onMenuOpen() {
if (this.menuContainer && !this.focusTrap) {
this.focusTrap = this.focusTrapFactory.create(this.menuContainer.nativeElement);
}
setTimeout(() => {
if (this.menuContainer && !this.focusTrap) {
this.focusTrap = this.focusTrapFactory.create(this.menuContainer.nativeElement);
this.focusTrap.focusInitialElement();
}
});
this.chipIcon = 'keyboard_arrow_up';
}
onClosed() {
this.focusTrap.destroy();
this.focusTrap?.destroy();
this.focusTrap = null;
this.chipIcon = 'keyboard_arrow_down';
}
@@ -26,6 +26,7 @@ import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatChipHarness } from '@angular/material/chips/testing';
import { MatIconHarness } from '@angular/material/icon/testing';
import { ConfigurableFocusTrapFactory } from '@angular/cdk/a11y';
describe('SearchWidgetChipComponent', () => {
let loader: HarnessLoader;
@@ -33,9 +34,13 @@ describe('SearchWidgetChipComponent', () => {
let fixture: ComponentFixture<SearchWidgetChipComponent>;
let queryBuilder: SearchQueryBuilderService;
const focusTrapFactory = jasmine.createSpyObj('ConfigurableFocusTrapFactory', ['create']);
const focusTrap = jasmine.createSpyObj('ConfigurableFocusTrap', ['focusInitialElement', 'destroy']);
beforeEach(() => {
TestBed.configureTestingModule({
imports: [MatMenuModule, ContentTestingModule]
imports: [MatMenuModule, ContentTestingModule],
providers: [{ provide: ConfigurableFocusTrapFactory, useValue: focusTrapFactory }]
});
queryBuilder = TestBed.inject(SearchQueryBuilderService);
fixture = TestBed.createComponent(SearchWidgetChipComponent);
@@ -45,6 +50,7 @@ describe('SearchWidgetChipComponent', () => {
component.category = simpleCategories[1];
fixture.detectChanges();
loader = TestbedHarnessEnvironment.loader(fixture);
focusTrapFactory.create.and.returnValue(focusTrap);
});
it('should update search query on apply click', async () => {
@@ -80,4 +86,23 @@ describe('SearchWidgetChipComponent', () => {
const icon = await chip.getHarness(MatIconHarness);
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;
chipIcon = 'keyboard_arrow_down';
constructor(private readonly cd: ChangeDetectorRef, private readonly focusTrapFactory: ConfigurableFocusTrapFactory) {}
constructor(
private readonly cd: ChangeDetectorRef,
private readonly focusTrapFactory: ConfigurableFocusTrapFactory
) {}
ngAfterViewInit(): void {
this.widgetContainerComponent
@@ -78,9 +81,12 @@ export class SearchWidgetChipComponent implements AfterViewInit {
}
onMenuOpen() {
if (this.menuContainer && !this.focusTrap) {
this.focusTrap = this.focusTrapFactory.create(this.menuContainer.nativeElement);
}
setTimeout(() => {
if (this.menuContainer && !this.focusTrap) {
this.focusTrap = this.focusTrapFactory.create(this.menuContainer.nativeElement);
this.focusTrap.focusInitialElement();
}
});
this.chipIcon = 'keyboard_arrow_up';
}