diff --git a/api-goldens/element-ng/application-header/index.api.md b/api-goldens/element-ng/application-header/index.api.md index 53167d8357..fffc8c7d6a 100644 --- a/api-goldens/element-ng/application-header/index.api.md +++ b/api-goldens/element-ng/application-header/index.api.md @@ -13,7 +13,6 @@ import { NavigationExtras } from '@angular/router'; import { OnChanges } from '@angular/core'; import { OnDestroy } from '@angular/core'; import { OnInit } from '@angular/core'; -import * as rxjs from 'rxjs'; import * as _siemens_element_ng_application_header from '@siemens/element-ng/application-header'; import * as _siemens_element_translate_ng_translate from '@siemens/element-translate-ng/translate'; import { SiHeaderDropdownTriggerDirective } from '@siemens/element-ng/header-dropdown'; @@ -78,6 +77,7 @@ export class SiAccountDetailsComponent { // @public export class SiApplicationHeaderComponent implements HeaderWithDropdowns, OnDestroy { + constructor(); readonly expandBreakpoint: _angular_core.InputSignal<"sm" | "md" | "lg" | "xl" | "xxl" | "never">; // (undocumented) readonly launchpad: _angular_core.InputSignal | undefined>; diff --git a/api-goldens/element-ng/header-dropdown/index.api.md b/api-goldens/element-ng/header-dropdown/index.api.md index e81f289710..dfc88cb60e 100644 --- a/api-goldens/element-ng/header-dropdown/index.api.md +++ b/api-goldens/element-ng/header-dropdown/index.api.md @@ -8,11 +8,11 @@ import * as _angular_core from '@angular/core'; import { ConnectedPosition } from '@angular/cdk/overlay'; import { InjectionToken } from '@angular/core'; import { MenuItem } from '@siemens/element-ng/common'; -import { Observable } from 'rxjs'; import { OnChanges } from '@angular/core'; import { OnDestroy } from '@angular/core'; import { OnInit } from '@angular/core'; import * as _siemens_element_ng_header_dropdown from '@siemens/element-ng/header-dropdown'; +import { Signal } from '@angular/core'; import { TemplateRef } from '@angular/core'; // @public @@ -31,6 +31,7 @@ export class SiHeaderDropdownItemComponent { // @public export class SiHeaderDropdownTriggerDirective implements OnChanges, OnInit, OnDestroy { + constructor(); close(options?: { all?: boolean; }): void; diff --git a/api-goldens/element-ng/navbar/index.api.md b/api-goldens/element-ng/navbar/index.api.md index 7cbf0a1c3d..ac44f6b052 100644 --- a/api-goldens/element-ng/navbar/index.api.md +++ b/api-goldens/element-ng/navbar/index.api.md @@ -16,7 +16,6 @@ import { MenuItem } from '@siemens/element-ng/common'; import { OnChanges } from '@angular/core'; import { OnDestroy } from '@angular/core'; import { OnInit } from '@angular/core'; -import * as rxjs from 'rxjs'; import { SiApplicationHeaderComponent } from '@siemens/element-ng/application-header'; import * as _siemens_element_translate_ng_translate from '@siemens/element-translate-ng/translate'; import { SiHeaderCollapsibleActionsComponent } from '@siemens/element-ng/application-header'; diff --git a/projects/element-ng/application-header/si-application-header.component.ts b/projects/element-ng/application-header/si-application-header.component.ts index 7b2430773d..f2a9338e46 100644 --- a/projects/element-ng/application-header/si-application-header.component.ts +++ b/projects/element-ng/application-header/si-application-header.component.ts @@ -8,6 +8,7 @@ import { NgTemplateOutlet } from '@angular/common'; import { ChangeDetectionStrategy, Component, + effect, ElementRef, inject, Injector, @@ -15,8 +16,10 @@ import { OnDestroy, signal, TemplateRef, + untracked, viewChild } from '@angular/core'; +import { toObservable, toSignal } from '@angular/core/rxjs-interop'; import { elementMenu, elementThumbnails } from '@siemens/element-icons'; import { HeaderWithDropdowns, @@ -26,8 +29,8 @@ import { import { addIcons, SiIconComponent } from '@siemens/element-ng/icon'; import { BOOTSTRAP_BREAKPOINTS, Breakpoints } from '@siemens/element-ng/resize-observer'; import { SiTranslatePipe, t } from '@siemens/element-translate-ng/translate'; -import { defer, of, Subject } from 'rxjs'; -import { map, skip, takeUntil } from 'rxjs/operators'; +import { of, Subject } from 'rxjs'; +import { map, switchMap } from 'rxjs/operators'; /** Root component for the application header. */ @Component({ @@ -83,20 +86,30 @@ export class SiApplicationHeaderComponent implements HeaderWithDropdowns, OnDest }); /** @internal */ - // defer is required to re-check the current breakpoint as it may change. - readonly inlineDropdown = defer(() => { - const expandBreakpoint = this.expandBreakpoint(); - if (expandBreakpoint === 'never') { - return of(true); - } - return this.breakpointObserver - .observe( - `(min-width: ${ - BOOTSTRAP_BREAKPOINTS[(expandBreakpoint + 'Minimum') as keyof Breakpoints] - }px)` - ) - .pipe(map(({ matches }) => !matches)); - }); + readonly inlineDropdown = toSignal( + toObservable(this.expandBreakpoint).pipe( + switchMap(expandBreakpoint => { + if (expandBreakpoint === 'never') { + return of(true); + } + return this.breakpointObserver + .observe( + `(min-width: ${ + BOOTSTRAP_BREAKPOINTS[(expandBreakpoint + 'Minimum') as keyof Breakpoints] + }px)` + ) + .pipe(map(({ matches }) => !matches)); + }) + ), + { initialValue: false } + ); + + constructor() { + effect(() => { + this.inlineDropdown(); + untracked(() => this.closeMobileMenus.next()); + }); + } ngOnDestroy(): void { this.closeMobileSub.unsubscribe(); @@ -177,9 +190,6 @@ export class SiApplicationHeaderComponent implements HeaderWithDropdowns, OnDest this.closeMobileMenus.next(); this.mobileNavigationExpanded.set(true); this.dropdownOpened(); - this.inlineDropdown - .pipe(skip(1), takeUntil(this.closeMobileMenus)) - .subscribe(() => this.closeMobileMenus.next()); this.focusTrap().focusTrap.focusFirstTabbableElementWhenReady(); } } diff --git a/projects/element-ng/application-header/si-header-collapsible-actions.component.ts b/projects/element-ng/application-header/si-header-collapsible-actions.component.ts index 919d653229..c2037f9f03 100644 --- a/projects/element-ng/application-header/si-header-collapsible-actions.component.ts +++ b/projects/element-ng/application-header/si-header-collapsible-actions.component.ts @@ -17,8 +17,6 @@ import { elementOptionsVertical } from '@siemens/element-icons'; import { SI_HEADER_DROPDOWN_OPTIONS } from '@siemens/element-ng/header-dropdown'; import { addIcons, SiIconComponent } from '@siemens/element-ng/icon'; import { SiTranslatePipe, t } from '@siemens/element-translate-ng/translate'; -import { Subscription } from 'rxjs'; -import { skip, takeUntil } from 'rxjs/operators'; import { SiApplicationHeaderComponent } from './si-application-header.component'; @@ -61,11 +59,9 @@ export class SiHeaderCollapsibleActionsComponent implements OnDestroy { private readonly focusTrap = viewChild.required(CdkTrapFocus); private header = inject(SiApplicationHeaderComponent); private closeMobileSub = this.header.closeMobileMenus.subscribe(() => this.closeMobile()); - private inlineChangeSubscription?: Subscription; ngOnDestroy(): void { this.closeMobileSub.unsubscribe(); - this.inlineChangeSubscription?.unsubscribe(); } protected toggleMobileExpanded(): void { @@ -86,9 +82,6 @@ export class SiHeaderCollapsibleActionsComponent implements OnDestroy { this.header.closeMobileMenus.next(); this.header.dropdownOpened(); this.mobileExpanded.set(true); - this.inlineChangeSubscription = this.header.inlineDropdown - .pipe(skip(1), takeUntil(this.header.closeMobileMenus)) - .subscribe(() => this.header.closeMobileMenus.next()); this.focusTrap().focusTrap.focusFirstTabbableElementWhenReady(); } } diff --git a/projects/element-ng/header-dropdown/si-header-dropdown-trigger.directive.ts b/projects/element-ng/header-dropdown/si-header-dropdown-trigger.directive.ts index bebc1d810f..58dcbd2d45 100644 --- a/projects/element-ng/header-dropdown/si-header-dropdown-trigger.directive.ts +++ b/projects/element-ng/header-dropdown/si-header-dropdown-trigger.directive.ts @@ -8,6 +8,7 @@ import { Component, ComponentRef, Directive, + effect, ElementRef, EmbeddedViewRef, inject, @@ -18,10 +19,11 @@ import { OnInit, output, TemplateRef, + untracked, ViewContainerRef } from '@angular/core'; -import { of, Subject } from 'rxjs'; -import { filter, skip, take, takeUntil } from 'rxjs/operators'; +import { Subject } from 'rxjs'; +import { filter, take, takeUntil } from 'rxjs/operators'; import { SI_HEADER_WITH_DROPDOWNS } from './si-header.model'; @@ -96,6 +98,17 @@ export class SiHeaderDropdownTriggerDirective implements OnChanges, OnInit, OnDe private headerAnchorComponentRef?: ComponentRef; + constructor() { + effect(() => { + const inline = this.navbar?.inlineDropdown() ?? false; + untracked(() => { + if (this._isOpen && inline === this.isOverlay) { + this.close(); + } + }); + }); + } + /** Whether the dropdown is open. */ get isOpen(): boolean { return this._isOpen; @@ -129,15 +142,10 @@ export class SiHeaderDropdownTriggerDirective implements OnChanges, OnInit, OnDe return; } - (this.navbar?.inlineDropdown ?? of(false)).pipe(take(1)).subscribe(inline => { - this._isOpen = true; - if (!inline) { - this.attachDropdownOverlay(); - } - this.navbar?.inlineDropdown - ?.pipe(skip(1), takeUntil(this.dropdownClose)) - .subscribe(() => this.close()); - }); + this._isOpen = true; + if (!(this.navbar?.inlineDropdown() ?? false)) { + this.attachDropdownOverlay(); + } if (this.parent) { this.parent.openSubmenu = this; diff --git a/projects/element-ng/header-dropdown/si-header-dropdown.directive.spec.ts b/projects/element-ng/header-dropdown/si-header-dropdown.directive.spec.ts index 6f58cb6c9a..6ccf6a0f64 100644 --- a/projects/element-ng/header-dropdown/si-header-dropdown.directive.spec.ts +++ b/projects/element-ng/header-dropdown/si-header-dropdown.directive.spec.ts @@ -4,9 +4,8 @@ */ import { HarnessLoader } from '@angular/cdk/testing'; import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; -import { Component, viewChild } from '@angular/core'; +import { Component, signal, viewChild } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { BehaviorSubject } from 'rxjs'; import { SiHeaderDropdownItemComponent } from './si-header-dropdown-item.component'; import { SiHeaderDropdownTriggerDirective } from './si-header-dropdown-trigger.directive'; @@ -43,7 +42,7 @@ import { SiHeaderDropdownTriggerHarness } from './testing/si-header-dropdown-tri providers: [{ provide: SI_HEADER_WITH_DROPDOWNS, useExisting: TestHostComponent }] }) class TestHostComponent implements HeaderWithDropdowns { - inlineDropdown = new BehaviorSubject(false); + readonly inlineDropdown = signal(false); readonly trigger1 = viewChild.required('trigger1', { read: SiHeaderDropdownTriggerDirective }); readonly trigger2 = viewChild.required('trigger2', { read: SiHeaderDropdownTriggerDirective }); } @@ -95,7 +94,7 @@ describe('SiHeaderDropdown', () => { it('should close on resize', async () => { await trigger1Harness.toggle(); expect(await trigger1Harness.isOpen()).toBe(true); - fixture.componentInstance.inlineDropdown.next(false); + fixture.componentInstance.inlineDropdown.set(true); expect(await trigger1Harness.isOpen()).toBe(false); }); @@ -130,7 +129,7 @@ describe('SiHeaderDropdown', () => { }); describe('in mobile mode', () => { - beforeEach(() => fixture.componentInstance.inlineDropdown.next(true)); + beforeEach(() => fixture.componentInstance.inlineDropdown.set(true)); it('should open inline in mobile view', async () => { await trigger1Harness.toggle(); @@ -155,7 +154,7 @@ describe('SiHeaderDropdown', () => { it('should close on resize', async () => { await trigger1Harness.toggle(); expect(await trigger1Harness.isOpen()).toBe(true); - fixture.componentInstance.inlineDropdown.next(true); + fixture.componentInstance.inlineDropdown.set(false); expect(await trigger1Harness.isOpen()).toBe(false); }); diff --git a/projects/element-ng/header-dropdown/si-header.model.ts b/projects/element-ng/header-dropdown/si-header.model.ts index 9b8245866b..56d2948a38 100644 --- a/projects/element-ng/header-dropdown/si-header.model.ts +++ b/projects/element-ng/header-dropdown/si-header.model.ts @@ -3,8 +3,7 @@ * SPDX-License-Identifier: MIT */ import { ConnectedPosition } from '@angular/cdk/overlay'; -import { InjectionToken } from '@angular/core'; -import { Observable } from 'rxjs'; +import { InjectionToken, Signal } from '@angular/core'; import { SiHeaderDropdownTriggerDirective } from './si-header-dropdown-trigger.directive'; @@ -13,7 +12,7 @@ export interface HeaderWithDropdowns { /** Called whenever an item is triggered that is not opening another dropdown. */ onDropdownItemTriggered?(): void; /** Whether the dropdown should be opened inline. */ - inlineDropdown?: Observable; + inlineDropdown: Signal; /** The position of the dropdown if opened in an overlay. */ overlayPosition?: ConnectedPosition[]; /** Called whenever a dropdown is opened **/ diff --git a/projects/element-ng/navbar/si-navbar-primary/si-navbar-primary.component.ts b/projects/element-ng/navbar/si-navbar-primary/si-navbar-primary.component.ts index c7e05345e3..debbc32a35 100644 --- a/projects/element-ng/navbar/si-navbar-primary/si-navbar-primary.component.ts +++ b/projects/element-ng/navbar/si-navbar-primary/si-navbar-primary.component.ts @@ -7,6 +7,7 @@ import { NgTemplateOutlet } from '@angular/common'; import { booleanAttribute, Component, + computed, input, OnChanges, output, @@ -38,7 +39,6 @@ import { } from '@siemens/element-ng/header-dropdown'; import { Link, SiLinkDirective } from '@siemens/element-ng/link'; import { SiTranslatePipe, t } from '@siemens/element-translate-ng/translate'; -import { defer } from 'rxjs'; import { AccountItem } from '../account.model'; import { AppItem, AppItemCategory } from './si-navbar-primary.model'; @@ -277,8 +277,7 @@ export class SiNavbarPrimaryComponent implements OnChanges, HeaderWithDropdowns protected active?: MenuItem; /** @internal */ - // defer is required as header is not available at the time of creation.` - readonly inlineDropdown = defer(() => this.header().inlineDropdown); + readonly inlineDropdown = computed(() => this.header().inlineDropdown()); /** @internal */ onDropdownItemTriggered(): void {