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="&#8288;"
-                  [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="&#8288;"
+              [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' })