Skip to content

Commit 2641658

Browse files
[ACS-10303] Fix context menu keyboard navigation (#4844)
* [ACS-10303] Fix context menu keyboard navigation * [ACS-10303] cr fixes * [ACS-10303] cr fixes * [ACS-10303] cr fixes
1 parent fb2a70f commit 2641658

File tree

11 files changed

+240
-50
lines changed

11 files changed

+240
-50
lines changed

projects/aca-content/src/lib/components/common/toggle-shared/toggle-shared.component.html

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@
22
<ng-container *ngIf="!data.iconButton">
33
<button mat-menu-item data-automation-id="share-action-button" (click)="editSharedNode(selectionState, '.adf-context-menu-source')">
44
<mat-icon>link</mat-icon>
5-
<ng-container *ngIf="isShared; else not_shared">
6-
<span>{{ 'APP.ACTIONS.SHARE_EDIT' | translate }}</span>
7-
</ng-container>
5+
<span>{{ (isShared ? 'APP.ACTIONS.SHARE_EDIT' : 'APP.ACTIONS.SHARE') | translate }}</span>
86
</button>
97
</ng-container>
108

@@ -21,7 +19,3 @@
2119
</button>
2220
</ng-container>
2321
</ng-container>
24-
25-
<ng-template #not_shared>
26-
<span>{{ 'APP.ACTIONS.SHARE' | translate }}</span>
27-
</ng-template>

projects/aca-content/src/lib/components/common/toggle-shared/toggle-shared.component.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@
2222
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
2323
*/
2424

25-
import { Component, DestroyRef, inject, Input, OnInit, ViewEncapsulation } from '@angular/core';
25+
import { Component, DestroyRef, inject, Input, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
2626
import { Observable } from 'rxjs';
2727
import { Store } from '@ngrx/store';
2828
import { SelectionState } from '@alfresco/adf-extensions';
2929
import { AppStore, getAppSelection, ShareNodeAction } from '@alfresco/aca-shared/store';
3030
import { CommonModule } from '@angular/common';
31-
import { MatMenuModule } from '@angular/material/menu';
31+
import { MatMenuItem, MatMenuModule } from '@angular/material/menu';
3232
import { MatIconModule } from '@angular/material/icon';
3333
import { TranslatePipe } from '@ngx-translate/core';
3434
import { MatButtonModule } from '@angular/material/button';
@@ -46,6 +46,9 @@ export class ToggleSharedComponent implements OnInit {
4646
iconButton?: string;
4747
};
4848

49+
@ViewChild(MatMenuItem)
50+
menuItem: MatMenuItem;
51+
4952
selection$: Observable<SelectionState>;
5053
selectionState: SelectionState;
5154
selectionLabel = '';

projects/aca-content/src/lib/components/context-menu/context-menu.component.spec.ts

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,35 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
2626
import { AppTestingModule } from '../../testing/app-testing.module';
2727
import { ContextMenuComponent } from './context-menu.component';
2828
import { ContextMenuOverlayRef } from './context-menu-overlay';
29-
import { ContentActionType } from '@alfresco/adf-extensions';
29+
import { ContentActionRef, ContentActionType, ExtensionService } from '@alfresco/adf-extensions';
3030

3131
import { of } from 'rxjs';
3232
import { Store } from '@ngrx/store';
3333
import { AppExtensionService } from '@alfresco/aca-shared';
34+
import { Component, ViewChild } from '@angular/core';
35+
import { MatMenuItem, MatMenuModule } from '@angular/material/menu';
36+
import { UnitTestingUtils } from '@alfresco/adf-core';
37+
38+
@Component({
39+
selector: 'aca-custom-menu-component',
40+
standalone: true,
41+
imports: [MatMenuModule],
42+
// eslint-disable-next-line @alfresco/eslint-angular/no-angular-material-selectors
43+
template: '<button mat-menu-item id="custom-action">Custom Component Content</button>'
44+
})
45+
class TestCustomMenuComponent {
46+
data: any;
47+
@ViewChild(MatMenuItem) menuItem: MatMenuItem;
48+
}
3449

3550
describe('ContextMenuComponent', () => {
3651
let fixture: ComponentFixture<ContextMenuComponent>;
3752
let component: ContextMenuComponent;
3853
let extensionsService: AppExtensionService;
54+
let extensionService: ExtensionService;
55+
let unitTestingUtils: UnitTestingUtils;
3956

40-
const contextItem = {
57+
const contextItem: ContentActionRef = {
4158
type: ContentActionType.button,
4259
id: 'action-button',
4360
title: 'Test Button',
@@ -48,7 +65,7 @@ describe('ContextMenuComponent', () => {
4865

4966
beforeEach(() => {
5067
TestBed.configureTestingModule({
51-
imports: [AppTestingModule],
68+
imports: [AppTestingModule, TestCustomMenuComponent],
5269
providers: [
5370
{
5471
provide: ContextMenuOverlayRef,
@@ -70,6 +87,8 @@ describe('ContextMenuComponent', () => {
7087
component = fixture.componentInstance;
7188

7289
extensionsService = TestBed.inject(AppExtensionService);
90+
extensionService = TestBed.inject(ExtensionService);
91+
unitTestingUtils = new UnitTestingUtils(fixture.debugElement);
7392
});
7493

7594
it('should load context menu actions on init', () => {
@@ -84,20 +103,55 @@ describe('ContextMenuComponent', () => {
84103
fixture.detectChanges();
85104
await fixture.whenStable();
86105

87-
const contextMenuElements = document.body.querySelector('.aca-context-menu')?.querySelectorAll('button');
88-
const actionButtonLabel: HTMLElement = contextMenuElements?.[0].querySelector(`[data-automation-id="${contextItem.id}-label"]`);
106+
const contextMenuButtons = unitTestingUtils.getAllByCSS('.aca-context-menu button');
107+
const actionButtonLabel = unitTestingUtils.getInnerTextByCSS(
108+
`.aca-context-menu button:first-child [data-automation-id="${contextItem.id}-label"]`
109+
);
89110

90-
expect(contextMenuElements?.length).toBe(1);
91-
expect(actionButtonLabel.innerText).toBe(contextItem.title);
111+
expect(contextMenuButtons?.length).toBe(1);
112+
expect(actionButtonLabel).toBe(contextItem.title);
92113
});
93114

94115
it('should not render context menu if no actions items', async () => {
95116
spyOn(extensionsService, 'getAllowedContextMenuActions').and.returnValue(of([]));
96117
fixture.detectChanges();
97118
await fixture.whenStable();
98119

99-
const contextMenuElements = document.body.querySelector('.aca-context-menu');
120+
const contextMenuElements = unitTestingUtils.getByCSS('.aca-context-menu');
100121

101122
expect(contextMenuElements).toBeNull();
102123
});
124+
125+
it('should append menu items in the correct order according to actions array', async () => {
126+
const customComponentAction: ContentActionRef = {
127+
type: ContentActionType.custom,
128+
component: 'test-custom-component',
129+
id: 'custom-action',
130+
data: { testProp: 'test-value' }
131+
};
132+
133+
const buttonAction2: ContentActionRef = {
134+
type: ContentActionType.button,
135+
id: 'button-action-2',
136+
title: 'Button 2',
137+
actions: {
138+
click: 'EVENT_2'
139+
}
140+
};
141+
142+
const orderedActions = [contextItem, customComponentAction, buttonAction2];
143+
144+
spyOn(extensionsService, 'getAllowedContextMenuActions').and.returnValue(of(orderedActions));
145+
spyOn(extensionService, 'getComponentById').and.returnValue(TestCustomMenuComponent);
146+
147+
fixture.detectChanges();
148+
await fixture.whenStable();
149+
150+
const menuItems = component.menu._allItems.toArray();
151+
expect(menuItems.length).toBe(3);
152+
153+
const menuItemsIds = menuItems.map((item) => item._getHostElement().getAttribute('id'));
154+
const domIds: string[] = Array.from(unitTestingUtils.getAllByCSS('button')).map((button) => button.nativeElement.getAttribute('id'));
155+
expect(domIds).toEqual(menuItemsIds);
156+
});
103157
});

projects/aca-content/src/lib/components/context-menu/context-menu.component.ts

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@
2222
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
2323
*/
2424

25-
import { AfterViewInit, Component, DestroyRef, inject, Inject, OnInit, ViewEncapsulation } from '@angular/core';
26-
import { MatMenuModule } from '@angular/material/menu';
27-
import { DynamicExtensionComponent } from '@alfresco/adf-extensions';
25+
import { AfterViewInit, Component, DestroyRef, inject, Inject, OnInit, QueryList, ViewChild, ViewChildren, ViewEncapsulation } from '@angular/core';
26+
import { MatMenu, MatMenuItem, MatMenuModule } from '@angular/material/menu';
27+
import { ContentActionType, DynamicExtensionComponent } from '@alfresco/adf-extensions';
2828
import { ContextMenuOverlayRef } from './context-menu-overlay';
2929
import { CONTEXT_MENU_DIRECTION } from './direction.token';
3030
import { Direction } from '@angular/cdk/bidi';
@@ -58,6 +58,15 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
5858
encapsulation: ViewEncapsulation.None
5959
})
6060
export class ContextMenuComponent extends BaseContextMenuDirective implements OnInit, AfterViewInit {
61+
@ViewChildren(DynamicExtensionComponent)
62+
dynamicExtensionComponents: QueryList<DynamicExtensionComponent>;
63+
64+
@ViewChild(MatMenu)
65+
menu: MatMenu;
66+
67+
@ViewChildren(MatMenuItem)
68+
matMenuItems: QueryList<MatMenuItem>;
69+
6170
private readonly destroyRef = inject(DestroyRef);
6271

6372
constructor(contextMenuOverlayRef: ContextMenuOverlayRef, extensions: AppExtensionService, @Inject(CONTEXT_MENU_DIRECTION) direction: Direction) {
@@ -77,5 +86,41 @@ export class ContextMenuComponent extends BaseContextMenuDirective implements On
7786
if (this.actions.length) {
7887
setTimeout(() => this.trigger.openMenu(), 0);
7988
}
89+
90+
const itemsById = this.createMenuItemsLookup();
91+
const orderedItems = this.createOrderedItemsList(itemsById);
92+
93+
const menuItemsQueryList = new QueryList<MatMenuItem>();
94+
menuItemsQueryList.reset(orderedItems);
95+
this.menu._allItems = menuItemsQueryList;
96+
this.menu.ngAfterContentInit();
97+
}
98+
99+
private createMenuItemsLookup(): Map<string, MatMenuItem> {
100+
const itemsById = new Map<string, MatMenuItem>();
101+
this.matMenuItems.forEach((item) => {
102+
itemsById.set(item._getHostElement()?.getAttribute('id'), item);
103+
});
104+
105+
this.dynamicExtensionComponents.forEach((component) => {
106+
if (component.menuItem && component.id) {
107+
itemsById.set(component.id, component.menuItem);
108+
}
109+
});
110+
return itemsById;
111+
}
112+
113+
private createOrderedItemsList(itemsById: Map<string, MatMenuItem>): MatMenuItem[] {
114+
const orderedItems: MatMenuItem[] = [];
115+
116+
this.actions.forEach((action) => {
117+
const lookupId = action.type === ContentActionType.custom ? action.component : action.id;
118+
const item = lookupId ? itemsById.get(lookupId) : undefined;
119+
120+
if (item) {
121+
orderedItems.push(item);
122+
}
123+
});
124+
return orderedItems;
80125
}
81126
}

projects/aca-content/src/lib/components/toolbar/toggle-edit-offline/toggle-edit-offline.component.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,14 @@
2424

2525
import { AppStore, DownloadNodesAction, EditOfflineAction, SetSelectedNodesAction, getAppSelection } from '@alfresco/aca-shared/store';
2626
import { NodeEntry, SharedLinkEntry, Node, NodesApi } from '@alfresco/js-api';
27-
import { Component, inject, OnInit, ViewEncapsulation } from '@angular/core';
27+
import { Component, inject, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
2828
import { Store } from '@ngrx/store';
2929
import { AppExtensionService, isLocked } from '@alfresco/aca-shared';
3030
import { NotificationService } from '@alfresco/adf-core';
3131
import { AlfrescoApiService } from '@alfresco/adf-content-services';
3232
import { CommonModule } from '@angular/common';
3333
import { TranslatePipe } from '@ngx-translate/core';
34-
import { MatMenuModule } from '@angular/material/menu';
34+
import { MatMenuItem, MatMenuModule } from '@angular/material/menu';
3535
import { MatIconModule } from '@angular/material/icon';
3636

3737
@Component({
@@ -47,6 +47,9 @@ import { MatIconModule } from '@angular/material/icon';
4747
host: { class: 'app-toggle-edit-offline' }
4848
})
4949
export class ToggleEditOfflineComponent implements OnInit {
50+
@ViewChild(MatMenuItem)
51+
menuItem: MatMenuItem;
52+
5053
private notificationService = inject(NotificationService);
5154

5255
private nodesApi: NodesApi;

projects/aca-content/src/lib/components/toolbar/toggle-favorite-library/toggle-favorite-library.component.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
2323
*/
2424

25-
import { Component, DestroyRef, inject, OnInit, ViewEncapsulation } from '@angular/core';
25+
import { Component, DestroyRef, inject, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
2626
import { Store } from '@ngrx/store';
2727
import { AppHookService } from '@alfresco/aca-shared';
2828
import { AppStore, getAppSelection } from '@alfresco/aca-shared/store';
@@ -33,7 +33,7 @@ import { CommonModule } from '@angular/common';
3333
import { TranslatePipe } from '@ngx-translate/core';
3434
import { LibraryFavoriteDirective } from '@alfresco/adf-content-services';
3535
import { MatIconModule } from '@angular/material/icon';
36-
import { MatMenuModule } from '@angular/material/menu';
36+
import { MatMenuItem, MatMenuModule } from '@angular/material/menu';
3737
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
3838

3939
@Component({
@@ -46,8 +46,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
4646
[adf-favorite-library]="library"
4747
[attr.title]="library.isFavorite ? ('APP.ACTIONS.REMOVE_FAVORITE' | translate) : ('APP.ACTIONS.FAVORITE' | translate)"
4848
>
49-
<mat-icon *ngIf="library.isFavorite">star</mat-icon>
50-
<mat-icon *ngIf="!library.isFavorite">star_border</mat-icon>
49+
<mat-icon class="app-context-menu-item--icon">{{ library.isFavorite ? 'star' : 'star_border' }}</mat-icon>
5150
<span>{{ (library.isFavorite ? 'APP.ACTIONS.REMOVE_FAVORITE' : 'APP.ACTIONS.FAVORITE') | translate }}</span>
5251
</button>
5352
`,
@@ -57,6 +56,9 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
5756
export class ToggleFavoriteLibraryComponent implements OnInit {
5857
library;
5958

59+
@ViewChild(MatMenuItem)
60+
menuItem: MatMenuItem;
61+
6062
private readonly destroyRef = inject(DestroyRef);
6163

6264
constructor(

projects/aca-content/src/lib/components/toolbar/toggle-favorite/toggle-favorite.component.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
2323
*/
2424

25-
import { Component, inject, Input, OnInit, ViewEncapsulation } from '@angular/core';
25+
import { Component, inject, Input, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
2626
import { Store } from '@ngrx/store';
2727
import { Observable } from 'rxjs';
2828
import { SelectionState } from '@alfresco/adf-extensions';
@@ -32,15 +32,14 @@ import { CommonModule } from '@angular/common';
3232
import { DocumentListService, NodeFavoriteDirective } from '@alfresco/adf-content-services';
3333
import { MatIconModule } from '@angular/material/icon';
3434
import { TranslatePipe } from '@ngx-translate/core';
35-
import { MatMenuModule } from '@angular/material/menu';
35+
import { MatMenuItem, MatMenuModule } from '@angular/material/menu';
3636

3737
@Component({
3838
imports: [CommonModule, TranslatePipe, MatIconModule, MatMenuModule, NodeFavoriteDirective],
3939
selector: 'app-toggle-favorite',
4040
template: `
4141
<button mat-menu-item #favorites="adfFavorite" (toggle)="onToggleEvent()" [adf-node-favorite]="(selection$ | async).nodes">
42-
<mat-icon *ngIf="favorites.hasFavorites()">star</mat-icon>
43-
<mat-icon *ngIf="!favorites.hasFavorites()">star_border</mat-icon>
42+
<mat-icon class="app-context-menu-item--icon">{{ favorites.hasFavorites() ? 'star' : 'star_border' }}</mat-icon>
4443
<span>{{ (favorites.hasFavorites() ? 'APP.ACTIONS.REMOVE_FAVORITE' : 'APP.ACTIONS.FAVORITE') | translate }}</span>
4544
</button>
4645
`,
@@ -54,6 +53,9 @@ export class ToggleFavoriteComponent implements OnInit {
5453
selection$: Observable<SelectionState>;
5554
private reloadOnRoutes: string[] = [];
5655

56+
@ViewChild(MatMenuItem)
57+
menuItem: MatMenuItem;
58+
5759
constructor(
5860
private store: Store<AppStore>,
5961
private router: Router

projects/aca-content/src/lib/components/toolbar/toggle-join-library/toggle-join-library-button.component.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
import { AppStore, SetSelectedNodesAction, getAppSelection } from '@alfresco/aca-shared/store';
2626
import { AppHookService, UserProfileService } from '@alfresco/aca-shared';
2727
import { SelectionState } from '@alfresco/adf-extensions';
28-
import { Component, inject, ViewEncapsulation } from '@angular/core';
28+
import { Component, inject, ViewChild, ViewEncapsulation } from '@angular/core';
2929
import { Store } from '@ngrx/store';
3030
import { Observable } from 'rxjs';
3131
import { LibraryMembershipDirective, LibraryMembershipErrorEvent, LibraryMembershipToggleEvent } from '@alfresco/adf-content-services';
@@ -34,6 +34,7 @@ import { MatButtonModule } from '@angular/material/button';
3434
import { TranslatePipe } from '@ngx-translate/core';
3535
import { MatIconModule } from '@angular/material/icon';
3636
import { NotificationService } from '@alfresco/adf-core';
37+
import { MatMenuItem } from '@angular/material/menu';
3738

3839
@Component({
3940
imports: [CommonModule, TranslatePipe, MatButtonModule, MatIconModule, LibraryMembershipDirective],
@@ -57,6 +58,9 @@ import { NotificationService } from '@alfresco/adf-core';
5758
host: { class: 'app-toggle-join-library' }
5859
})
5960
export class ToggleJoinLibraryButtonComponent {
61+
@ViewChild(MatMenuItem)
62+
menuItem: MatMenuItem;
63+
6064
private userProfileService = inject(UserProfileService);
6165
private notificationService = inject(NotificationService);
6266
private appHookService = inject(AppHookService);

projects/aca-content/src/lib/components/toolbar/view-node/view-node.component.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
2323
*/
2424

25-
import { Component, inject, Input, ViewEncapsulation } from '@angular/core';
25+
import { Component, inject, Input, ViewChild, ViewEncapsulation } from '@angular/core';
2626
import { Store } from '@ngrx/store';
2727
import { AppStore, getAppSelection, ViewNodeAction } from '@alfresco/aca-shared/store';
2828
import { ActivatedRoute, Router } from '@angular/router';
@@ -33,7 +33,7 @@ import { CommonModule } from '@angular/common';
3333
import { TranslatePipe } from '@ngx-translate/core';
3434
import { MatButtonModule } from '@angular/material/button';
3535
import { MatIconModule } from '@angular/material/icon';
36-
import { MatMenuModule } from '@angular/material/menu';
36+
import { MatMenuItem, MatMenuModule } from '@angular/material/menu';
3737
import { MatDialogModule } from '@angular/material/dialog';
3838

3939
@Component({
@@ -63,6 +63,9 @@ export class ViewNodeComponent {
6363

6464
@Input() data: { title?: string; menuButton?: boolean; iconButton?: boolean };
6565

66+
@ViewChild(MatMenuItem)
67+
menuItem: MatMenuItem;
68+
6669
constructor(
6770
private store: Store<AppStore>,
6871
private router: Router,

0 commit comments

Comments
 (0)