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 = new EventEmitter(); + @ViewChildren(MatMenuItem) + menuItems: QueryList; + languages$: Observable; 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; + 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(); + 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'; ` }) -export class LanguagePickerComponent { +export class LanguagePickerComponent implements AfterViewInit { @Output() public changedLanguage = new EventEmitter(); + + @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(); + 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 @@ -
- - -
-
- {{ 'NOTIFICATIONS.TITLE' | translate }} - -
+ - + +
+ {{ 'NOTIFICATIONS.TITLE' | translate }} + +
- - - -
- - {{notification.icon}} - -
-

{{ message }}

-

{{notification.datetime | adfTimeAgo}}

-
-
-
- - -

{{ 'NOTIFICATIONS.NO_MESSAGE' | translate }}

-
-
-
+ - +
+ + + + +

+ {{ 'NOTIFICATIONS.NO_MESSAGE' | translate }} +

+
+
- -
-
-
+ + +
+ +
+ 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: `
` }) -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; 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(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' })