mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-07-24 17:32:15 +00:00
[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:
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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', () => {
|
||||
|
@@ -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);
|
||||
|
@@ -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"
|
||||
|
@@ -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();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user