[ACS-10273] a11y: Saved Search - fix focus management on dialog open and return to trigger on close (#5101)

* [ACS-10273] a11y: Saved Search - fix focus management on dialog open and return to trigger on close

* [ACS-10273] cr fixes
This commit is contained in:
Mykyta Maliarchuk
2026-03-18 16:24:14 +01:00
committed by GitHub
parent 73fd3085b8
commit d1b7b051de
6 changed files with 91 additions and 23 deletions
@@ -14,6 +14,7 @@
<mat-dialog-actions align="end">
<button mat-button
mat-dialog-close
adf-auto-focus
id="aca-save-search-delete-dialog-cancel-button">{{ 'CANCEL' | titlecase | translate }}</button>
<button mat-flat-button
id="aca-save-search-delete-dialog-submit-button"
@@ -23,7 +23,7 @@
*/
import { Component, Inject, ViewEncapsulation } from '@angular/core';
import { SavedSearch } from '@alfresco/adf-content-services';
import { AutoFocusDirective, SavedSearch } from '@alfresco/adf-content-services';
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { take } from 'rxjs/operators';
import { NotificationService } from '@alfresco/adf-core';
@@ -34,7 +34,7 @@ import { MatButtonModule } from '@angular/material/button';
import { SavedSearchesContextService } from '../../../../../services/saved-searches-context.service';
@Component({
imports: [TranslatePipe, TitleCasePipe, MatIconModule, MatButtonModule, MatDialogModule],
imports: [TranslatePipe, TitleCasePipe, MatIconModule, MatButtonModule, MatDialogModule, AutoFocusDirective],
selector: 'aca-saved-search-delete-dialog',
templateUrl: './saved-search-delete-dialog.component.html',
styleUrls: ['./saved-search-delete-dialog.component.scss'],
@@ -53,12 +53,72 @@ describe('NodeTemplateService', () => {
it('should open edit save search dialog with proper params', fakeAsync(() => {
savedSearchesListUiService.openEditSavedSearch(mockedSearch);
expect(dialog.open).toHaveBeenCalledWith(SavedSearchEditDialogComponent, { data: mockedSearch, width: '600px' });
expect(dialog.open).toHaveBeenCalledWith(SavedSearchEditDialogComponent, {
data: mockedSearch,
width: '600px',
restoreFocus: false
});
}));
it('should open delete save search dialog with proper params', fakeAsync(() => {
savedSearchesListUiService.confirmDeleteSavedSearch(mockedSearch);
expect(dialog.open).toHaveBeenCalledWith(SavedSearchDeleteDialogComponent, { data: mockedSearch, minWidth: '500px' });
expect(dialog.open).toHaveBeenCalledWith(SavedSearchDeleteDialogComponent, {
data: mockedSearch,
minWidth: '500px',
restoreFocus: false
});
}));
describe('focusAfterClose', () => {
let mockRow: jasmine.SpyObj<HTMLElement>;
let mockButton: jasmine.SpyObj<HTMLElement>;
let mockCell: jasmine.SpyObj<HTMLElement>;
beforeEach(() => {
mockButton = jasmine.createSpyObj<HTMLElement>('button', ['focus']);
mockRow = jasmine.createSpyObj<HTMLElement>('adf-datatable-row', ['focus', 'querySelector']);
mockRow.querySelector.and.returnValue(mockButton);
mockCell = jasmine.createSpyObj<HTMLElement>('cell', ['closest', 'getAttribute']);
mockCell.getAttribute.and.returnValue(mockedSearch.name);
mockCell.closest.and.returnValue(mockRow);
spyOn(document, 'querySelectorAll').and.returnValue([mockCell] as unknown as NodeListOf<HTMLElement>);
});
it('should focus the actions button when edit dialog closes from actions button', () => {
savedSearchesListUiService.openEditSavedSearch(mockedSearch, false);
expect(mockRow.focus).toHaveBeenCalled();
expect(mockButton.focus).toHaveBeenCalled();
});
it('should focus row when edit dialog closes from context menu', () => {
savedSearchesListUiService.openEditSavedSearch(mockedSearch, true);
expect(mockRow.focus).toHaveBeenCalled();
expect(mockButton.focus).not.toHaveBeenCalled();
});
it('should focus the row before the actions button', () => {
const callOrder: string[] = [];
mockRow.focus.and.callFake(() => callOrder.push('row'));
mockButton.focus.and.callFake(() => callOrder.push('button'));
savedSearchesListUiService.openEditSavedSearch(mockedSearch, false);
expect(callOrder).toEqual(['row', 'button']);
});
it('should focus the actions button when delete dialog closes from actions button', () => {
savedSearchesListUiService.confirmDeleteSavedSearch(mockedSearch, false);
expect(mockRow.focus).toHaveBeenCalled();
expect(mockButton.focus).toHaveBeenCalled();
});
it('should focus row when delete dialog closes from context menu', () => {
savedSearchesListUiService.confirmDeleteSavedSearch(mockedSearch, true);
expect(mockRow.focus).toHaveBeenCalled();
expect(mockButton.focus).not.toHaveBeenCalled();
});
});
});
@@ -32,29 +32,36 @@ import { SavedSearchEditDialogComponent } from '../dialog/edit/saved-search-edit
export class SavedSearchesListUiService {
private readonly dialog = inject(MatDialog);
openEditSavedSearch(savedSearch: SavedSearch): void {
openEditSavedSearch(savedSearch: SavedSearch, fromContextMenu = false): void {
this.dialog
.open(SavedSearchEditDialogComponent, {
data: savedSearch,
width: '600px'
width: '600px',
restoreFocus: false
})
.afterClosed()
.subscribe(() => this.focusAfterClose(`.adf-datatable-cell--${savedSearch.name}`));
.subscribe(() => this.focusAfterClose(savedSearch.name, fromContextMenu));
}
confirmDeleteSavedSearch(savedSearch: SavedSearch): void {
confirmDeleteSavedSearch(savedSearch: SavedSearch, fromContextMenu = false): void {
this.dialog
.open(SavedSearchDeleteDialogComponent, {
data: savedSearch,
minWidth: '500px'
minWidth: '500px',
restoreFocus: false
})
.afterClosed()
.subscribe(() => this.focusAfterClose(`.adf-datatable-cell--${savedSearch.name}`));
.subscribe(() => this.focusAfterClose(savedSearch.name, fromContextMenu));
}
private focusAfterClose(focusedElementSelector: string): void {
if (focusedElementSelector) {
document.querySelector<HTMLElement>(focusedElementSelector)?.closest<HTMLElement>('adf-datatable-row').focus();
private focusAfterClose(name: string, fromContextMenu: boolean): void {
const row = Array.from(document.querySelectorAll<HTMLElement>('.adf-datatable-cell'))
.find((el) => el.getAttribute('data-automation-id') === name)
?.closest<HTMLElement>('adf-datatable-row');
row?.focus();
if (!fromContextMenu) {
row?.querySelector<HTMLElement>('.adf-datatable-actions-menu button')?.focus();
}
}
}
@@ -214,7 +214,7 @@ describe('SavedSearchesListUiComponent ', () => {
});
it('should call openEditSavedSearch when selected edit option', () => {
expect(savedSearchesListUiService.openEditSavedSearch).toHaveBeenCalledWith(editAction.data);
expect(savedSearchesListUiService.openEditSavedSearch).toHaveBeenCalledWith(editAction.data, true);
});
});
@@ -227,7 +227,7 @@ describe('SavedSearchesListUiComponent ', () => {
});
it('should call confirmDeleteSavedSearch when selected delete option', () => {
expect(savedSearchesListUiService.confirmDeleteSavedSearch).toHaveBeenCalledWith(deleteAction.data);
expect(savedSearchesListUiService.confirmDeleteSavedSearch).toHaveBeenCalledWith(deleteAction.data, true);
});
});
@@ -110,7 +110,7 @@ export class SavedSearchesListUiComponent extends DataTableSchema implements Aft
ngAfterContentInit() {
this.createDatatableSchema();
this.contextMenuAction$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((action) => this.executeMenuOption(action.key, action.data));
this.contextMenuAction$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((action) => this.executeMenuOption(action.key, action.data, true));
}
@HostListener('document:keydown.escape')
@@ -129,13 +129,13 @@ export class SavedSearchesListUiComponent extends DataTableSchema implements Aft
this.savedSearchOrderChanged.next(event);
}
executeMenuOption(optionKey: string, savedSearchData: SavedSearch): void {
executeMenuOption(optionKey: string, savedSearchData: SavedSearch, fromContextMenu = false): void {
switch (optionKey) {
case this.editSavedSearchOptionKey:
this.openEditSavedSearchDialog(savedSearchData);
this.openEditSavedSearchDialog(savedSearchData, fromContextMenu);
break;
case this.deleteSavedSearchOptionKey:
this.openDeleteSavedSearchDialog(savedSearchData);
this.openDeleteSavedSearchDialog(savedSearchData, fromContextMenu);
break;
case this.copyToClipboardUrlOptionKey:
this.copyToClipboard(savedSearchData);
@@ -146,12 +146,12 @@ export class SavedSearchesListUiComponent extends DataTableSchema implements Aft
}
}
openEditSavedSearchDialog(savedSearch: SavedSearch): void {
this.savedSearchesListUiService.openEditSavedSearch(savedSearch);
openEditSavedSearchDialog(savedSearch: SavedSearch, fromContextMenu = false): void {
this.savedSearchesListUiService.openEditSavedSearch(savedSearch, fromContextMenu);
}
openDeleteSavedSearchDialog(savedSearch: SavedSearch): void {
this.savedSearchesListUiService.confirmDeleteSavedSearch(savedSearch);
openDeleteSavedSearchDialog(savedSearch: SavedSearch, fromContextMenu = false): void {
this.savedSearchesListUiService.confirmDeleteSavedSearch(savedSearch, fromContextMenu);
}
copyToClipboard(savedSearch: SavedSearch): void {