diff --git a/app/src/app/content-plugin/components/common/toggle-shared/toggle-shared.component.html b/app/src/app/content-plugin/components/common/toggle-shared/toggle-shared.component.html index da886269b..8721a52cf 100644 --- a/app/src/app/content-plugin/components/common/toggle-shared/toggle-shared.component.html +++ b/app/src/app/content-plugin/components/common/toggle-shared/toggle-shared.component.html @@ -1,6 +1,6 @@ - diff --git a/app/src/app/content-plugin/components/common/toggle-shared/toggle-shared.component.ts b/app/src/app/content-plugin/components/common/toggle-shared/toggle-shared.component.ts index ad8f3938a..cffb9a992 100644 --- a/app/src/app/content-plugin/components/common/toggle-shared/toggle-shared.component.ts +++ b/app/src/app/content-plugin/components/common/toggle-shared/toggle-shared.component.ts @@ -34,7 +34,10 @@ import { AppStore, ShareNodeAction, getAppSelection } from '@alfresco/aca-shared templateUrl: './toggle-shared.component.html' }) export class ToggleSharedComponent implements OnInit { - @Input() data: { iconButton?: string }; + @Input() + data: { + iconButton?: string; + }; selection$: Observable; @@ -53,8 +56,13 @@ export class ToggleSharedComponent implements OnInit { return selection.first && selection.first.entry && selection.first.entry.properties && !!selection.first.entry.properties['qshare:sharedId']; } - editSharedNode(selection: SelectionState) { - this.store.dispatch(new ShareNodeAction(selection.first)); + editSharedNode(selection: SelectionState, focusedElementOnCloseSelector: string) { + this.store.dispatch( + new ShareNodeAction({ + ...selection.first, + focusedElementOnCloseSelector + }) + ); } getLabel(selection: SelectionState): string { diff --git a/app/src/app/content-plugin/components/context-menu/context-menu.component.html b/app/src/app/content-plugin/components/context-menu/context-menu.component.html index 0c6c5c564..34ad3c90d 100644 --- a/app/src/app/content-plugin/components/context-menu/context-menu.component.html +++ b/app/src/app/content-plugin/components/context-menu/context-menu.component.html @@ -4,7 +4,7 @@ - diff --git a/app/src/app/content-plugin/components/context-menu/context-menu.component.spec.ts b/app/src/app/content-plugin/components/context-menu/context-menu.component.spec.ts index 1102f28ac..85f5898dc 100644 --- a/app/src/app/content-plugin/components/context-menu/context-menu.component.spec.ts +++ b/app/src/app/content-plugin/components/context-menu/context-menu.component.spec.ts @@ -96,11 +96,13 @@ describe('ContextMenuComponent', () => { expect(contextMenuElements[0].querySelector('span').innerText).toBe(contextItem.title); }); - it('should run action with provided action id', () => { + it('should run action with provided action id and correct payload', () => { spyOn(extensionsService, 'runActionById'); - component.runAction(contextItem.actions.click); + component.runAction(contextItem); - expect(extensionsService.runActionById).toHaveBeenCalledWith(contextItem.actions.click); + expect(extensionsService.runActionById).toHaveBeenCalledWith(contextItem.actions.click, { + focusedElementOnCloseSelector: '.adf-context-menu-source' + }); }); }); diff --git a/app/src/app/content-plugin/components/context-menu/context-menu.component.ts b/app/src/app/content-plugin/components/context-menu/context-menu.component.ts index 38a33a6b9..29e4a1116 100644 --- a/app/src/app/content-plugin/components/context-menu/context-menu.component.ts +++ b/app/src/app/content-plugin/components/context-menu/context-menu.component.ts @@ -69,8 +69,10 @@ export class ContextMenuComponent implements OnInit, OnDestroy, AfterViewInit { } } - runAction(actionId: string) { - this.extensions.runActionById(actionId); + runAction(contentActionRef: ContentActionRef) { + this.extensions.runActionById(contentActionRef.actions.click, { + focusedElementOnCloseSelector: '.adf-context-menu-source' + }); } ngOnDestroy() { diff --git a/app/src/app/content-plugin/services/content-management.service.spec.ts b/app/src/app/content-plugin/services/content-management.service.spec.ts index 4c1423b57..72705cc8b 100644 --- a/app/src/app/content-plugin/services/content-management.service.spec.ts +++ b/app/src/app/content-plugin/services/content-management.service.spec.ts @@ -24,7 +24,7 @@ */ import { TestBed, fakeAsync, tick, flush } from '@angular/core/testing'; -import { of, throwError, Subject } from 'rxjs'; +import { of, throwError, Subject, BehaviorSubject, EMPTY } from 'rxjs'; import { Actions, ofType, EffectsModule } from '@ngrx/effects'; import { AppStore, @@ -56,7 +56,7 @@ import { NodeActionsService } from './node-actions.service'; import { TranslationService, AlfrescoApiService, FileModel, NotificationService } from '@alfresco/adf-core'; import { MatDialog, MatDialogRef } from '@angular/material/dialog'; import { MatSnackBarRef, SimpleSnackBar } from '@angular/material/snack-bar'; -import { NodeEntry, Node, VersionPaging } from '@alfresco/js-api'; +import { NodeEntry, Node, VersionPaging, MinimalNodeEntity } from '@alfresco/js-api'; import { NewVersionUploaderDataAction, NewVersionUploaderService, NodeAspectService, ViewVersion } from '@alfresco/adf-content-services'; describe('ContentManagementService', () => { @@ -1410,30 +1410,79 @@ describe('ContentManagementService', () => { })); it('should update node selection after dialog is closed', fakeAsync(() => { + spyOn(document, 'querySelector').and.returnValue(document.createElement('button')); const node = { entry: { id: '1', name: 'name1' } } as NodeEntry; spyOn(store, 'dispatch').and.callThrough(); spyOn(dialog, 'open').and.returnValue({ afterClosed: () => of(null) } as MatDialogRef); - - store.dispatch(new ShareNodeAction(node)); - - expect(store.dispatch['calls'].argsFor(1)[0]).toEqual(new SetSelectedNodesAction([node])); + const payload = { + ...node, + ...{ + focusedElementOnCloseSelector: 'some-selector' + } + }; + store.dispatch(new ShareNodeAction(payload)); + expect(store.dispatch['calls'].argsFor(1)[0]).toEqual(new SetSelectedNodesAction([payload])); })); it('should emit event when node is un-shared', fakeAsync(() => { + spyOn(document, 'querySelector').and.returnValue(document.createElement('button')); const node = { entry: { id: '1', name: 'name1' } } as NodeEntry; spyOn(appHookService.linksUnshared, 'next').and.callThrough(); spyOn(dialog, 'open').and.returnValue({ afterClosed: () => of(node) } as MatDialogRef); - - store.dispatch(new ShareNodeAction(node)); + store.dispatch( + new ShareNodeAction({ + ...node, + ...{ + focusedElementOnCloseSelector: 'some-selector' + } + }) + ); tick(); flush(); expect(appHookService.linksUnshared.next).toHaveBeenCalled(); })); + + it('should focus element indicated by passed selector after closing modal', () => { + const elementToFocusSelector = 'button'; + const afterClosed$ = new Subject(); + spyOn(dialog, 'open').and.returnValue({ + afterClosed: () => afterClosed$.asObservable() + } as MatDialogRef); + const elementToFocus = document.createElement(elementToFocusSelector); + spyOn(elementToFocus, 'focus'); + spyOn(document, 'querySelector').withArgs(elementToFocusSelector).and.returnValue(elementToFocus); + spyOn(store, 'select').and.returnValue(new BehaviorSubject('')); + contentManagementService.shareNode( + { + entry: {} + }, + elementToFocusSelector + ); + afterClosed$.next(); + expect(elementToFocus.focus).toHaveBeenCalled(); + }); + + it('should not looking for element to focus if passed selector is empty string', () => { + const afterClosed$ = new Subject(); + spyOn(dialog, 'open').and.returnValue({ + afterClosed: () => afterClosed$.asObservable() + } as MatDialogRef); + spyOn(document, 'querySelector'); + spyOn(store, 'select').and.returnValue(new BehaviorSubject('')); + contentManagementService.shareNode( + { + entry: {} + }, + '' + ); + afterClosed$.next(); + expect(document.querySelector).not.toHaveBeenCalled(); + }); }); describe('Unlock Node', () => { @@ -1544,8 +1593,10 @@ describe('ContentManagementService', () => { showVersionsOnly: true, title: 'VERSION.DIALOG.TITLE' }; - contentManagementService.manageVersions(fakeNodeIsFile); + const elementToFocusSelector = 'some-selector'; + contentManagementService.manageVersions(fakeNodeIsFile, elementToFocusSelector); expect(spyOnOpenUploadNewVersionDialog['calls'].argsFor(0)[0]).toEqual(expectedArgument); + expect(spyOnOpenUploadNewVersionDialog['calls'].argsFor(0)[2]).toEqual(elementToFocusSelector); }); it('should dispatch ReloadDocumentListAction if dialog emit refresh action', () => { @@ -1615,6 +1666,37 @@ describe('ContentManagementService', () => { expect(alfrescoApiService.nodeUpdated.next).toHaveBeenCalledWith(newNode); })); + + it('should focus element indicated by passed selector after closing modal', () => { + const elementToFocusSelector = 'button'; + const afterClosed$ = new Subject(); + spyOn(dialog, 'open').and.returnValue({ + afterClosed: () => afterClosed$.asObservable(), + componentInstance: { + error: EMPTY + } + } as MatDialogRef); + const elementToFocus = document.createElement(elementToFocusSelector); + spyOn(elementToFocus, 'focus'); + spyOn(document, 'querySelector').withArgs(elementToFocusSelector).and.returnValue(elementToFocus); + contentManagementService.editFolder({} as MinimalNodeEntity, elementToFocusSelector); + afterClosed$.next(); + expect(elementToFocus.focus).toHaveBeenCalled(); + }); + + it('should not looking for element to focus if passed selector is empty string', () => { + const afterClosed$ = new Subject(); + spyOn(dialog, 'open').and.returnValue({ + afterClosed: () => afterClosed$.asObservable(), + componentInstance: { + error: EMPTY + } + } as MatDialogRef); + spyOn(document, 'querySelector'); + contentManagementService.editFolder({} as MinimalNodeEntity, ''); + afterClosed$.next(); + expect(document.querySelector).not.toHaveBeenCalled(); + }); }); describe('aspect list dialog', () => { @@ -1633,21 +1715,50 @@ describe('ContentManagementService', () => { createdAt: null, createdByUser: null }; + const elementToFocusSelector = 'some-selector'; spyOn(contentApi, 'getNodeInfo').and.returnValue(of(responseNode)); - contentManagementService.manageAspects(fakeNode); + contentManagementService.manageAspects(fakeNode, elementToFocusSelector); - expect(nodeAspectService.updateNodeAspects).toHaveBeenCalledWith('real-node-ghostbuster'); + expect(nodeAspectService.updateNodeAspects).toHaveBeenCalledWith('real-node-ghostbuster', elementToFocusSelector); }); it('should open dialog for managing the aspects', () => { spyOn(nodeAspectService, 'updateNodeAspects').and.stub(); const fakeNode = { entry: { id: 'fake-node-id' } }; + const elementToFocusSelector = 'some-selector'; - contentManagementService.manageAspects(fakeNode); + contentManagementService.manageAspects(fakeNode, elementToFocusSelector); - expect(nodeAspectService.updateNodeAspects).toHaveBeenCalledWith('fake-node-id'); + expect(nodeAspectService.updateNodeAspects).toHaveBeenCalledWith('fake-node-id', elementToFocusSelector); + }); + }); + + describe('leaveLibrary', () => { + it('should focus element indicated by passed selector after closing modal', () => { + const elementToFocusSelector = 'button'; + const afterClosed$ = new Subject(); + spyOn(dialog, 'open').and.returnValue({ + afterClosed: () => afterClosed$.asObservable() + } as MatDialogRef); + const elementToFocus = document.createElement(elementToFocusSelector); + spyOn(elementToFocus, 'focus'); + spyOn(document, 'querySelector').withArgs(elementToFocusSelector).and.returnValue(elementToFocus); + contentManagementService.leaveLibrary('', elementToFocusSelector); + afterClosed$.next(); + expect(elementToFocus.focus).toHaveBeenCalled(); + }); + + it('should not looking for element to focus if passed selector is empty string', () => { + const afterClosed$ = new Subject(); + spyOn(dialog, 'open').and.returnValue({ + afterClosed: () => afterClosed$.asObservable() + } as MatDialogRef); + spyOn(document, 'querySelector'); + contentManagementService.leaveLibrary('', ''); + afterClosed$.next(); + expect(document.querySelector).not.toHaveBeenCalled(); }); }); }); diff --git a/app/src/app/content-plugin/services/content-management.service.ts b/app/src/app/content-plugin/services/content-management.service.ts index 4a4db6e6e..75cbc9398 100644 --- a/app/src/app/content-plugin/services/content-management.service.ts +++ b/app/src/app/content-plugin/services/content-management.service.ts @@ -85,6 +85,8 @@ interface RestoredNode { providedIn: 'root' }) export class ContentManagementService { + private readonly createMenuButtonSelector = 'app-create-menu button'; + constructor( private alfrescoApiService: AlfrescoApiService, private store: Store, @@ -128,32 +130,32 @@ export class ContentManagementService { } } - manageVersions(node: any) { + manageVersions(node: any, focusedElementOnCloseSelector?: string) { if (node && node.entry) { // shared and favorite const id = node.entry.nodeId || (node as any).entry.guid; if (id) { this.contentApi.getNodeInfo(id).subscribe((entry) => { - this.openVersionManagerDialog(entry); + this.openVersionManagerDialog(entry, focusedElementOnCloseSelector); }); } else { - this.openVersionManagerDialog(node.entry); + this.openVersionManagerDialog(node.entry, focusedElementOnCloseSelector); } } } - manageAspects(node: any) { + manageAspects(node: any, focusedElementOnCloseSelector?: string) { if (node && node.entry) { // shared and favorite const id = node.entry.nodeId || (node as any).entry.guid; if (id) { this.contentApi.getNodeInfo(id).subscribe((entry) => { - this.openAspectListDialog(entry); + this.openAspectListDialog(entry, focusedElementOnCloseSelector); }); } else { - this.openAspectListDialog(node.entry); + this.openAspectListDialog(node.entry, focusedElementOnCloseSelector); } } } @@ -180,22 +182,22 @@ export class ContentManagementService { }); } - shareNode(node: any): void { + shareNode(node: any, focusedElementOnCloseSelector?: string): void { if (node && node.entry) { // shared and favorite const id = node.entry.nodeId || (node as any).entry.guid; if (id) { this.contentApi.getNodeInfo(id).subscribe((entry) => { - this.openShareLinkDialog({ entry }); + this.openShareLinkDialog({ entry }, focusedElementOnCloseSelector); }); } else { - this.openShareLinkDialog(node); + this.openShareLinkDialog(node, focusedElementOnCloseSelector); } } } - openShareLinkDialog(node) { + openShareLinkDialog(node, focusedElementOnCloseSelector?: string) { this.store .select(getSharedUrl) .pipe(take(1)) @@ -214,6 +216,7 @@ export class ContentManagementService { .subscribe(() => { this.store.dispatch(new SetSelectedNodesAction([node])); this.appHookService.linksUnshared.next(); + this.focusAfterClose(focusedElementOnCloseSelector); }); }); } @@ -237,11 +240,11 @@ export class ContentManagementService { if (node) { this.store.dispatch(new ReloadDocumentListAction()); } - ContentManagementService.focusCreateMenuButton(); + this.focusAfterClose(this.createMenuButtonSelector); }); } - editFolder(folder: MinimalNodeEntity) { + editFolder(folder: MinimalNodeEntity, focusedElementOnCloseSelector?: string) { if (!folder) { return; } @@ -261,6 +264,7 @@ export class ContentManagementService { if (node) { this.alfrescoApiService.nodeUpdated.next(node); } + this.focusAfterClose(focusedElementOnCloseSelector); }); } @@ -278,7 +282,7 @@ export class ContentManagementService { if (node) { this.appHookService.libraryCreated.next(node); } - ContentManagementService.focusCreateMenuButton(); + this.focusAfterClose(this.createMenuButtonSelector); }), map((node: SiteEntry) => { if (node && node.entry && node.entry.guid) { @@ -301,7 +305,7 @@ export class ContentManagementService { ); } - leaveLibrary(siteId: string): void { + leaveLibrary(siteId: string, focusedElementOnCloseSelector?: string): void { const dialogRef = this.dialogRef.open(ConfirmDialogComponent, { data: { title: 'APP.DIALOGS.CONFIRM_LEAVE.TITLE', @@ -324,6 +328,7 @@ export class ContentManagementService { } ); } + this.focusAfterClose(focusedElementOnCloseSelector); }); } @@ -421,8 +426,8 @@ export class ContentManagementService { }); } - copyNodes(nodes: Array) { - zip(this.nodeActionsService.copyNodes(nodes), this.nodeActionsService.contentCopied).subscribe( + copyNodes(nodes: Array, focusedElementOnCloseSelector?: string) { + zip(this.nodeActionsService.copyNodes(nodes, undefined, focusedElementOnCloseSelector), this.nodeActionsService.contentCopied).subscribe( (result) => { const [operationResult, newItems] = result; this.showCopyMessage(operationResult, nodes, newItems); @@ -433,10 +438,10 @@ export class ContentManagementService { ); } - moveNodes(nodes: Array) { + moveNodes(nodes: Array, focusedElementOnCloseSelector?: string) { const permissionForMove = '!'; - zip(this.nodeActionsService.moveNodes(nodes, permissionForMove), this.nodeActionsService.contentMoved).subscribe( + zip(this.nodeActionsService.moveNodes(nodes, permissionForMove, focusedElementOnCloseSelector), this.nodeActionsService.contentMoved).subscribe( (result) => { const [operationResult, moveResponse] = result; this.showMoveMessage(nodes, operationResult, moveResponse); @@ -570,7 +575,7 @@ export class ContentManagementService { ); } - private openVersionManagerDialog(node: any) { + private openVersionManagerDialog(node: any, focusedElementOnCloseSelector?: string) { // workaround Shared if (node.isFile || node.nodeId) { const newVersionUploaderDialogData: NewVersionUploaderDialogData = { @@ -579,21 +584,23 @@ export class ContentManagementService { title: 'VERSION.DIALOG.TITLE' }; this.newVersionUploaderService - .openUploadNewVersionDialog(newVersionUploaderDialogData, { width: '630px', role: 'dialog' }) - .subscribe((newVersionUploaderData: NewVersionUploaderData) => { - switch (newVersionUploaderData.action) { - case NewVersionUploaderDataAction.refresh: - this.store.dispatch(new ReloadDocumentListAction()); - break; - case NewVersionUploaderDataAction.view: - this.store.dispatch( - new ViewNodeVersionAction(node.id, newVersionUploaderData.versionId, { - location: this.router.url - }) - ); - break; - default: - break; + .openUploadNewVersionDialog(newVersionUploaderDialogData, { width: '630px', role: 'dialog' }, focusedElementOnCloseSelector) + .subscribe({ + next: (newVersionUploaderData: NewVersionUploaderData) => { + switch (newVersionUploaderData.action) { + case NewVersionUploaderDataAction.refresh: + this.store.dispatch(new ReloadDocumentListAction()); + break; + case NewVersionUploaderDataAction.view: + this.store.dispatch( + new ViewNodeVersionAction(node.id, newVersionUploaderData.versionId, { + location: this.router.url + }) + ); + break; + default: + break; + } } }); } else { @@ -601,10 +608,10 @@ export class ContentManagementService { } } - private openAspectListDialog(node: any) { + private openAspectListDialog(node: any, focusedElementOnCloseSelector?: string) { // workaround Shared if (node.isFile || node.id) { - this.nodeAspectService.updateNodeAspects(node.id); + this.nodeAspectService.updateNodeAspects(node.id, focusedElementOnCloseSelector); } else { this.store.dispatch(new SnackbarErrorAction('APP.MESSAGES.ERRORS.PERMISSION')); } @@ -1080,7 +1087,9 @@ export class ContentManagementService { .subscribe(() => this.undoMoveNodes(moveResponse, initialParentId)); } - private static focusCreateMenuButton(): void { - document.querySelector('app-create-menu button').focus(); + private focusAfterClose(focusedElementSelector: string): void { + if (focusedElementSelector) { + document.querySelector(focusedElementSelector).focus(); + } } } diff --git a/app/src/app/content-plugin/services/node-actions.service.spec.ts b/app/src/app/content-plugin/services/node-actions.service.spec.ts index 7619944d2..857e18567 100644 --- a/app/src/app/content-plugin/services/node-actions.service.spec.ts +++ b/app/src/app/content-plugin/services/node-actions.service.spec.ts @@ -115,7 +115,9 @@ describe('NodeActionsService', () => { describe('ContentNodeSelector configuration', () => { it('should validate selection when allowableOperation has `create`', () => { - spyOn(dialog, 'open'); + spyOn(dialog, 'open').and.returnValue({ + afterClosed: of + } as MatDialogRef); const contentEntities = [new TestNode(), { entry: { nodeId: '1234' } }]; service.getContentNodeSelection(NodeAction.CHOOSE, contentEntities as NodeEntry[]); @@ -131,7 +133,9 @@ describe('NodeActionsService', () => { }); it('should invalidate selection when allowableOperation does not have `create`', () => { - spyOn(dialog, 'open'); + spyOn(dialog, 'open').and.returnValue({ + afterClosed: of + } as MatDialogRef); const contentEntities = [new TestNode(), { entry: { nodeId: '1234' } }]; service.getContentNodeSelection(NodeAction.CHOOSE, contentEntities as NodeEntry[]); @@ -147,7 +151,9 @@ describe('NodeActionsService', () => { }); it('should invalidate selection if isSite', () => { - spyOn(dialog, 'open'); + spyOn(dialog, 'open').and.returnValue({ + afterClosed: of + } as MatDialogRef); const contentEntities = [new TestNode(), { entry: { nodeId: '1234' } }]; service.getContentNodeSelection(NodeAction.CHOOSE, contentEntities as NodeEntry[]); @@ -164,7 +170,9 @@ describe('NodeActionsService', () => { }); it('should validate selection if not a Site', () => { - spyOn(dialog, 'open'); + spyOn(dialog, 'open').and.returnValue({ + afterClosed: of + } as MatDialogRef); const contentEntities = [new TestNode(), { entry: { nodeId: '1234' } }]; service.getContentNodeSelection(NodeAction.CHOOSE, contentEntities as NodeEntry[]); @@ -280,7 +288,7 @@ describe('NodeActionsService', () => { spyOn(dialog, 'open').and.callFake((_contentNodeSelectorComponent: any, data: any) => { dialogData = data; - return { componentInstance: {} } as MatDialogRef; + return { componentInstance: {}, afterClosed: of } as MatDialogRef; }); service.copyNodes([fileToCopy, folderToCopy]); @@ -335,7 +343,7 @@ describe('NodeActionsService', () => { subject.next([destinationFolder.entry]); expect(spyOnBatchOperation.calls.count()).toEqual(1); - expect(spyOnBatchOperation).toHaveBeenCalledWith(NodeAction.COPY, [fileToCopy, folderToCopy], undefined); + expect(spyOnBatchOperation).toHaveBeenCalledWith(NodeAction.COPY, [fileToCopy, folderToCopy], undefined, undefined); }); it('should use the custom data object with custom rowFilter & imageResolver & title with destination picker', () => { @@ -346,12 +354,12 @@ describe('NodeActionsService', () => { let dialogData = null; const spyOnDialog = spyOn(dialog, 'open').and.callFake((_contentNodeSelectorComponent: any, data: any) => { dialogData = data; - return { componentInstance: {} } as MatDialogRef; + return { componentInstance: {}, afterClosed: of } as MatDialogRef; }); service.copyNodes([fileToCopy, folderToCopy]); - expect(spyOnBatchOperation).toHaveBeenCalledWith(NodeAction.COPY, [fileToCopy, folderToCopy], undefined); + expect(spyOnBatchOperation).toHaveBeenCalledWith(NodeAction.COPY, [fileToCopy, folderToCopy], undefined, undefined); expect(spyOnDestinationPicker.calls.count()).toEqual(1); expect(spyOnDialog.calls.count()).toEqual(1); @@ -388,7 +396,7 @@ describe('NodeActionsService', () => { let dialogData: any; spyOn(dialog, 'open').and.callFake((_contentNodeSelectorComponent: any, data: any) => { dialogData = data; - return { componentInstance: {} } as MatDialogRef; + return { componentInstance: {}, afterClosed: of } as MatDialogRef; }); service.copyNodes([{ entry: { id: 'entry-id', name: 'entry-name' } }]); @@ -410,7 +418,7 @@ describe('NodeActionsService', () => { let dialogData = null; spyOn(dialog, 'open').and.callFake((_contentNodeSelectorComponent: any, data: any) => { dialogData = data; - return { componentInstance: {} } as MatDialogRef; + return { componentInstance: {}, afterClosed: of } as MatDialogRef; }); service.copyNodes([{ entry: { id: 'entry-id' } }]); @@ -736,7 +744,7 @@ describe('NodeActionsService', () => { service.moveNodes([fileToMove, folderToMove], permissionToMove); subject.next([destinationFolder.entry]); - expect(spyOnBatchOperation).toHaveBeenCalledWith(NodeAction.MOVE, [fileToMove, folderToMove], permissionToMove); + expect(spyOnBatchOperation).toHaveBeenCalledWith(NodeAction.MOVE, [fileToMove, folderToMove], permissionToMove, undefined); expect(spyOnDestinationPicker).toHaveBeenCalled(); }); @@ -749,7 +757,7 @@ describe('NodeActionsService', () => { service.moveNodes([fileToMove, folderToMove], permissionToMove); subject.next([destinationFolder.entry]); - expect(spyOnBatchOperation).toHaveBeenCalledWith(NodeAction.MOVE, [fileToMove, folderToMove], permissionToMove); + expect(spyOnBatchOperation).toHaveBeenCalledWith(NodeAction.MOVE, [fileToMove, folderToMove], permissionToMove, undefined); expect(spyOnDestinationPicker).not.toHaveBeenCalled(); }); diff --git a/app/src/app/content-plugin/services/node-actions.service.ts b/app/src/app/content-plugin/services/node-actions.service.ts index 868335d95..aeac3a76c 100644 --- a/app/src/app/content-plugin/services/node-actions.service.ts +++ b/app/src/app/content-plugin/services/node-actions.service.ts @@ -78,9 +78,10 @@ export class NodeActionsService { * * @param contentEntities nodes to copy * @param permission permission which is needed to apply the action + * @param focusedElementOnCloseSelector element's selector which should be autofocused after closing modal */ - copyNodes(contentEntities: any[], permission?: string): Subject { - return this.doBatchOperation(NodeAction.COPY, contentEntities, permission); + copyNodes(contentEntities: any[], permission?: string, focusedElementOnCloseSelector?: string): Subject { + return this.doBatchOperation(NodeAction.COPY, contentEntities, permission, focusedElementOnCloseSelector); } /** @@ -88,9 +89,10 @@ export class NodeActionsService { * * @param contentEntities nodes to move * @param permission permission which is needed to apply the action + * @param focusedElementOnCloseSelector element's selector which should be autofocused after closing modal */ - moveNodes(contentEntities: any[], permission?: string): Subject { - return this.doBatchOperation(NodeAction.MOVE, contentEntities, permission); + moveNodes(contentEntities: any[], permission?: string, focusedElementOnCloseSelector?: string): Subject { + return this.doBatchOperation(NodeAction.MOVE, contentEntities, permission, focusedElementOnCloseSelector); } /** @@ -99,14 +101,15 @@ export class NodeActionsService { * @param action the action to perform (copy|move) * @param contentEntities the contentEntities which have to have the action performed on * @param permission permission which is needed to apply the action + * @param focusedElementOnCloseSelector element's selector which should be autofocused after closing modal */ - doBatchOperation(action: BatchOperationType, contentEntities: any[], permission?: string): Subject { + doBatchOperation(action: BatchOperationType, contentEntities: any[], permission?: string, focusedElementOnCloseSelector?: string): Subject { const observable: Subject = new Subject(); if (!this.isEntryEntitiesArray(contentEntities)) { observable.error(new Error(JSON.stringify({ error: { statusCode: 400 } }))); } else if (this.checkPermission(action, contentEntities, permission)) { - const destinationSelection = this.getContentNodeSelection(action, contentEntities); + const destinationSelection = this.getContentNodeSelection(action, contentEntities, focusedElementOnCloseSelector); destinationSelection.subscribe((selections: MinimalNodeEntryEntity[]) => { const contentEntry = contentEntities[0].entry; // Check if there's nodeId for Shared Files @@ -171,7 +174,11 @@ export class NodeActionsService { return entryParentId; } - getContentNodeSelection(action: NodeAction, contentEntities: MinimalNodeEntity[]): Subject { + getContentNodeSelection( + action: NodeAction, + contentEntities: MinimalNodeEntity[], + focusedElementOnCloseSelector?: string + ): Subject { const currentParentFolderId = this.getEntryParentId(contentEntities[0].entry); const customDropdown = new SitePaging({ @@ -211,12 +218,15 @@ export class NodeActionsService { excludeSiteContent: ContentNodeDialogService.nonDocumentSiteContent }; - this.dialog.open(ContentNodeSelectorComponent, { - data, - panelClass: 'adf-content-node-selector-dialog', - width: '630px', - role: 'dialog' - }); + this.dialog + .open(ContentNodeSelectorComponent, { + data, + panelClass: 'adf-content-node-selector-dialog', + width: '630px', + role: 'dialog' + }) + .afterClosed() + .subscribe(() => this.focusAfterClose(focusedElementOnCloseSelector)); data.select.subscribe({ complete: this.close.bind(this) @@ -687,4 +697,10 @@ export class NodeActionsService { return moveStatus; } } + + private focusAfterClose(focusedElementSelector: string): void { + if (focusedElementSelector) { + document.querySelector(focusedElementSelector).focus(); + } + } } diff --git a/app/src/app/content-plugin/store/effects/download.effects.spec.ts b/app/src/app/content-plugin/store/effects/download.effects.spec.ts new file mode 100644 index 000000000..df81df9a4 --- /dev/null +++ b/app/src/app/content-plugin/store/effects/download.effects.spec.ts @@ -0,0 +1,115 @@ +/*! + * @license + * Alfresco Example Content Application + * + * Copyright (C) 2005 - 2020 Alfresco Software Limited + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ + +import { TestBed } from '@angular/core/testing'; +import { AppTestingModule } from '../../testing/app-testing.module'; +import { EffectsModule } from '@ngrx/effects'; +import { Store } from '@ngrx/store'; +import { BehaviorSubject, Subject } from 'rxjs'; +import { MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { DownloadNodesAction } from '@alfresco/aca-shared/store'; +import { SelectionState } from '@alfresco/adf-extensions'; +import { VersionEntry } from '@alfresco/js-api'; +import { DownloadEffects } from './download.effects'; + +describe('DownloadEffects', () => { + let store: Store; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [AppTestingModule, EffectsModule.forRoot([DownloadEffects])] + }); + store = TestBed.inject(Store); + }); + + describe('downloadNode$', () => { + let dialog: MatDialog; + + beforeEach(() => { + dialog = TestBed.inject(MatDialog); + }); + + it('should focus element indicated by passed selector after closing modal', () => { + const elementToFocusSelector = 'button'; + const afterClosed$ = new Subject(); + spyOn(dialog, 'open').and.returnValue({ + afterClosed: () => afterClosed$.asObservable() + } as MatDialogRef); + const elementToFocus = document.createElement(elementToFocusSelector); + spyOn(elementToFocus, 'focus'); + spyOn(document, 'querySelector').withArgs(elementToFocusSelector).and.returnValue(elementToFocus); + spyOn(store, 'select').and.returnValues( + new BehaviorSubject({ + isEmpty: false, + nodes: [ + { + entry: { + id: 'someId', + isFolder: true + } + } + ] + } as SelectionState), + new BehaviorSubject(null) + ); + store.dispatch( + new DownloadNodesAction({ + focusedElementOnCloseSelector: elementToFocusSelector + }) + ); + afterClosed$.next(); + expect(elementToFocus.focus).toHaveBeenCalled(); + }); + + it('should not looking for element to focus if passed selector is empty string', () => { + const afterClosed$ = new Subject(); + spyOn(dialog, 'open').and.returnValue({ + afterClosed: () => afterClosed$.asObservable() + } as MatDialogRef); + spyOn(document, 'querySelector'); + spyOn(store, 'select').and.returnValues( + new BehaviorSubject({ + isEmpty: false, + nodes: [ + { + entry: { + id: 'someId', + isFolder: true + } + } + ] + } as SelectionState), + new BehaviorSubject(null) + ); + store.dispatch( + new DownloadNodesAction({ + focusedElementOnCloseSelector: '' + }) + ); + afterClosed$.next(); + expect(document.querySelector).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/app/src/app/content-plugin/store/effects/download.effects.ts b/app/src/app/content-plugin/store/effects/download.effects.ts index ebbcba9e2..e7bcb0846 100644 --- a/app/src/app/content-plugin/store/effects/download.effects.ts +++ b/app/src/app/content-plugin/store/effects/download.effects.ts @@ -31,7 +31,7 @@ import { MatDialog } from '@angular/material/dialog'; import { Actions, ofType, createEffect } from '@ngrx/effects'; import { Store } from '@ngrx/store'; import { map, take } from 'rxjs/operators'; -import { ContentApiService } from '@alfresco/aca-shared'; +import { ContentApiService, ModalConfiguration } from '@alfresco/aca-shared'; import { ContentUrlService } from '../../services/content-url.service'; @Injectable() @@ -49,7 +49,7 @@ export class DownloadEffects { this.actions$.pipe( ofType(NodeActionTypes.Download), map((action) => { - if (action.payload && action.payload.length > 0) { + if (Array.isArray(action.payload) && action.payload?.length > 0) { this.downloadNodes(action.payload); } else { this.store @@ -64,7 +64,7 @@ export class DownloadEffects { if (version) { this.downloadFileVersion(selection.nodes[0].entry, version.entry); } else { - this.downloadNodes(selection.nodes); + this.downloadNodes(selection.nodes, (action.payload as ModalConfiguration)?.focusedElementOnCloseSelector); } }); } @@ -75,7 +75,7 @@ export class DownloadEffects { { dispatch: false } ); - private downloadNodes(toDownload: Array) { + private downloadNodes(toDownload: Array, focusedElementSelector?: string) { const nodes = toDownload.map((node) => { const { id, nodeId, name, isFile, isFolder } = node.entry as any; @@ -92,16 +92,16 @@ export class DownloadEffects { } if (nodes.length === 1) { - this.downloadNode(nodes[0]); + this.downloadNode(nodes[0], focusedElementSelector); } else { - this.downloadZip(nodes); + this.downloadZip(nodes, focusedElementSelector); } } - private downloadNode(node: NodeInfo) { + private downloadNode(node: NodeInfo, focusedElementSelector?: string) { if (node) { if (node.isFolder) { - this.downloadZip([node]); + this.downloadZip([node], focusedElementSelector); } else { this.downloadFile(node); } @@ -128,17 +128,20 @@ export class DownloadEffects { } } - private downloadZip(nodes: Array) { + private downloadZip(nodes: Array, focusedElementSelector?: string) { if (nodes && nodes.length > 0) { const nodeIds = nodes.map((node) => node.id); - this.dialog.open(DownloadZipDialogComponent, { - width: '600px', - disableClose: true, - data: { - nodeIds - } - }); + this.dialog + .open(DownloadZipDialogComponent, { + width: '600px', + disableClose: true, + data: { + nodeIds + } + }) + .afterClosed() + .subscribe(() => this.focusAfterClose(focusedElementSelector)); } } @@ -159,4 +162,10 @@ export class DownloadEffects { private get isSharedLinkPreview() { return location.href.includes('/preview/s/'); } + + private focusAfterClose(focusedElementSelector: string): void { + if (focusedElementSelector) { + document.querySelector(focusedElementSelector).focus(); + } + } } diff --git a/app/src/app/content-plugin/store/effects/library.effects.ts b/app/src/app/content-plugin/store/effects/library.effects.ts index 509e3a60d..ad12e7732 100644 --- a/app/src/app/content-plugin/store/effects/library.effects.ts +++ b/app/src/app/content-plugin/store/effects/library.effects.ts @@ -39,7 +39,7 @@ import { Injectable } from '@angular/core'; import { Actions, ofType, createEffect } from '@ngrx/effects'; import { Store } from '@ngrx/store'; import { map, mergeMap, take } from 'rxjs/operators'; -import { ContentApiService } from '@alfresco/aca-shared'; +import { ContentApiService, ModalConfiguration } from '@alfresco/aca-shared'; import { ContentManagementService } from '../../services/content-management.service'; @Injectable() @@ -56,7 +56,7 @@ export class LibraryEffects { this.actions$.pipe( ofType(LibraryActionTypes.Delete), map((action) => { - if (action.payload) { + if (typeof action?.payload === 'string') { this.content.deleteLibrary(action.payload); } else { this.store @@ -78,7 +78,7 @@ export class LibraryEffects { this.actions$.pipe( ofType(LibraryActionTypes.Leave), map((action) => { - if (action.payload) { + if (typeof action.payload === 'string') { this.content.leaveLibrary(action.payload); } else { this.store @@ -86,7 +86,7 @@ export class LibraryEffects { .pipe(take(1)) .subscribe((selection) => { if (selection && selection.library) { - this.content.leaveLibrary(selection.library.entry.id); + this.content.leaveLibrary(selection.library.entry.id, (action.payload as ModalConfiguration)?.focusedElementOnCloseSelector); } }); } diff --git a/app/src/app/content-plugin/store/effects/node.effects.spec.ts b/app/src/app/content-plugin/store/effects/node.effects.spec.ts index b9f5094c5..fb251955f 100644 --- a/app/src/app/content-plugin/store/effects/node.effects.spec.ts +++ b/app/src/app/content-plugin/store/effects/node.effects.spec.ts @@ -79,10 +79,12 @@ describe('NodeEffects', () => { it('should share node from payload', () => { spyOn(contentService, 'shareNode').and.stub(); - const node: any = {}; + const node: any = { + entry: {} + }; store.dispatch(new ShareNodeAction(node)); - expect(contentService.shareNode).toHaveBeenCalledWith(node); + expect(contentService.shareNode).toHaveBeenCalledWith(node, undefined); }); it('should share node from active selection', fakeAsync(() => { @@ -94,7 +96,7 @@ describe('NodeEffects', () => { tick(100); store.dispatch(new ShareNodeAction(null)); - expect(contentService.shareNode).toHaveBeenCalledWith(node); + expect(contentService.shareNode).toHaveBeenCalledWith(node, undefined); })); it('should do nothing if invoking share with no data', () => { @@ -300,7 +302,7 @@ describe('NodeEffects', () => { tick(100); store.dispatch(new EditFolderAction(null)); - expect(contentService.editFolder).toHaveBeenCalledWith(currentFolder); + expect(contentService.editFolder).toHaveBeenCalledWith(currentFolder, undefined); })); it('should do nothing if editing folder with no selection and payload', () => { @@ -332,7 +334,7 @@ describe('NodeEffects', () => { store.dispatch(new CopyNodesAction(null)); - expect(contentService.copyNodes).toHaveBeenCalledWith([node]); + expect(contentService.copyNodes).toHaveBeenCalledWith([node], undefined); })); it('should do nothing if invoking copy with no data', () => { @@ -364,7 +366,7 @@ describe('NodeEffects', () => { store.dispatch(new MoveNodesAction(null)); - expect(contentService.moveNodes).toHaveBeenCalledWith([node]); + expect(contentService.moveNodes).toHaveBeenCalledWith([node], undefined); })); it('should do nothing if invoking move with no data', () => { @@ -488,7 +490,7 @@ describe('NodeEffects', () => { store.dispatch(new ManageAspectsAction(null)); - expect(contentService.manageAspects).toHaveBeenCalledWith({ entry: { isFile: true, id: 'file-node-id' } }); + expect(contentService.manageAspects).toHaveBeenCalledWith({ entry: { isFile: true, id: 'file-node-id' } }, undefined); })); it('should call aspect dialog from the active folder selection', fakeAsync(() => { @@ -501,7 +503,7 @@ describe('NodeEffects', () => { store.dispatch(new ManageAspectsAction(null)); - expect(contentService.manageAspects).toHaveBeenCalledWith({ entry: { isFile: false, id: 'folder-node-id' } }); + expect(contentService.manageAspects).toHaveBeenCalledWith({ entry: { isFile: false, id: 'folder-node-id' } }, undefined); })); }); }); diff --git a/app/src/app/content-plugin/store/effects/node.effects.ts b/app/src/app/content-plugin/store/effects/node.effects.ts index 3f5b907dd..4b8fa9b5a 100644 --- a/app/src/app/content-plugin/store/effects/node.effects.ts +++ b/app/src/app/content-plugin/store/effects/node.effects.ts @@ -54,6 +54,7 @@ import { } from '@alfresco/aca-shared/store'; import { ContentManagementService } from '../../services/content-management.service'; import { ViewUtilService } from '@alfresco/adf-core'; +import { ModalConfiguration } from '@alfresco/aca-shared'; @Injectable() export class NodeEffects { @@ -69,15 +70,15 @@ export class NodeEffects { this.actions$.pipe( ofType(NodeActionTypes.Share), map((action) => { - if (action.payload) { - this.contentService.shareNode(action.payload); + if (action.payload?.entry) { + this.contentService.shareNode(action.payload, action.payload?.focusedElementOnCloseSelector); } else { this.store .select(getAppSelection) .pipe(take(1)) .subscribe((selection) => { if (selection && selection.file) { - this.contentService.shareNode(selection.file); + this.contentService.shareNode(selection.file, action.payload?.focusedElementOnCloseSelector); } }); } @@ -215,7 +216,7 @@ export class NodeEffects { this.actions$.pipe( ofType(NodeActionTypes.EditFolder), map((action) => { - if (action.payload) { + if (action.payload?.entry) { this.contentService.editFolder(action.payload); } else { this.store @@ -223,7 +224,7 @@ export class NodeEffects { .pipe(take(1)) .subscribe((selection) => { if (selection && selection.folder) { - this.contentService.editFolder(selection.folder); + this.contentService.editFolder(selection.folder, action.payload?.focusedElementOnCloseSelector); } }); } @@ -237,7 +238,7 @@ export class NodeEffects { this.actions$.pipe( ofType(NodeActionTypes.Copy), map((action) => { - if (action.payload && action.payload.length > 0) { + if (Array.isArray(action.payload) && action.payload?.length > 0) { this.contentService.copyNodes(action.payload); } else { this.store @@ -245,7 +246,7 @@ export class NodeEffects { .pipe(take(1)) .subscribe((selection) => { if (selection && !selection.isEmpty) { - this.contentService.copyNodes(selection.nodes); + this.contentService.copyNodes(selection.nodes, (action.payload as ModalConfiguration)?.focusedElementOnCloseSelector); } }); } @@ -259,7 +260,7 @@ export class NodeEffects { this.actions$.pipe( ofType(NodeActionTypes.Move), map((action) => { - if (action.payload && action.payload.length > 0) { + if (Array.isArray(action.payload) && action.payload?.length > 0) { this.contentService.moveNodes(action.payload); } else { this.store @@ -267,7 +268,7 @@ export class NodeEffects { .pipe(take(1)) .subscribe((selection) => { if (selection && !selection.isEmpty) { - this.contentService.moveNodes(selection.nodes); + this.contentService.moveNodes(selection.nodes, (action.payload as ModalConfiguration)?.focusedElementOnCloseSelector); } }); } @@ -281,7 +282,7 @@ export class NodeEffects { this.actions$.pipe( ofType(NodeActionTypes.ManagePermissions), map((action) => { - if (action && action.payload) { + if (action?.payload?.entry) { const route = 'personal-files/details'; this.store.dispatch(new NavigateRouteAction([route, action.payload.entry.id, 'permissions'])); } else { @@ -305,7 +306,7 @@ export class NodeEffects { this.actions$.pipe( ofType(NodeActionTypes.ExpandInfoDrawer), map((action) => { - if (action && action.payload) { + if (action?.payload?.entry) { const route = 'personal-files/details'; this.store.dispatch(new NavigateRouteAction([route, action.payload.entry.id])); } else { @@ -329,7 +330,7 @@ export class NodeEffects { this.actions$.pipe( ofType(NodeActionTypes.ManageVersions), map((action) => { - if (action && action.payload) { + if (action?.payload?.entry) { this.contentService.manageVersions(action.payload); } else { this.store @@ -337,7 +338,7 @@ export class NodeEffects { .pipe(take(1)) .subscribe((selection) => { if (selection && selection.file) { - this.contentService.manageVersions(selection.file); + this.contentService.manageVersions(selection.file, action.payload?.focusedElementOnCloseSelector); } }); } @@ -395,7 +396,7 @@ export class NodeEffects { this.actions$.pipe( ofType(NodeActionTypes.ChangeAspects), map((action) => { - if (action && action.payload) { + if (action?.payload?.entry) { this.contentService.manageAspects(action.payload); } else { this.store @@ -403,7 +404,7 @@ export class NodeEffects { .pipe(take(1)) .subscribe((selection) => { if (selection && !selection.isEmpty) { - this.contentService.manageAspects(selection.nodes[0]); + this.contentService.manageAspects(selection.nodes[0], action.payload?.focusedElementOnCloseSelector); } }); } @@ -429,7 +430,7 @@ export class NodeEffects { this.actions$.pipe( ofType(NodeActionTypes.ManageRules), map((action) => { - if (action && action.payload) { + if (action?.payload?.entry) { this.store.dispatch(new NavigateRouteAction(['nodes', action.payload.entry.id, 'rules'])); } else { this.store diff --git a/app/src/app/content-plugin/store/effects/upload.effects.ts b/app/src/app/content-plugin/store/effects/upload.effects.ts index c06fce6cc..6ce394063 100644 --- a/app/src/app/content-plugin/store/effects/upload.effects.ts +++ b/app/src/app/content-plugin/store/effects/upload.effects.ts @@ -41,12 +41,14 @@ import { of } from 'rxjs'; import { catchError, map, take } from 'rxjs/operators'; import { ContentManagementService } from '../../services/content-management.service'; import { MinimalNodeEntryEntity } from '@alfresco/js-api'; +import { ModalConfiguration } from '@alfresco/aca-shared'; @Injectable() export class UploadEffects { private fileInput: HTMLInputElement; private folderInput: HTMLInputElement; private fileVersionInput: HTMLInputElement; + private readonly createMenuButtonSelector = 'app-create-menu button'; constructor( private store: Store, @@ -90,7 +92,7 @@ export class UploadEffects { this.actions$.pipe( ofType(UploadActionTypes.UploadFiles), map(() => { - this.registerFocusingCreateMenuButton(this.fileInput); + this.registerFocusingElementAfterModalClose(this.fileInput, this.createMenuButtonSelector); this.fileInput.click(); }) ), @@ -102,7 +104,7 @@ export class UploadEffects { this.actions$.pipe( ofType(UploadActionTypes.UploadFolder), map(() => { - this.registerFocusingCreateMenuButton(this.folderInput); + this.registerFocusingElementAfterModalClose(this.folderInput, this.createMenuButtonSelector); this.folderInput.click(); }) ), @@ -114,11 +116,15 @@ export class UploadEffects { this.actions$.pipe( ofType(UploadActionTypes.UploadFileVersion), map((action) => { - if (action?.payload) { + if (action?.payload instanceof CustomEvent) { const node = action?.payload?.detail?.data?.node?.entry; const file: any = action?.payload?.detail?.files[0]?.file; this.contentService.versionUpdateDialog(node, file); - } else if (!action?.payload) { + } else if (!action?.payload || !(action.payload instanceof CustomEvent)) { + this.registerFocusingElementAfterModalClose( + this.fileVersionInput, + (action?.payload as ModalConfiguration)?.focusedElementOnCloseSelector + ); this.fileVersionInput.click(); } }) @@ -199,18 +205,18 @@ export class UploadEffects { }); } - private registerFocusingCreateMenuButton(input: HTMLInputElement): void { + private registerFocusingElementAfterModalClose(input: HTMLInputElement, focusedElementSelector: string): void { input.addEventListener( 'click', () => { window.addEventListener( 'focus', () => { - const createMenuButton = document.querySelector('app-create-menu button'); - createMenuButton.addEventListener('focus', () => createMenuButton.classList.add('cdk-program-focused'), { + const elementToFocus = document.querySelector(focusedElementSelector); + elementToFocus.addEventListener('focus', () => elementToFocus.classList.add('cdk-program-focused'), { once: true }); - createMenuButton.focus(); + elementToFocus.focus(); }, { once: true diff --git a/package-lock.json b/package-lock.json index 54b45ead3..6157198ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,9 +11,9 @@ "dev": true }, "@alfresco/adf-cli": { - "version": "6.0.0-A.1-37352", - "resolved": "https://registry.npmjs.org/@alfresco/adf-cli/-/adf-cli-6.0.0-A.1-37352.tgz", - "integrity": "sha512-lM//JUPyBmyO9PVDd628VFyumMJElBKNDvy6E0Yv/iKZmj2S/93pCFhkjnnbG/hOMNAIy5HePOvwOHQ9+vQAgg==", + "version": "6.0.0-A.1-37376", + "resolved": "https://registry.npmjs.org/@alfresco/adf-cli/-/adf-cli-6.0.0-A.1-37376.tgz", + "integrity": "sha512-sEKwZ9DS4CAzub6oKOmJGFoLzc4esMW4859GnbLVU9V8pBZ+KPIfRECY3AsCrRvbuK7dSM8EQgsrT8KDb9IZDg==", "dev": true, "requires": { "@alfresco/js-api": "5.2.0", @@ -28,17 +28,17 @@ } }, "@alfresco/adf-content-services": { - "version": "6.0.0-A.1-37352", - "resolved": "https://registry.npmjs.org/@alfresco/adf-content-services/-/adf-content-services-6.0.0-A.1-37352.tgz", - "integrity": "sha512-vKtVJDl7WE19WkbB9KHfeka5lnSBOUFAEwjBsSr/UGhNFkQn0zSS1I4CXhjY5h0o+X31JVuIzVLfPqAWiEOZkA==", + "version": "6.0.0-A.1-37376", + "resolved": "https://registry.npmjs.org/@alfresco/adf-content-services/-/adf-content-services-6.0.0-A.1-37376.tgz", + "integrity": "sha512-QrwadBgJFG4n7iaUJfirhYPEWCO+BD1YiPBlo3+gK9LJvnaHiAiiAFbnXqaa2FY1RlFm6Td6NH0yoHJtwQH+sQ==", "requires": { "tslib": "^2.3.0" } }, "@alfresco/adf-core": { - "version": "6.0.0-A.1-37352", - "resolved": "https://registry.npmjs.org/@alfresco/adf-core/-/adf-core-6.0.0-A.1-37352.tgz", - "integrity": "sha512-zGvZkpIYTaUgZLzyGWBK17MCA+HOQbEEefr3V3KisuLFgCXWwD2GyGn1xSba+kp6CDbabGjfTG/2pG0oDk8NeA==", + "version": "6.0.0-A.1-37376", + "resolved": "https://registry.npmjs.org/@alfresco/adf-core/-/adf-core-6.0.0-A.1-37376.tgz", + "integrity": "sha512-NryzWdDie9eyrp0a7lC1P+6R0nXp7KzoHfIVrWO1rNhPWBhYgJlRHPP44KBO0/Ydgbio3jvnwZlX15+Kq0/Awg==", "requires": { "@editorjs/code": "2.7.0", "@editorjs/editorjs": "2.25.0", @@ -56,20 +56,20 @@ } }, "@alfresco/adf-extensions": { - "version": "6.0.0-A.1-37352", - "resolved": "https://registry.npmjs.org/@alfresco/adf-extensions/-/adf-extensions-6.0.0-A.1-37352.tgz", - "integrity": "sha512-OcalA6Jl9tr1ynFj9SPbH+i+h4faMpqdFzDza6ZjT2Hquq2Thk5ylzMz+LIHW1FB2wsfKssy9a/409sRHyy1Uw==", + "version": "6.0.0-A.1-37376", + "resolved": "https://registry.npmjs.org/@alfresco/adf-extensions/-/adf-extensions-6.0.0-A.1-37376.tgz", + "integrity": "sha512-amPdYFRlctYGfKIf1IKt9+SSsVcXOhvOxMsccbN9jI6ShlXBvhBTK+jtItqoJp3GDfWWGU/7RJ0anbZbbQQxTw==", "requires": { "tslib": "^2.3.0" } }, "@alfresco/adf-testing": { - "version": "6.0.0-A.1-37352", - "resolved": "https://registry.npmjs.org/@alfresco/adf-testing/-/adf-testing-6.0.0-A.1-37352.tgz", - "integrity": "sha512-0T18iLgKq76YzCK+iJwpRie8xrJXmGBM/YZ/OqMwAmyHPt5nUY7Uzy+stBlh/TDC9bULCejf3aZByMP82lT7hg==", + "version": "6.0.0-A.1-37376", + "resolved": "https://registry.npmjs.org/@alfresco/adf-testing/-/adf-testing-6.0.0-A.1-37376.tgz", + "integrity": "sha512-WXbpfnKgRHCk7yb/OiWOkJuCvldxRa4vBlP4sRQ4ggoywS85V1yRtJqespoDV/MmYZQ0/ZNXZtVFW8GHzQPOcQ==", "dev": true, "requires": { - "@alfresco/js-api": "5.3.0-466", + "@alfresco/js-api": "5.3.0-475", "@angular/compiler": "14.1.3", "@angular/core": "14.1.3", "rxjs": "6.6.6", @@ -78,9 +78,9 @@ }, "dependencies": { "@alfresco/js-api": { - "version": "5.3.0-466", - "resolved": "https://registry.npmjs.org/@alfresco/js-api/-/js-api-5.3.0-466.tgz", - "integrity": "sha512-ArBqTqEDbzR/jD6YtJTrPQG3Wz69WXURjnzZE8Os+JWUacJS1n/xEqncJY+xDoILuT1EtaEjogOWNYZixUqXlg==", + "version": "5.3.0-475", + "resolved": "https://registry.npmjs.org/@alfresco/js-api/-/js-api-5.3.0-475.tgz", + "integrity": "sha512-oNx3f6c7UlEhAry4pOSoXfZV5E53EM10dBXO2O2PrNS1hNJyT/XiWzeFT3szF8pMQWKyHc3fR5JRVThnTnR++A==", "dev": true, "requires": { "event-emitter": "^0.3.5", @@ -7548,14 +7548,6 @@ "color-convert": "^2.0.1" } }, - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "requires": { - "balanced-match": "^1.0.0" - } - }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -11343,16 +11335,6 @@ "dev": true, "requires": { "minimatch": "^5.0.1" - }, - "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "requires": { - "balanced-match": "^1.0.0" - } - } } }, "image-size": { @@ -13770,14 +13752,6 @@ "picomatch": "^2.0.4" } }, - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "requires": { - "balanced-match": "^1.0.0" - } - }, "cacache": { "version": "16.1.3", "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", @@ -14277,14 +14251,6 @@ "humanize-ms": "^1.2.1" } }, - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "requires": { - "balanced-match": "^1.0.0" - } - }, "cacache": { "version": "16.1.2", "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.2.tgz", @@ -14642,14 +14608,6 @@ "npm-normalize-package-bin": "^1.0.1" }, "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "requires": { - "balanced-match": "^1.0.0" - } - }, "glob": { "version": "8.0.3", "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", @@ -15162,14 +15120,6 @@ "humanize-ms": "^1.2.1" } }, - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "requires": { - "balanced-match": "^1.0.0" - } - }, "builtins": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz", diff --git a/package.json b/package.json index eba7adf06..6e9ab7c9e 100644 --- a/package.json +++ b/package.json @@ -26,9 +26,9 @@ }, "private": true, "dependencies": { - "@alfresco/adf-content-services": "6.0.0-A.1-37352", - "@alfresco/adf-core": "6.0.0-A.1-37352", - "@alfresco/adf-extensions": "6.0.0-A.1-37352", + "@alfresco/adf-content-services": "6.0.0-A.1-37376", + "@alfresco/adf-core": "6.0.0-A.1-37376", + "@alfresco/adf-extensions": "6.0.0-A.1-37376", "@alfresco/js-api": "5.2.0", "@angular/animations": "14.1.2", "@angular/cdk": "14.1.2", @@ -58,8 +58,8 @@ "zone.js": "0.11.8" }, "devDependencies": { - "@alfresco/adf-cli": "6.0.0-A.1-37352", - "@alfresco/adf-testing": "6.0.0-A.1-37352", + "@alfresco/adf-cli": "6.0.0-A.1-37376", + "@alfresco/adf-testing": "6.0.0-A.1-37376", "@angular-custom-builders/lite-serve": "^0.2.3", "@angular-devkit/build-angular": "14.1.2", "@angular-eslint/builder": "^14.1.2", diff --git a/projects/aca-shared/src/lib/components/tool-bar/toolbar-button/toolbar-button.component.ts b/projects/aca-shared/src/lib/components/tool-bar/toolbar-button/toolbar-button.component.ts index c39fc2a21..5f069f0e4 100644 --- a/projects/aca-shared/src/lib/components/tool-bar/toolbar-button/toolbar-button.component.ts +++ b/projects/aca-shared/src/lib/components/tool-bar/toolbar-button/toolbar-button.component.ts @@ -53,7 +53,9 @@ export class ToolbarButtonComponent { runAction() { if (this.hasClickAction(this.actionRef)) { - this.extensions.runActionById(this.actionRef.actions.click); + this.extensions.runActionById(this.actionRef.actions.click, { + focusedElementOnCloseSelector: `#${this.actionRef.id.replace(/\./g, '\\.')}` + }); } } diff --git a/projects/aca-shared/src/lib/components/tool-bar/toolbar-menu-item/toolbar-menu-item.component.ts b/projects/aca-shared/src/lib/components/tool-bar/toolbar-menu-item/toolbar-menu-item.component.ts index 70a4af30a..c1c60f253 100644 --- a/projects/aca-shared/src/lib/components/tool-bar/toolbar-menu-item/toolbar-menu-item.component.ts +++ b/projects/aca-shared/src/lib/components/tool-bar/toolbar-menu-item/toolbar-menu-item.component.ts @@ -44,6 +44,8 @@ import { MatMenuItem } from '@angular/material/menu'; export class ToolbarMenuItemComponent { @Input() actionRef: ContentActionRef; + @Input() + menuId?: string; @ViewChild(MatMenuItem) menuItem: MatMenuItem; @@ -52,7 +54,14 @@ export class ToolbarMenuItemComponent { runAction() { if (this.hasClickAction(this.actionRef)) { - this.extensions.runActionById(this.actionRef.actions.click); + this.extensions.runActionById( + this.actionRef.actions.click, + this.menuId + ? { + focusedElementOnCloseSelector: `#${this.menuId.replace(/\./g, '\\.')}` + } + : undefined + ); } } diff --git a/projects/aca-shared/src/lib/components/tool-bar/toolbar-menu/toolbar-menu.component.html b/projects/aca-shared/src/lib/components/tool-bar/toolbar-menu/toolbar-menu.component.html index b496dae3c..a89c571f6 100644 --- a/projects/aca-shared/src/lib/components/tool-bar/toolbar-menu/toolbar-menu.component.html +++ b/projects/aca-shared/src/lib/components/tool-bar/toolbar-menu/toolbar-menu.component.html @@ -18,7 +18,7 @@ - + diff --git a/projects/aca-shared/src/lib/models/modal-configuration.ts b/projects/aca-shared/src/lib/models/modal-configuration.ts new file mode 100644 index 000000000..0a5182153 --- /dev/null +++ b/projects/aca-shared/src/lib/models/modal-configuration.ts @@ -0,0 +1,3 @@ +export interface ModalConfiguration { + focusedElementOnCloseSelector?: string; +} diff --git a/projects/aca-shared/src/lib/services/app.extension.service.ts b/projects/aca-shared/src/lib/services/app.extension.service.ts index b77e1b185..71e044973 100644 --- a/projects/aca-shared/src/lib/services/app.extension.service.ts +++ b/projects/aca-shared/src/lib/services/app.extension.service.ts @@ -498,7 +498,7 @@ export class AppExtensionService implements RuleContext { return false; } - runActionById(id: string) { + runActionById(id: string, additionalPayload?: { [key: string]: any }) { const action = this.extensions.getActionById(id); if (action) { const { type, payload } = action; @@ -507,9 +507,21 @@ export class AppExtensionService implements RuleContext { }; const expression = this.extensions.runExpression(payload, context); - this.store.dispatch({ type, payload: expression }); + this.store.dispatch({ + type, + payload: + typeof expression === 'object' + ? { + ...expression, + ...additionalPayload + } + : expression + }); } else { - this.store.dispatch({ type: id }); + this.store.dispatch({ + type: id, + payload: additionalPayload + }); } } diff --git a/projects/aca-shared/src/public-api.ts b/projects/aca-shared/src/public-api.ts index c7d316b65..ef179bb49 100644 --- a/projects/aca-shared/src/public-api.ts +++ b/projects/aca-shared/src/public-api.ts @@ -48,6 +48,7 @@ export * from './lib/directives/shared.directives.module'; export * from './lib/models/types'; export * from './lib/models/viewer.rules'; +export * from './lib/models/modal-configuration'; export * from './lib/routing/shared.guard'; diff --git a/projects/aca-shared/store/src/actions/library.actions.ts b/projects/aca-shared/store/src/actions/library.actions.ts index dcd03c509..51d3e4e7d 100644 --- a/projects/aca-shared/store/src/actions/library.actions.ts +++ b/projects/aca-shared/store/src/actions/library.actions.ts @@ -25,6 +25,7 @@ import { Action } from '@ngrx/store'; import { SiteBody } from '@alfresco/js-api'; +import { ModalConfiguration } from '@alfresco/aca-shared'; export enum LibraryActionTypes { Delete = 'DELETE_LIBRARY', @@ -59,5 +60,5 @@ export class UpdateLibraryAction implements Action { export class LeaveLibraryAction implements Action { readonly type = LibraryActionTypes.Leave; - constructor(public payload?: string) {} + constructor(public payload?: string | ModalConfiguration) {} } diff --git a/projects/aca-shared/store/src/actions/node.actions.ts b/projects/aca-shared/store/src/actions/node.actions.ts index e17ac7385..3def3199a 100644 --- a/projects/aca-shared/store/src/actions/node.actions.ts +++ b/projects/aca-shared/store/src/actions/node.actions.ts @@ -25,6 +25,7 @@ import { Action } from '@ngrx/store'; import { MinimalNodeEntity } from '@alfresco/js-api'; +import { ModalConfiguration } from '@alfresco/aca-shared'; export enum NodeActionTypes { SetSelection = 'SET_SELECTED_NODES', @@ -84,7 +85,7 @@ export class PurgeDeletedNodesAction implements Action { export class DownloadNodesAction implements Action { readonly type = NodeActionTypes.Download; - constructor(public payload: MinimalNodeEntity[] = []) {} + constructor(public payload: MinimalNodeEntity[] | ModalConfiguration = []) {} } export class CreateFolderAction implements Action { @@ -96,13 +97,13 @@ export class CreateFolderAction implements Action { export class EditFolderAction implements Action { readonly type = NodeActionTypes.EditFolder; - constructor(public payload: MinimalNodeEntity) {} + constructor(public payload: MinimalNodeEntity & ModalConfiguration) {} } export class ShareNodeAction implements Action { readonly type = NodeActionTypes.Share; - constructor(public payload: MinimalNodeEntity) {} + constructor(public payload: MinimalNodeEntity & ModalConfiguration) {} } export class UnshareNodesAction implements Action { @@ -114,13 +115,13 @@ export class UnshareNodesAction implements Action { export class CopyNodesAction implements Action { readonly type = NodeActionTypes.Copy; - constructor(public payload: Array) {} + constructor(public payload: Array | ModalConfiguration) {} } export class MoveNodesAction implements Action { readonly type = NodeActionTypes.Move; - constructor(public payload: Array) {} + constructor(public payload: Array | ModalConfiguration) {} } export class ManagePermissionsAction implements Action { @@ -143,7 +144,7 @@ export class PrintFileAction implements Action { export class ManageVersionsAction implements Action { readonly type = NodeActionTypes.ManageVersions; - constructor(public payload: MinimalNodeEntity) {} + constructor(public payload: MinimalNodeEntity & ModalConfiguration) {} } export class EditOfflineAction implements Action { @@ -172,7 +173,7 @@ export class RemoveFavoriteAction implements Action { export class ManageAspectsAction implements Action { readonly type = NodeActionTypes.ChangeAspects; - constructor(public payload: MinimalNodeEntity) {} + constructor(public payload: MinimalNodeEntity & ModalConfiguration) {} } export class ManageRulesAction implements Action { diff --git a/projects/aca-shared/store/src/actions/upload.actions.ts b/projects/aca-shared/store/src/actions/upload.actions.ts index 05e8e163a..9bfaaaf09 100644 --- a/projects/aca-shared/store/src/actions/upload.actions.ts +++ b/projects/aca-shared/store/src/actions/upload.actions.ts @@ -24,6 +24,7 @@ */ import { Action } from '@ngrx/store'; +import { ModalConfiguration } from '@alfresco/aca-shared'; export enum UploadActionTypes { UploadFiles = 'UPLOAD_FILES', @@ -46,5 +47,5 @@ export class UploadFolderAction implements Action { export class UploadFileVersionAction implements Action { readonly type = UploadActionTypes.UploadFileVersion; - constructor(public payload: CustomEvent) {} + constructor(public payload: CustomEvent | ModalConfiguration) {} }