diff --git a/projects/aca-content/src/lib/components/context-menu/context-menu-outside-event.directive.spec.ts b/projects/aca-content/src/lib/components/context-menu/context-menu-outside-event.directive.spec.ts
index 839922b6d..04b180d5f 100644
--- a/projects/aca-content/src/lib/components/context-menu/context-menu-outside-event.directive.spec.ts
+++ b/projects/aca-content/src/lib/components/context-menu/context-menu-outside-event.directive.spec.ts
@@ -22,10 +22,84 @@
* from Hyland Software. If not, see .
*/
+import { Component, DebugElement } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
import { OutsideEventDirective } from './context-menu-outside-event.directive';
+import { MatMenuModule } from '@angular/material/menu';
+import { UnitTestingUtils } from '@alfresco/adf-core';
+
+@Component({
+ standalone: true,
+ imports: [OutsideEventDirective, MatMenuModule],
+ template: `
+
+
+
+
+
+ `
+})
+class TestComponent {}
describe('OutsideEventDirective', () => {
- it('should be defined', () => {
- expect(OutsideEventDirective).toBeDefined();
+ let fixture: ComponentFixture;
+ let directive: DebugElement;
+ let unitTestingUtils: UnitTestingUtils;
+ let directiveInstance: OutsideEventDirective;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [TestComponent, OutsideEventDirective, MatMenuModule]
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(TestComponent);
+ unitTestingUtils = new UnitTestingUtils(fixture.debugElement);
+ directive = unitTestingUtils.getByDirective(OutsideEventDirective);
+ directiveInstance = directive.injector.get(OutsideEventDirective);
+ fixture.detectChanges();
+ });
+
+ it('should emit clickOutside when clicking outside the context menu', () => {
+ spyOn(directiveInstance.clickOutside, 'next');
+
+ unitTestingUtils.clickByDataAutomationId('outside-button');
+ fixture.detectChanges();
+
+ expect(directiveInstance.clickOutside.next).toHaveBeenCalled();
+ });
+
+ it('should not emit clickOutside when clicking inside the context menu', () => {
+ unitTestingUtils.clickByDataAutomationId('trigger-button');
+ fixture.detectChanges();
+
+ spyOn(directiveInstance.clickOutside, 'next');
+
+ unitTestingUtils.clickByDataAutomationId('menu-item');
+ fixture.detectChanges();
+
+ expect(directiveInstance.clickOutside.next).not.toHaveBeenCalled();
+ });
+
+ it('should focus focusTargetSelector element on escape key', () => {
+ expect(directiveInstance.focusTargetSelector).toBe('.adf-context-menu-source');
+
+ directiveInstance.focusTargetSelector = '.custom-selector';
+ unitTestingUtils.clickByDataAutomationId('trigger-button');
+ fixture.detectChanges();
+
+ const contextMenuSource: HTMLElement = unitTestingUtils.getByCSS(directiveInstance.focusTargetSelector).nativeElement;
+ spyOn(contextMenuSource, 'focus');
+
+ document.dispatchEvent(
+ new KeyboardEvent('keydown', {
+ key: 'Escape',
+ code: 'Escape',
+ bubbles: true
+ })
+ );
+
+ expect(contextMenuSource.focus).toHaveBeenCalled();
});
});
diff --git a/projects/aca-content/src/lib/components/context-menu/context-menu-outside-event.directive.ts b/projects/aca-content/src/lib/components/context-menu/context-menu-outside-event.directive.ts
index ca2a28361..a6e47edf3 100644
--- a/projects/aca-content/src/lib/components/context-menu/context-menu-outside-event.directive.ts
+++ b/projects/aca-content/src/lib/components/context-menu/context-menu-outside-event.directive.ts
@@ -22,7 +22,7 @@
* from Hyland Software. If not, see .
*/
-import { DestroyRef, Directive, EventEmitter, inject, OnInit, Output } from '@angular/core';
+import { DestroyRef, Directive, EventEmitter, HostListener, inject, Input, OnInit, Output } from '@angular/core';
import { fromEvent } from 'rxjs';
import { filter } from 'rxjs/operators';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@@ -35,6 +35,9 @@ export class OutsideEventDirective implements OnInit {
@Output()
clickOutside: EventEmitter = new EventEmitter();
+ @Input()
+ focusTargetSelector = '.adf-context-menu-source';
+
private readonly destroyRef = inject(DestroyRef);
ngOnInit() {
@@ -46,6 +49,11 @@ export class OutsideEventDirective implements OnInit {
.subscribe(() => this.clickOutside.next());
}
+ @HostListener('document:keydown.escape', ['$event'])
+ onEscapeKeydown() {
+ document.querySelector(this.focusTargetSelector)?.focus();
+ }
+
private findAncestor(el: Element): boolean {
const className = 'aca-context-menu';
diff --git a/projects/aca-content/src/lib/components/search/search-save/directive/save-search.directive.spec.ts b/projects/aca-content/src/lib/components/search/search-save/directive/save-search.directive.spec.ts
index 67b493c57..d37264b4e 100644
--- a/projects/aca-content/src/lib/components/search/search-save/directive/save-search.directive.spec.ts
+++ b/projects/aca-content/src/lib/components/search/search-save/directive/save-search.directive.spec.ts
@@ -80,7 +80,8 @@ describe('SaveSearchDirective', () => {
element.triggerEventHandler('click', event);
const expectedConfig = {
- data: { searchUrl: 'encodedQuery' }
+ data: { searchUrl: 'encodedQuery' },
+ restoreFocus: true
};
expect(dialog.open).toHaveBeenCalledWith(SaveSearchDialogComponent, expectedConfig);
diff --git a/projects/aca-content/src/lib/components/search/search-save/directive/save-search.directive.ts b/projects/aca-content/src/lib/components/search/search-save/directive/save-search.directive.ts
index 0487c77da..f25cd0d5e 100644
--- a/projects/aca-content/src/lib/components/search/search-save/directive/save-search.directive.ts
+++ b/projects/aca-content/src/lib/components/search/search-save/directive/save-search.directive.ts
@@ -22,7 +22,7 @@
* from Hyland Software. If not, see .
*/
-import { Directive, HostListener, Input } from '@angular/core';
+import { Directive, ElementRef, HostListener, Input } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { SaveSearchDialogComponent } from '../dialog/save-search-dialog.component';
@@ -39,16 +39,20 @@ export class SaveSearchDirective {
@Input()
acaSaveSearchQuery: string;
- constructor(private readonly dialogRef: MatDialog) {}
+ constructor(
+ private readonly dialogRef: MatDialog,
+ private readonly elementRef: ElementRef
+ ) {}
@HostListener('click', ['$event'])
onClick(event: MouseEvent) {
event.preventDefault();
+ this.elementRef.nativeElement.focus();
this.openDialog();
}
private openDialog(): void {
- this.dialogRef.open(SaveSearchDialogComponent, this.getDialogConfig());
+ this.dialogRef.open(SaveSearchDialogComponent, { ...this.getDialogConfig(), restoreFocus: true });
}
private getDialogConfig(): { data: SaveSearchDirectiveDialogData } {
diff --git a/projects/aca-content/src/lib/components/search/search-save/list/saved-searches-list-ui.service.spec.ts b/projects/aca-content/src/lib/components/search/search-save/list/saved-searches-list-ui.service.spec.ts
index 88a878a1e..337ec7ba7 100644
--- a/projects/aca-content/src/lib/components/search/search-save/list/saved-searches-list-ui.service.spec.ts
+++ b/projects/aca-content/src/lib/components/search/search-save/list/saved-searches-list-ui.service.spec.ts
@@ -22,16 +22,18 @@
* from Hyland Software. If not, see .
*/
-import { TestBed } from '@angular/core/testing';
-import { MatDialog, MatDialogModule } from '@angular/material/dialog';
+import { fakeAsync, TestBed } from '@angular/core/testing';
+import { MatDialog, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { SavedSearchesListUiService } from './saved-searches-list-ui.service';
import { SavedSearch } from '@alfresco/adf-content-services';
import { SavedSearchEditDialogComponent } from '../dialog/edit/saved-search-edit-dialog.component';
import { SavedSearchDeleteDialogComponent } from '../dialog/delete/saved-search-delete-dialog.component';
+import { of } from 'rxjs';
describe('NodeTemplateService', () => {
let dialog: MatDialog;
let savedSearchesListUiService: SavedSearchesListUiService;
+ let dialogRefSpy: jasmine.SpyObj>;
const mockedSearch: SavedSearch = { name: 'test', encodedUrl: 'test', order: 1 };
@@ -42,19 +44,21 @@ describe('NodeTemplateService', () => {
dialog = TestBed.inject(MatDialog);
savedSearchesListUiService = TestBed.inject(SavedSearchesListUiService);
+
+ dialogRefSpy = jasmine.createSpyObj('MatDialogRef', ['afterClosed']);
+ dialogRefSpy.afterClosed.and.returnValue(of(null));
+ spyOn(dialog, 'open').and.returnValue(dialogRefSpy);
});
- it('should open edit save search dialog with proper params', () => {
- spyOn(dialog, 'open');
+ it('should open edit save search dialog with proper params', fakeAsync(() => {
savedSearchesListUiService.openEditSavedSearch(mockedSearch);
expect(dialog.open).toHaveBeenCalledWith(SavedSearchEditDialogComponent, { data: mockedSearch, width: '600px' });
- });
+ }));
- it('should open delete save search dialog with proper params', () => {
- spyOn(dialog, 'open');
+ it('should open delete save search dialog with proper params', fakeAsync(() => {
savedSearchesListUiService.confirmDeleteSavedSearch(mockedSearch);
expect(dialog.open).toHaveBeenCalledWith(SavedSearchDeleteDialogComponent, { data: mockedSearch, minWidth: '500px' });
- });
+ }));
});
diff --git a/projects/aca-content/src/lib/components/search/search-save/list/saved-searches-list-ui.service.ts b/projects/aca-content/src/lib/components/search/search-save/list/saved-searches-list-ui.service.ts
index dfe9545ec..d53e7cfbe 100644
--- a/projects/aca-content/src/lib/components/search/search-save/list/saved-searches-list-ui.service.ts
+++ b/projects/aca-content/src/lib/components/search/search-save/list/saved-searches-list-ui.service.ts
@@ -33,16 +33,28 @@ export class SavedSearchesListUiService {
private readonly dialog = inject(MatDialog);
openEditSavedSearch(savedSearch: SavedSearch): void {
- this.dialog.open(SavedSearchEditDialogComponent, {
- data: savedSearch,
- width: '600px'
- });
+ this.dialog
+ .open(SavedSearchEditDialogComponent, {
+ data: savedSearch,
+ width: '600px'
+ })
+ .afterClosed()
+ .subscribe(() => this.focusAfterClose(`.adf-datatable-cell--${savedSearch.name}`));
}
confirmDeleteSavedSearch(savedSearch: SavedSearch): void {
- this.dialog.open(SavedSearchDeleteDialogComponent, {
- data: savedSearch,
- minWidth: '500px'
- });
+ this.dialog
+ .open(SavedSearchDeleteDialogComponent, {
+ data: savedSearch,
+ minWidth: '500px'
+ })
+ .afterClosed()
+ .subscribe(() => this.focusAfterClose(`.adf-datatable-cell--${savedSearch.name}`));
+ }
+
+ private focusAfterClose(focusedElementSelector: string): void {
+ if (focusedElementSelector) {
+ document.querySelector(focusedElementSelector)?.closest('adf-datatable-row').focus();
+ }
}
}
diff --git a/projects/aca-content/src/lib/components/search/search-save/list/ui-list/saved-searches-list.ui-component.spec.ts b/projects/aca-content/src/lib/components/search/search-save/list/ui-list/saved-searches-list.ui-component.spec.ts
index 747328267..f7bdb67f7 100644
--- a/projects/aca-content/src/lib/components/search/search-save/list/ui-list/saved-searches-list.ui-component.spec.ts
+++ b/projects/aca-content/src/lib/components/search/search-save/list/ui-list/saved-searches-list.ui-component.spec.ts
@@ -23,14 +23,14 @@
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
-import { DataCellEvent, DataTableComponent, NoopTranslateModule, NotificationService } from '@alfresco/adf-core';
+import { DataCellEvent, DataTableComponent, NoopTranslateModule, NotificationService, UnitTestingUtils } from '@alfresco/adf-core';
import { SavedSearchesListUiComponent } from './saved-searches-list.ui-component';
import { SavedSearchesListUiService } from '../saved-searches-list-ui.service';
-import { By } from '@angular/platform-browser';
import { SavedSearch } from '@alfresco/adf-content-services';
import { Subject } from 'rxjs';
import { Clipboard } from '@angular/cdk/clipboard';
import { Router } from '@angular/router';
+import { provideNoopAnimations } from '@angular/platform-browser/animations';
describe('SavedSearchesListUiComponent ', () => {
let fixture: ComponentFixture;
@@ -39,11 +39,12 @@ describe('SavedSearchesListUiComponent ', () => {
let savedSearchesListUiService: SavedSearchesListUiService;
let clipboard: Clipboard;
let router: Router;
+ let unitTestingUtils: UnitTestingUtils;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [NoopTranslateModule, SavedSearchesListUiComponent],
- providers: [SavedSearchesListUiService]
+ providers: [SavedSearchesListUiService, provideNoopAnimations()]
});
notificationService = TestBed.inject(NotificationService);
@@ -53,6 +54,7 @@ describe('SavedSearchesListUiComponent ', () => {
fixture = TestBed.createComponent(SavedSearchesListUiComponent);
component = fixture.componentInstance;
+ unitTestingUtils = new UnitTestingUtils(fixture.debugElement);
});
function getColumnDefinition(key: string, title: string) {
@@ -73,7 +75,7 @@ describe('SavedSearchesListUiComponent ', () => {
beforeEach(() => {
fixture.detectChanges();
- dataTable = fixture.debugElement.query(By.directive(DataTableComponent)).componentInstance;
+ dataTable = unitTestingUtils.getByDirective(DataTableComponent).componentInstance;
dataCellEvent = new DataCellEvent(
{
isSelected: true,
@@ -159,6 +161,42 @@ describe('SavedSearchesListUiComponent ', () => {
]);
});
+ it('should focus .adf-context-menu-source element when escape key is pressed and context menu exists', () => {
+ component.savedSearches = [{ name: '1', order: 0, encodedUrl: '123' }] as SavedSearch[];
+
+ fixture.detectChanges();
+
+ dataTable.showRowContextMenu.emit(dataCellEvent);
+ fixture.detectChanges();
+
+ const tableCell = unitTestingUtils.getByCSS('adf-datatable-cell');
+
+ tableCell.nativeElement.dispatchEvent(
+ new MouseEvent('contextmenu', {
+ bubbles: true,
+ cancelable: true,
+ button: 2
+ })
+ );
+ fixture.detectChanges();
+
+ const contextMenu = document.querySelector('.adf-context-menu');
+ const contextMenuSource: HTMLElement = unitTestingUtils.getByCSS('.adf-context-menu-source').nativeElement;
+
+ expect(contextMenu).toBeTruthy();
+ spyOn(contextMenuSource, 'focus');
+
+ document.dispatchEvent(
+ new KeyboardEvent('keydown', {
+ key: 'Escape',
+ code: 'Escape',
+ bubbles: true
+ })
+ );
+
+ expect(contextMenuSource.focus).toHaveBeenCalled();
+ });
+
describe('Context menu actions', () => {
beforeEach(() => {
spyOn(savedSearchesListUiService, 'openEditSavedSearch');
diff --git a/projects/aca-content/src/lib/components/search/search-save/list/ui-list/saved-searches-list.ui-component.ts b/projects/aca-content/src/lib/components/search/search-save/list/ui-list/saved-searches-list.ui-component.ts
index 7ba811844..789ac05fa 100644
--- a/projects/aca-content/src/lib/components/search/search-save/list/ui-list/saved-searches-list.ui-component.ts
+++ b/projects/aca-content/src/lib/components/search/search-save/list/ui-list/saved-searches-list.ui-component.ts
@@ -22,7 +22,18 @@
* from Hyland Software. If not, see .
*/
-import { AfterContentInit, Component, DestroyRef, EventEmitter, inject, Input, Output, ViewEncapsulation } from '@angular/core';
+import {
+ AfterContentInit,
+ Component,
+ DestroyRef,
+ ElementRef,
+ EventEmitter,
+ HostListener,
+ inject,
+ Input,
+ Output,
+ ViewEncapsulation
+} from '@angular/core';
import {
AppConfigService,
DataCellEvent,
@@ -93,7 +104,8 @@ export class SavedSearchesListUiComponent extends DataTableSchema implements Aft
constructor(
protected appConfig: AppConfigService,
private readonly clipboard: Clipboard,
- private readonly router: Router
+ private readonly router: Router,
+ private readonly hostElement: ElementRef
) {
super(appConfig, '', savedSearchesListSchema);
}
@@ -103,6 +115,14 @@ export class SavedSearchesListUiComponent extends DataTableSchema implements Aft
this.contextMenuAction$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((action) => this.executeMenuOption(action.key, action.data));
}
+ @HostListener('document:keydown.escape', ['$event'])
+ onEscapeKeydown() {
+ const contextMenu = document.querySelector('.adf-context-menu');
+ if (contextMenu) {
+ this.hostElement.nativeElement.querySelector('.adf-context-menu-source')?.focus();
+ }
+ }
+
onShowRowActionsMenu(event: DataCellEvent): void {
event.value.actions = this.menuOptions;
}