[ACS-3757] returning focus to element from which they were opened (#8034)

* ACS-3757 Focus first focusable element in modals and allow to autofocus specific element after modal closing

* ACS-3757 Added possibility for autofocus after closing some modals, marking datatable row as source of context menu, fixing tests

* ACS-3757 Run lint

* ACS-3757 Updated documentation

* ACS-3757 Added unit tests

* ACS-3757 Replaced toHaveBeenCalledWith with toHaveBeenCalled and removed testing all falsy
This commit is contained in:
AleksanderSklorz
2022-12-13 11:55:46 +01:00
committed by GitHub
parent ef278bde79
commit c1cffa9cfb
12 changed files with 164 additions and 11 deletions

View File

@@ -0,0 +1,77 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { setupTestBed } from '@alfresco/adf-core';
import { TranslateModule } from '@ngx-translate/core';
import { ContentTestingModule } from '../../testing/content.testing.module';
import { DialogAspectListService } from '@alfresco/adf-content-services';
import { TestBed } from '@angular/core/testing';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { Subject } from 'rxjs';
describe('DialogAspectListService', () => {
let dialogAspectListService: DialogAspectListService;
let dialog: MatDialog;
setupTestBed({
imports: [
TranslateModule.forRoot(),
ContentTestingModule
]
});
beforeEach(() => {
dialogAspectListService = TestBed.inject(DialogAspectListService);
dialog = TestBed.inject(MatDialog);
});
describe('openAspectListDialog', () => {
const elementToFocusSelector = 'button';
it('should focus element indicated by passed selector after closing modal', () => {
const afterClosed$ = new Subject<void>();
spyOn(dialog, 'open').and.returnValue({
afterClosed: () => afterClosed$.asObservable()
} as MatDialogRef<any>);
const elementToFocus = document.createElement(elementToFocusSelector);
spyOn(elementToFocus, 'focus');
spyOn(document, 'querySelector').withArgs(elementToFocusSelector).and.returnValue(elementToFocus);
dialogAspectListService.openAspectListDialog('some-node-id', elementToFocusSelector);
afterClosed$.next();
expect(elementToFocus.focus).toHaveBeenCalled();
});
it('should not focus element indicated by passed selector if modal is not closed', () => {
const elementToFocus = document.createElement(elementToFocusSelector);
spyOn(elementToFocus, 'focus');
spyOn(document, 'querySelector').withArgs(elementToFocusSelector).and.returnValue(elementToFocus);
dialogAspectListService.openAspectListDialog('some-node-id', elementToFocusSelector);
expect(elementToFocus.focus).not.toHaveBeenCalled();
});
it('should not looking for element to focus if passed selector is empty string', () => {
const afterClosed$ = new Subject<void>();
spyOn(dialog, 'open').and.returnValue({
afterClosed: () => afterClosed$.asObservable()
} as MatDialogRef<any>);
spyOn(document, 'querySelector');
dialogAspectListService.openAspectListDialog('some-node-id', '');
afterClosed$.next();
expect(document.querySelector).not.toHaveBeenCalled();
});
});
});

View File

@@ -30,7 +30,7 @@ export class DialogAspectListService {
constructor(private dialog: MatDialog, private overlayContainer: OverlayContainer) {
}
openAspectListDialog(nodeId?: string): Observable<string[]> {
openAspectListDialog(nodeId?: string, selectorAutoFocusedOnClose?: string): Observable<string[]> {
const select = new Subject<string[]>();
select.subscribe({
complete: this.close.bind(this)
@@ -44,18 +44,19 @@ export class DialogAspectListService {
nodeId
};
this.openDialog(data, 'adf-aspect-list-dialog', '750px');
this.openDialog(data, 'adf-aspect-list-dialog', '750px', selectorAutoFocusedOnClose);
return select;
}
private openDialog(data: AspectListDialogComponentData, panelClass: string, width: string) {
private openDialog(data: AspectListDialogComponentData, panelClass: string, width: string,
selectorAutoFocusedOnClose?: string) {
this.dialog.open(AspectListDialogComponent, {
data,
panelClass,
width,
role: 'dialog',
disableClose: true
});
}).afterClosed().subscribe(() => DialogAspectListService.focusOnClose(selectorAutoFocusedOnClose));
this.overlayContainer.getContainerElement().setAttribute('role', 'main');
}
@@ -63,4 +64,10 @@ export class DialogAspectListService {
this.dialog.closeAll();
this.overlayContainer.getContainerElement().setAttribute('role', 'region');
}
private static focusOnClose(selectorAutoFocusedOnClose: string): void {
if (selectorAutoFocusedOnClose) {
document.querySelector<HTMLElement>(selectorAutoFocusedOnClose).focus();
}
}
}

View File

@@ -19,7 +19,7 @@ import { MinimalNode } from '@alfresco/js-api';
import { TestBed } from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core';
import { AlfrescoApiService, CardViewUpdateService, NodesApiService, setupTestBed } from '@alfresco/adf-core';
import { of } from 'rxjs';
import { EMPTY, of } from 'rxjs';
import { ContentTestingModule } from '../../testing/content.testing.module';
import { NodeAspectService } from './node-aspect.service';
import { DialogAspectListService } from './dialog-aspect-list.service';
@@ -47,11 +47,26 @@ describe('NodeAspectService', () => {
cardViewUpdateService = TestBed.inject(CardViewUpdateService);
});
it('should call openAspectListDialog with correct parameters when selectorAutoFocusedOnClose is passed', () => {
spyOn(dialogAspectListService, 'openAspectListDialog').and.returnValue(EMPTY);
const nodeId = 'some node id';
const selector = 'some-selector';
nodeAspectService.updateNodeAspects(nodeId, selector);
expect(dialogAspectListService.openAspectListDialog).toHaveBeenCalledWith(nodeId, selector);
});
it('should call openAspectListDialog with correct parameters when selectorAutoFocusedOnClose is not passed', () => {
spyOn(dialogAspectListService, 'openAspectListDialog').and.returnValue(EMPTY);
const nodeId = 'some node id';
nodeAspectService.updateNodeAspects(nodeId);
expect(dialogAspectListService.openAspectListDialog).toHaveBeenCalledWith(nodeId, undefined);
});
it('should open the aspect list dialog', () => {
spyOn(dialogAspectListService, 'openAspectListDialog').and.returnValue(of([]));
spyOn(nodeApiService, 'updateNode').and.returnValue(of(null));
nodeAspectService.updateNodeAspects('fake-node-id');
expect(dialogAspectListService.openAspectListDialog).toHaveBeenCalledWith('fake-node-id');
expect(dialogAspectListService.openAspectListDialog).toHaveBeenCalledWith('fake-node-id', undefined);
});
it('should update the node when the aspect dialog apply the changes', () => {

View File

@@ -30,8 +30,8 @@ export class NodeAspectService {
private cardViewUpdateService: CardViewUpdateService) {
}
updateNodeAspects(nodeId: string) {
this.dialogAspectListService.openAspectListDialog(nodeId).subscribe((aspectList) => {
updateNodeAspects(nodeId: string, selectorAutoFocusedOnClose?: string) {
this.dialogAspectListService.openAspectListDialog(nodeId, selectorAutoFocusedOnClose).subscribe((aspectList) => {
this.nodesApiService.updateNode(nodeId, { aspectNames: [...aspectList] }).subscribe((updatedNode) => {
this.alfrescoApiService.nodeUpdated.next(updatedNode);
this.cardViewUpdateService.updateNodeAspect(updatedNode);

View File

@@ -6,6 +6,7 @@
type="text"
placeholder="{{'NODE_SELECTOR.SEARCH' | translate}}"
[value]="searchTerm"
adf-auto-focus
data-automation-id="content-node-selector-search-input">
<mat-icon *ngIf="searchTerm.length > 0"

View File

@@ -284,6 +284,21 @@ describe('NewVersionUploaderService', () => {
});
});
it('should focus element indicated by passed selector after closing modal', (done) => {
dialogRefSpyObj.componentInstance.dialogAction = new BehaviorSubject<VersionManagerUploadData>(mockNewVersionUploaderData);
const afterClosed$ = new BehaviorSubject<void>(undefined);
dialogRefSpyObj.afterClosed = () => afterClosed$;
const elementToFocusSelector = 'button';
const elementToFocus = document.createElement(elementToFocusSelector);
spyOn(elementToFocus, 'focus').and.callFake(() => {
expect(elementToFocus.focus).toHaveBeenCalled();
done();
});
spyOn(document, 'querySelector').and.returnValue(elementToFocus);
service.openUploadNewVersionDialog(mockNewVersionUploaderDialogData, undefined, elementToFocusSelector)
.subscribe();
});
});
});

View File

@@ -51,9 +51,10 @@ export class NewVersionUploaderService {
*
* @param data data to pass to MatDialog
* @param config allow to override default MatDialogConfig
* @param selectorAutoFocusedOnClose element's selector which should be autofocused after closing modal
* @returns an Observable represents the triggered dialog action or an error in case of an error condition
*/
openUploadNewVersionDialog(data: NewVersionUploaderDialogData, config?: MatDialogConfig) {
openUploadNewVersionDialog(data: NewVersionUploaderDialogData, config?: MatDialogConfig, selectorAutoFocusedOnClose?: string) {
const { file, node, showVersionsOnly } = data;
const showComments = true;
const allowDownload = true;
@@ -76,6 +77,7 @@ export class NewVersionUploaderService {
});
dialogRef.afterClosed().subscribe(() => {
this.overlayContainer.getContainerElement().setAttribute('role', 'region');
NewVersionUploaderService.focusOnClose(selectorAutoFocusedOnClose);
});
this.overlayContainer.getContainerElement().setAttribute('role', 'main');
});
@@ -90,4 +92,10 @@ export class NewVersionUploaderService {
const dialogCssClass = 'adf-new-version-uploader-dialog';
return [dialogCssClass, `${dialogCssClass}-${showVersionsOnly ? 'list' : 'upload'}`];
}
private static focusOnClose(selectorAutoFocusedOnClose: string): void {
if (selectorAutoFocusedOnClose) {
document.querySelector<HTMLElement>(selectorAutoFocusedOnClose).focus();
}
}
}