From e4feb6838148d64d78de8a15b36eba7bcc1795e9 Mon Sep 17 00:00:00 2001 From: Mykyta Maliarchuk <84377976+nikita-web-ua@users.noreply.github.com> Date: Mon, 17 Feb 2025 10:52:27 +0100 Subject: [PATCH] [ACS-9266][ACS-9234] Make notification and user menu accessible with keyboard (#10647) * [ACS-9266][ACS-9234] Make notification and user menu accessible with keyboard * [ACS-9266] cr fix * [ACS-9266] cr fixes * [ACS-9266] fix eslint * [ACS-9266] fix circular dependency * [ACS-9266] cr fix --- .../language-menu/language-menu.component.ts | 7 +- .../language-picker.component.spec.ts | 59 +++++++ .../language-picker.component.ts | 24 ++- .../notification-history.component.html | 146 +++++++++--------- .../notification-history.component.scss | 143 +++++++++-------- .../notification-history.component.spec.ts | 2 +- .../notification-history.component.ts | 29 +--- lib/core/src/lib/styles/_mat-selectors.scss | 1 - .../dynamic.component.spec.ts | 14 +- .../dynamic-component/dynamic.component.ts | 12 +- .../services/component-register.service.ts | 2 + 11 files changed, 263 insertions(+), 176 deletions(-) create mode 100644 lib/core/src/lib/language-menu/language-picker.component.spec.ts diff --git a/lib/core/src/lib/language-menu/language-menu.component.ts b/lib/core/src/lib/language-menu/language-menu.component.ts index feb03e92b3..c549a7f821 100644 --- a/lib/core/src/lib/language-menu/language-menu.component.ts +++ b/lib/core/src/lib/language-menu/language-menu.component.ts @@ -15,12 +15,12 @@ * limitations under the License. */ -import { Component, EventEmitter, Output } from '@angular/core'; +import { Component, EventEmitter, Output, QueryList, ViewChildren } from '@angular/core'; import { LanguageService } from './service/language.service'; import { Observable } from 'rxjs'; import { LanguageItem } from '../common/services/language-item.interface'; import { CommonModule } from '@angular/common'; -import { MatMenuModule } from '@angular/material/menu'; +import { MatMenuItem, MatMenuModule } from '@angular/material/menu'; @Component({ selector: 'adf-language-menu', @@ -37,6 +37,9 @@ export class LanguageMenuComponent { @Output() changedLanguage: EventEmitter<LanguageItem> = new EventEmitter<LanguageItem>(); + @ViewChildren(MatMenuItem) + menuItems: QueryList<MatMenuItem>; + languages$: Observable<LanguageItem[]>; constructor(private languageService: LanguageService) { diff --git a/lib/core/src/lib/language-menu/language-picker.component.spec.ts b/lib/core/src/lib/language-menu/language-picker.component.spec.ts new file mode 100644 index 0000000000..fd8c6e503f --- /dev/null +++ b/lib/core/src/lib/language-menu/language-picker.component.spec.ts @@ -0,0 +1,59 @@ +/*! + * @license + * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * 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 { ComponentFixture, TestBed } from '@angular/core/testing'; +import { LanguagePickerComponent } from './language-picker.component'; +import { MatMenuItem, MatMenuTrigger } from '@angular/material/menu'; +import { LanguageMenuComponent } from './language-menu.component'; +import { QueryList } from '@angular/core'; +import { CoreTestingModule, UnitTestingUtils } from '@alfresco/adf-core'; + +describe('LanguagePickerComponent', () => { + let component: LanguagePickerComponent; + let fixture: ComponentFixture<LanguagePickerComponent>; + let testingUtils: UnitTestingUtils; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CoreTestingModule, LanguagePickerComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(LanguagePickerComponent); + testingUtils = new UnitTestingUtils(fixture.debugElement); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should assign menuItems to MatMenu in ngAfterViewInit', () => { + testingUtils.getByDirective(MatMenuTrigger).nativeElement.click(); + fixture.detectChanges(); + const languageMenuComponent = testingUtils.getByDirective(LanguageMenuComponent).componentInstance; + const menuItem1 = new MatMenuItem(null, null, null, null, null); + const menuItem2 = new MatMenuItem(null, null, null, null, null); + + languageMenuComponent.menuItems = new QueryList<MatMenuItem>(); + languageMenuComponent.menuItems.reset([menuItem1, menuItem2]); + spyOn(component.menu, 'ngAfterContentInit').and.callThrough(); + + component.ngAfterViewInit(); + // eslint-disable-next-line no-underscore-dangle + expect(component.menu._allItems.length).toBe(2); + // eslint-disable-next-line no-underscore-dangle + expect(component.menu._allItems.toArray()).toEqual([menuItem1, menuItem2]); + expect(component.menu.ngAfterContentInit).toHaveBeenCalled(); + }); +}); diff --git a/lib/core/src/lib/language-menu/language-picker.component.ts b/lib/core/src/lib/language-menu/language-picker.component.ts index 889ea4372f..869db82546 100644 --- a/lib/core/src/lib/language-menu/language-picker.component.ts +++ b/lib/core/src/lib/language-menu/language-picker.component.ts @@ -15,10 +15,10 @@ * limitations under the License. */ -import { Component, EventEmitter, Output } from '@angular/core'; +import { AfterViewInit, Component, EventEmitter, Output, QueryList, ViewChild } from '@angular/core'; import { LanguageItem } from '../common/services/language-item.interface'; import { CommonModule } from '@angular/common'; -import { MatMenuModule } from '@angular/material/menu'; +import { MatMenu, MatMenuItem, MatMenuModule } from '@angular/material/menu'; import { TranslateModule } from '@ngx-translate/core'; import { LanguageMenuComponent } from './language-menu.component'; import { MatIconModule } from '@angular/material/icon'; @@ -37,7 +37,25 @@ import { MatIconModule } from '@angular/material/icon'; </mat-menu> ` }) -export class LanguagePickerComponent { +export class LanguagePickerComponent implements AfterViewInit { @Output() public changedLanguage = new EventEmitter<LanguageItem>(); + + @ViewChild('langMenu') + menu: MatMenu; + + @ViewChild(MatMenuItem) + menuItem: MatMenuItem; + + @ViewChild(LanguageMenuComponent) + languageMenuComponent: LanguageMenuComponent; + + ngAfterViewInit() { + const menuItems = this.languageMenuComponent.menuItems.filter((menuItem) => menuItem !== undefined); + const menuItemsQueryList = new QueryList<MatMenuItem>(); + menuItemsQueryList.reset(menuItems); + // eslint-disable-next-line no-underscore-dangle + this.menu._allItems = menuItemsQueryList; + this.menu.ngAfterContentInit(); + } } diff --git a/lib/core/src/lib/notifications/components/notification-history.component.html b/lib/core/src/lib/notifications/components/notification-history.component.html index c9d8729a0f..8540b83e33 100644 --- a/lib/core/src/lib/notifications/components/notification-history.component.html +++ b/lib/core/src/lib/notifications/components/notification-history.component.html @@ -1,79 +1,75 @@ -<div (keyup)="onKeyPress($event)" tabindex="-1" role="button" class="adf-notification-history-container"> - <button mat-button - [matMenuTriggerFor]="menu" - aria-hidden="false" - [attr.aria-label]="'NOTIFICATIONS.OPEN_HISTORY' | translate" - title="{{ 'NOTIFICATIONS.OPEN_HISTORY' | translate }}" - class="adf-notification-history-menu_button" - id="adf-notification-history-open-button" - (menuOpened)="onMenuOpened()"> - <mat-icon matBadge="⁠" - [matBadgeHidden]="!notifications.length" - class="adf-notification-history-menu_button-icon" - matBadgeColor="accent" - matBadgeSize="small">notifications - </mat-icon> - </button> - <mat-menu #menu="matMenu" - [xPosition]="menuPositionX" - [yPosition]="menuPositionY" - id="adf-notification-history-menu" - class="adf-notification-history-menu adf-notification-history-menu-panel"> - <div class="adf-notification-history-list" - role="button" - tabindex="0" - (keyup.enter)="$event.stopPropagation()" - (click)="$event.stopPropagation()"> - <div mat-subheader role="menuitem"> - <span class="adf-notification-history-menu-title">{{ 'NOTIFICATIONS.TITLE' | translate }}</span> - <button *ngIf="notifications.length" - id="adf-notification-history-mark-as-read" - class="adf-notification-history-mark-as-read" - mat-icon-button - title="{{ 'NOTIFICATIONS.MARK_AS_READ' | translate }}" - (click)="markAsRead()"> - <mat-icon>done_all</mat-icon> - </button> - </div> +<button mat-button + [matMenuTriggerFor]="menu" + aria-hidden="false" + [attr.aria-label]="'NOTIFICATIONS.OPEN_HISTORY' | translate" + title="{{ 'NOTIFICATIONS.OPEN_HISTORY' | translate }}" + class="adf-notification-history-menu_button" + id="adf-notification-history-open-button" + (menuOpened)="onMenuOpened()"> + <mat-icon matBadge="⁠" + [matBadgeHidden]="!notifications.length" + class="adf-notification-history-menu_button-icon" + matBadgeColor="accent" + matBadgeSize="small">notifications + </mat-icon> +</button> - <mat-divider /> +<mat-menu #menu="matMenu" + [xPosition]="menuPositionX" + [yPosition]="menuPositionY" + id="adf-notification-history-menu" + class="adf-notification-history-menu adf-notification-history-menu-panel"> + <div class="adf-notification-history-list-header"> + <span class="adf-notification-history-menu-title">{{ 'NOTIFICATIONS.TITLE' | translate }}</span> + <button mat-menu-item + *ngIf="notifications.length" + id="adf-notification-history-mark-as-read" + class="adf-notification-history-mark-as-read" + title="{{ 'NOTIFICATIONS.MARK_AS_READ' | translate }}" + (click)="markAsRead()"> + <mat-icon class="adf-notification-history-mark-as-read-icon">done_all</mat-icon> + </button> + </div> - <mat-list role="menuitem"> - <ng-container *ngIf="notifications.length; else empty_list_template"> - <mat-list-item *ngFor="let notification of paginatedNotifications" - class="adf-notification-history-menu-item" - (click)="onNotificationClick(notification)"> - <div *ngIf="notification.initiator; else no_avatar" - matListItemAvatar - [outerHTML]="notification.initiator | usernameInitials : 'adf-notification-initiator-pic'"></div> - <ng-template #no_avatar> - <mat-icon matListItemLine - class="adf-notification-history-menu-initiator">{{notification.icon}}</mat-icon> - </ng-template> - <div class="adf-notification-history-menu-item-content"> - <p class="adf-notification-history-menu-text adf-notification-history-menu-message" - *ngFor="let message of notification.messages" - matListItemLine [title]="message">{{ message }}</p> - <p class="adf-notification-history-menu-text adf-notification-history-menu-date" - matListItemLine> {{notification.datetime | adfTimeAgo}} </p> - </div> - </mat-list-item> - </ng-container> - <ng-template #empty_list_template> - <mat-list-item id="adf-notification-history-component-no-message" - class="adf-notification-history-menu-no-message"> - <p class="adf-notification-history-menu-no-message-text" matListItemLine>{{ 'NOTIFICATIONS.NO_MESSAGE' | translate }}</p> - </mat-list-item> - </ng-template> - </mat-list> + <mat-divider/> - <mat-divider /> + <div class="adf-notification-history-item-list"> + <ng-container *ngIf="notifications.length; else empty_list_template"> + <button mat-menu-item + *ngFor="let notification of paginatedNotifications" + (click)="onNotificationClick(notification, $event)" + class="adf-notification-history-menu-item"> + <div class="adf-notification-history-menu-item-content"> + <div *ngIf="notification.initiator; else no_avatar" + [outerHTML]="notification.initiator | usernameInitials : 'adf-notification-initiator-pic'"></div> + <ng-template #no_avatar> + <mat-icon class="adf-notification-history-menu-initiator"> + {{ notification.icon }} + </mat-icon> + </ng-template> + <div class="adf-notification-history-menu-item-content-message"> + <p class="adf-notification-history-menu-text adf-notification-history-menu-message" + *ngFor="let message of notification.messages" + [title]="message">{{ message }}</p> + <p class="adf-notification-history-menu-text adf-notification-history-menu-date" + > {{ notification.datetime | adfTimeAgo }} </p> + </div> + </div> + </button> + </ng-container> + <ng-template #empty_list_template> + <p mat-menu-item id="adf-notification-history-component-no-message" + class="adf-notification-history-menu-no-message-text"> + {{ 'NOTIFICATIONS.NO_MESSAGE' | translate }} + </p> + </ng-template> + </div> - <div class="adf-notification-history-load-more" role="menuitem" *ngIf="hasMoreNotifications()"> - <button mat-button (click)="loadMore()"> - {{ 'NOTIFICATIONS.LOAD_MORE' | translate }} - </button> - </div> - </div> - </mat-menu> -</div> + <mat-divider/> + + <div class="adf-notification-history-load-more" *ngIf="hasMoreNotifications()"> + <button mat-menu-item (click)="loadMore($event)"> + {{ 'NOTIFICATIONS.LOAD_MORE' | translate }} + </button> + </div> +</mat-menu> diff --git a/lib/core/src/lib/notifications/components/notification-history.component.scss b/lib/core/src/lib/notifications/components/notification-history.component.scss index b75bb486ce..f0e5108068 100644 --- a/lib/core/src/lib/notifications/components/notification-history.component.scss +++ b/lib/core/src/lib/notifications/components/notification-history.component.scss @@ -3,37 +3,6 @@ $notification-item-height: 72px; .adf { - &-notification-history-container { - margin-top: 1px; - } - - &-notification-history-list { - .adf-notification-history-menu-item-content-wrapper { - height: 100%; - display: flex; - align-items: center; - } - - /* stylelint-disable selector-class-pattern */ - .mdc-list-item__secondary-text::before { - height: auto; - } - } - - &-notification-history-menu-item-content { - display: flex; - flex-direction: column; - box-sizing: border-box; - overflow: hidden; - padding: 0 0 0 16px; - - p { - line-height: 16px; - margin-bottom: 0; - color: var(--theme-sidenav-user-menu-color); - } - } - &-notification-history-menu-title { font-size: 14px; -webkit-font-smoothing: subpixel-antialiased; @@ -44,6 +13,7 @@ $notification-item-height: 72px; padding: 0; min-width: 40px; height: 40px; + margin-top: 1px; .adf-notification-history-menu_button-icon { margin-right: 0; @@ -54,18 +24,67 @@ $notification-item-height: 72px; } } - &-notification-history-list #{$mat-subheader} { - display: flex; - justify-content: space-between; - align-items: center; - } - - &-notification-history-menu:has(.adf-notification-history-list) { - .adf-notification-history-menu-item { - cursor: pointer; - height: $notification-item-height; + &-notification-history-menu { + .adf-notification-history-list-header { + padding: 10.5px 16px; + display: flex; + justify-content: space-between; align-items: center; - padding-left: 16px; + + .adf-notification-history-mark-as-read { + display: flex; + padding: 10px; + width: auto; + margin: 4px 0; + + &:hover, + &:focus, + &:active { + background: none; + } + + &-icon { + margin: 0; + color: inherit; + } + } + } + + .adf-notification-history-menu-title { + line-height: 28px; + } + + .adf-notification-history-item-list { + padding-top: 8px; + + .adf-notification-history-menu-item { + cursor: pointer; + height: $notification-item-height; + align-items: center; + display: block; + padding: 0 14px; + + &-content { + height: 100%; + display: flex; + align-items: center; + + &-message { + display: flex; + flex-direction: column; + justify-content: center; + box-sizing: border-box; + overflow: hidden; + padding: 0 0 0 16px; + + p { + line-height: normal; + margin: 0; + color: var(--theme-sidenav-user-menu-color); + } + } + } + } } .adf-notification-history-menu-item:focus { @@ -77,20 +96,29 @@ $notification-item-height: 72px; background-color: var(--adf-theme-background-hover-color); } - .adf-notification-history-menu-message:is(p), - .adf-notification-history-menu-no-message:is(p) { + .adf-notification-history-menu-message:is(p) { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; font-size: var(--theme-body-1-font-size); } - .adf-notification-history-menu-no-message-text { - font-size: 16px; + .adf-notification-history-item-list .adf-notification-history-menu-no-message-text { color: var(--theme-sidenav-user-menu-color); - margin-bottom: 0; - -webkit-font-smoothing: subpixel-antialiased; + margin: 0; + padding: 12px 16px; + opacity: inherit; + border: none; + + span { + font-size: 16px; + -webkit-font-smoothing: subpixel-antialiased; + } } .adf-notification-history-menu-date.adf-notification-history-menu-text:is(p) { font-size: var(--theme-caption-font-size); + text-indent: 3px; } .adf-notification-history-menu-initiator { @@ -119,13 +147,11 @@ $notification-item-height: 72px; padding: 10px; button { + justify-content: center; width: 100%; + min-height: 36px; } } - - &-notification-history-mark-as-read { - margin: 4px 0; - } } #{$mat-menu-panel}.adf-notification-history-menu.adf-notification-history-menu-panel { @@ -134,18 +160,5 @@ $notification-item-height: 72px; #{$mat-menu-content} { padding: 0; - - #{$mat-list} { - padding: 8px 0 0; - - #{$mat-list-item-unscoped-content} { - display: flex; - } - - #{$mat-list-item-content} { - display: flex; - align-items: center; - } - } } } diff --git a/lib/core/src/lib/notifications/components/notification-history.component.spec.ts b/lib/core/src/lib/notifications/components/notification-history.component.spec.ts index 16390031ac..5e1524b934 100644 --- a/lib/core/src/lib/notifications/components/notification-history.component.spec.ts +++ b/lib/core/src/lib/notifications/components/notification-history.component.spec.ts @@ -95,7 +95,7 @@ describe('Notification History Component', () => { fixture.whenStable().then(() => { fixture.detectChanges(); expect(overlayContainerElement.querySelector('#adf-notification-history-component-no-message')).toBeNull(); - expect(overlayContainerElement.querySelector('.adf-notification-history-list').innerHTML).toContain('Example Message'); + expect(overlayContainerElement.querySelector('.adf-notification-history-item-list').innerHTML).toContain('Example Message'); done(); }); }); diff --git a/lib/core/src/lib/notifications/components/notification-history.component.ts b/lib/core/src/lib/notifications/components/notification-history.component.ts index c5f68a3233..1f0ab2f415 100644 --- a/lib/core/src/lib/notifications/components/notification-history.component.ts +++ b/lib/core/src/lib/notifications/components/notification-history.component.ts @@ -15,17 +15,7 @@ * limitations under the License. */ -import { - AfterViewInit, - ChangeDetectorRef, - Component, - DestroyRef, - inject, - Input, - OnInit, - ViewChild, - ViewEncapsulation -} from '@angular/core'; +import { AfterViewInit, ChangeDetectorRef, Component, DestroyRef, inject, Input, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; import { NotificationService } from '../services/notification.service'; import { NOTIFICATION_TYPE, NotificationModel } from '../models/notification.model'; import { MatMenuModule, MatMenuTrigger, MenuPositionX, MenuPositionY } from '@angular/material/menu'; @@ -121,22 +111,11 @@ export class NotificationHistoryComponent implements OnInit, AfterViewInit { this.createPagination(); } - onKeyPress(event: KeyboardEvent) { - this.closeUserModal(event); - } - - private closeUserModal($event: KeyboardEvent) { - if ($event.keyCode === 27) { - this.trigger.closeMenu(); - } - } - markAsRead() { this.notifications = []; this.paginatedNotifications = []; this.storageService.removeItem(NotificationHistoryComponent.NOTIFICATION_STORAGE); this.createPagination(); - this.trigger.closeMenu(); } createPagination() { @@ -149,7 +128,8 @@ export class NotificationHistoryComponent implements OnInit, AfterViewInit { this.paginatedNotifications = this.notifications.slice(0, this.pagination.skipCount); } - loadMore() { + loadMore($event: MouseEvent) { + $event.stopPropagation(); this.pagination.skipCount = this.pagination.maxItems + this.pagination.skipCount; this.pagination.hasMoreItems = this.notifications.length > this.pagination.skipCount; this.paginatedNotifications = this.notifications.slice(0, this.pagination.skipCount); @@ -159,7 +139,8 @@ export class NotificationHistoryComponent implements OnInit, AfterViewInit { return this.pagination?.hasMoreItems; } - onNotificationClick(notification: NotificationModel) { + onNotificationClick(notification: NotificationModel, $event: MouseEvent) { + $event.stopPropagation(); if (notification.clickCallBack) { notification.clickCallBack(notification.args); this.trigger.closeMenu(); diff --git a/lib/core/src/lib/styles/_mat-selectors.scss b/lib/core/src/lib/styles/_mat-selectors.scss index 2fe5c4cc06..7509a32d8f 100644 --- a/lib/core/src/lib/styles/_mat-selectors.scss +++ b/lib/core/src/lib/styles/_mat-selectors.scss @@ -87,7 +87,6 @@ $mat-calendar-table-header: '.mat-calendar-table-header'; $mat-calendar-body-disabled: '.mat-calendar-body-disabled'; $mat-toolbar: '.mat-toolbar'; $mat-slide-toggle: '.mat-mdc-slide-toggle'; -$mat-list: '.mat-mdc-list'; $mat-list-item-content: '.mdc-list-item__content'; $mat-list-item-unscoped-content: '.mat-mdc-list-item-unscoped-content'; $mat-text-field-no-label: '.mdc-text-field--no-label'; diff --git a/lib/extensions/src/lib/components/dynamic-component/dynamic.component.spec.ts b/lib/extensions/src/lib/components/dynamic-component/dynamic.component.spec.ts index c6a506ff23..86b2909074 100644 --- a/lib/extensions/src/lib/components/dynamic-component/dynamic.component.spec.ts +++ b/lib/extensions/src/lib/components/dynamic-component/dynamic.component.spec.ts @@ -19,10 +19,11 @@ import { Component, Input, OnChanges, SimpleChange } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; import { DynamicExtensionComponent } from './dynamic.component'; import { ComponentRegisterService } from '../../services/component-register.service'; import { HttpClientModule } from '@angular/common/http'; +import { MatMenuItem } from '@angular/material/menu'; +import { By } from '@angular/platform-browser'; @Component({ selector: 'test-component', @@ -71,9 +72,10 @@ describe('DynamicExtensionComponent', () => { TestBed.resetTestingModule(); }); + const getInnerElement = () => fixture.debugElement.query(By.css('[data-automation-id="found-me"]')); + it('should load the TestComponent', () => { - const innerElement = fixture.debugElement.query(By.css('[data-automation-id="found-me"]')); - expect(innerElement).not.toBeNull(); + expect(getInnerElement()).not.toBeNull(); }); it('should pass through the data', () => { @@ -90,6 +92,12 @@ describe('DynamicExtensionComponent', () => { const testComponent = fixture.debugElement.query(By.css('test-component')).componentInstance; expect(testComponent.data).toBe(data); }); + + it('should assign menuItem from dynamically generated component in ngAfterViewInit', () => { + getInnerElement().componentInstance.menuItem = new MatMenuItem(null, null, null, null, null); + component.ngAfterViewInit(); + expect(component.menuItem).toBeInstanceOf(MatMenuItem); + }); }); describe('Angular life-cycle methods in sub-component', () => { diff --git a/lib/extensions/src/lib/components/dynamic-component/dynamic.component.ts b/lib/extensions/src/lib/components/dynamic-component/dynamic.component.ts index 83a8490990..ed05587238 100644 --- a/lib/extensions/src/lib/components/dynamic-component/dynamic.component.ts +++ b/lib/extensions/src/lib/components/dynamic-component/dynamic.component.ts @@ -15,9 +15,10 @@ * limitations under the License. */ -import { Component, Input, ComponentRef, ViewChild, ViewContainerRef, OnDestroy, OnChanges, SimpleChanges } from '@angular/core'; +import { Component, Input, ComponentRef, ViewChild, ViewContainerRef, OnDestroy, OnChanges, SimpleChanges, AfterViewInit } from '@angular/core'; import { ExtensionService } from '../../services/extension.service'; import { ExtensionComponent } from '../../services/component-register.service'; +import { MatMenuItem } from '@angular/material/menu'; // cSpell:words lifecycle @Component({ @@ -25,7 +26,7 @@ import { ExtensionComponent } from '../../services/component-register.service'; standalone: true, template: `<div #content></div>` }) -export class DynamicExtensionComponent implements OnChanges, OnDestroy { +export class DynamicExtensionComponent implements OnChanges, OnDestroy, AfterViewInit { @ViewChild('content', { read: ViewContainerRef, static: true }) content: ViewContainerRef; @@ -35,6 +36,9 @@ export class DynamicExtensionComponent implements OnChanges, OnDestroy { /** Data for the dynamically-loaded component instance. */ @Input() data: any; + /** Provides the menu item of dynamically-loaded component instance. */ + menuItem: MatMenuItem; + private componentRef: ComponentRef<ExtensionComponent>; private loaded: boolean = false; @@ -61,6 +65,10 @@ export class DynamicExtensionComponent implements OnChanges, OnDestroy { } } + ngAfterViewInit() { + this.menuItem = this.componentRef?.instance?.menuItem; + } + private loadComponent() { const componentType = this.extensions.getComponentById<ExtensionComponent>(this.id); if (componentType) { diff --git a/lib/extensions/src/lib/services/component-register.service.ts b/lib/extensions/src/lib/services/component-register.service.ts index 34348afbcf..2a12815398 100644 --- a/lib/extensions/src/lib/services/component-register.service.ts +++ b/lib/extensions/src/lib/services/component-register.service.ts @@ -16,9 +16,11 @@ */ import { Injectable, Type } from '@angular/core'; +import { MatMenuItem } from '@angular/material/menu'; export interface ExtensionComponent { data: any; + menuItem?: MatMenuItem; } @Injectable({ providedIn: 'root' })