mirror of
https://github.com/Alfresco/alfresco-content-app.git
synced 2026-09-09 18:02:54 +00:00
[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:
+1
@@ -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"
|
||||
|
||||
+2
-2
@@ -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'],
|
||||
|
||||
+62
-2
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+16
-9
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+8
-8
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user