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">
|
<mat-dialog-actions align="end">
|
||||||
<button mat-button
|
<button mat-button
|
||||||
mat-dialog-close
|
mat-dialog-close
|
||||||
|
adf-auto-focus
|
||||||
id="aca-save-search-delete-dialog-cancel-button">{{ 'CANCEL' | titlecase | translate }}</button>
|
id="aca-save-search-delete-dialog-cancel-button">{{ 'CANCEL' | titlecase | translate }}</button>
|
||||||
<button mat-flat-button
|
<button mat-flat-button
|
||||||
id="aca-save-search-delete-dialog-submit-button"
|
id="aca-save-search-delete-dialog-submit-button"
|
||||||
|
|||||||
+2
-2
@@ -23,7 +23,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component, Inject, ViewEncapsulation } from '@angular/core';
|
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 { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
|
||||||
import { take } from 'rxjs/operators';
|
import { take } from 'rxjs/operators';
|
||||||
import { NotificationService } from '@alfresco/adf-core';
|
import { NotificationService } from '@alfresco/adf-core';
|
||||||
@@ -34,7 +34,7 @@ import { MatButtonModule } from '@angular/material/button';
|
|||||||
import { SavedSearchesContextService } from '../../../../../services/saved-searches-context.service';
|
import { SavedSearchesContextService } from '../../../../../services/saved-searches-context.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
imports: [TranslatePipe, TitleCasePipe, MatIconModule, MatButtonModule, MatDialogModule],
|
imports: [TranslatePipe, TitleCasePipe, MatIconModule, MatButtonModule, MatDialogModule, AutoFocusDirective],
|
||||||
selector: 'aca-saved-search-delete-dialog',
|
selector: 'aca-saved-search-delete-dialog',
|
||||||
templateUrl: './saved-search-delete-dialog.component.html',
|
templateUrl: './saved-search-delete-dialog.component.html',
|
||||||
styleUrls: ['./saved-search-delete-dialog.component.scss'],
|
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(() => {
|
it('should open edit save search dialog with proper params', fakeAsync(() => {
|
||||||
savedSearchesListUiService.openEditSavedSearch(mockedSearch);
|
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(() => {
|
it('should open delete save search dialog with proper params', fakeAsync(() => {
|
||||||
savedSearchesListUiService.confirmDeleteSavedSearch(mockedSearch);
|
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 {
|
export class SavedSearchesListUiService {
|
||||||
private readonly dialog = inject(MatDialog);
|
private readonly dialog = inject(MatDialog);
|
||||||
|
|
||||||
openEditSavedSearch(savedSearch: SavedSearch): void {
|
openEditSavedSearch(savedSearch: SavedSearch, fromContextMenu = false): void {
|
||||||
this.dialog
|
this.dialog
|
||||||
.open(SavedSearchEditDialogComponent, {
|
.open(SavedSearchEditDialogComponent, {
|
||||||
data: savedSearch,
|
data: savedSearch,
|
||||||
width: '600px'
|
width: '600px',
|
||||||
|
restoreFocus: false
|
||||||
})
|
})
|
||||||
.afterClosed()
|
.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
|
this.dialog
|
||||||
.open(SavedSearchDeleteDialogComponent, {
|
.open(SavedSearchDeleteDialogComponent, {
|
||||||
data: savedSearch,
|
data: savedSearch,
|
||||||
minWidth: '500px'
|
minWidth: '500px',
|
||||||
|
restoreFocus: false
|
||||||
})
|
})
|
||||||
.afterClosed()
|
.afterClosed()
|
||||||
.subscribe(() => this.focusAfterClose(`.adf-datatable-cell--${savedSearch.name}`));
|
.subscribe(() => this.focusAfterClose(savedSearch.name, fromContextMenu));
|
||||||
}
|
}
|
||||||
|
|
||||||
private focusAfterClose(focusedElementSelector: string): void {
|
private focusAfterClose(name: string, fromContextMenu: boolean): void {
|
||||||
if (focusedElementSelector) {
|
const row = Array.from(document.querySelectorAll<HTMLElement>('.adf-datatable-cell'))
|
||||||
document.querySelector<HTMLElement>(focusedElementSelector)?.closest<HTMLElement>('adf-datatable-row').focus();
|
.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', () => {
|
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', () => {
|
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() {
|
ngAfterContentInit() {
|
||||||
this.createDatatableSchema();
|
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')
|
@HostListener('document:keydown.escape')
|
||||||
@@ -129,13 +129,13 @@ export class SavedSearchesListUiComponent extends DataTableSchema implements Aft
|
|||||||
this.savedSearchOrderChanged.next(event);
|
this.savedSearchOrderChanged.next(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
executeMenuOption(optionKey: string, savedSearchData: SavedSearch): void {
|
executeMenuOption(optionKey: string, savedSearchData: SavedSearch, fromContextMenu = false): void {
|
||||||
switch (optionKey) {
|
switch (optionKey) {
|
||||||
case this.editSavedSearchOptionKey:
|
case this.editSavedSearchOptionKey:
|
||||||
this.openEditSavedSearchDialog(savedSearchData);
|
this.openEditSavedSearchDialog(savedSearchData, fromContextMenu);
|
||||||
break;
|
break;
|
||||||
case this.deleteSavedSearchOptionKey:
|
case this.deleteSavedSearchOptionKey:
|
||||||
this.openDeleteSavedSearchDialog(savedSearchData);
|
this.openDeleteSavedSearchDialog(savedSearchData, fromContextMenu);
|
||||||
break;
|
break;
|
||||||
case this.copyToClipboardUrlOptionKey:
|
case this.copyToClipboardUrlOptionKey:
|
||||||
this.copyToClipboard(savedSearchData);
|
this.copyToClipboard(savedSearchData);
|
||||||
@@ -146,12 +146,12 @@ export class SavedSearchesListUiComponent extends DataTableSchema implements Aft
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
openEditSavedSearchDialog(savedSearch: SavedSearch): void {
|
openEditSavedSearchDialog(savedSearch: SavedSearch, fromContextMenu = false): void {
|
||||||
this.savedSearchesListUiService.openEditSavedSearch(savedSearch);
|
this.savedSearchesListUiService.openEditSavedSearch(savedSearch, fromContextMenu);
|
||||||
}
|
}
|
||||||
|
|
||||||
openDeleteSavedSearchDialog(savedSearch: SavedSearch): void {
|
openDeleteSavedSearchDialog(savedSearch: SavedSearch, fromContextMenu = false): void {
|
||||||
this.savedSearchesListUiService.confirmDeleteSavedSearch(savedSearch);
|
this.savedSearchesListUiService.confirmDeleteSavedSearch(savedSearch, fromContextMenu);
|
||||||
}
|
}
|
||||||
|
|
||||||
copyToClipboard(savedSearch: SavedSearch): void {
|
copyToClipboard(savedSearch: SavedSearch): void {
|
||||||
|
|||||||
Reference in New Issue
Block a user