1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-22 20:34:04 +00:00

conditionally render app-header in portal/slot

This commit is contained in:
Bryan Cunningham
2025-11-26 12:37:04 -05:00
parent a8d955e6c7
commit e2309ea322
4 changed files with 84 additions and 5 deletions

View File

@@ -1,5 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { TemplatePortal } from "@angular/cdk/portal";
import {
Component,
DestroyRef,
@@ -9,6 +10,8 @@ import {
Type,
ViewChild,
ViewContainerRef,
TemplateRef,
AfterViewInit,
} from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { Router } from "@angular/router";
@@ -84,6 +87,7 @@ import { PremiumComponent } from "../billing/app/accounts/premium.component";
import { MenuAccount, MenuUpdateRequest } from "../main/menu/menu.updater";
import { SettingsComponent } from "./accounts/settings.component";
import { DesktopHeaderService } from "./layout/desktop-header.service";
import { ExportDesktopComponent } from "./tools/export/export-desktop.component";
import { CredentialGeneratorComponent } from "./tools/generator/credential-generator.component";
import { ImportDesktopComponent } from "./tools/import/import-desktop.component";
@@ -104,7 +108,12 @@ const SyncInterval = 6 * 60 * 60 * 1000; // 6 hours
<ng-template #exportVault></ng-template>
<ng-template #appGenerator></ng-template>
<ng-template #loginApproval></ng-template>
<app-header></app-header>
<ng-template #headerPortal>
<app-header></app-header>
</ng-template>
@if (!headerInPortal()) {
<app-header></app-header>
}
<div id="container">
<div class="loading" *ngIf="loading">
@@ -117,7 +126,7 @@ const SyncInterval = 6 * 60 * 60 * 1000; // 6 hours
`,
standalone: false,
})
export class AppComponent implements OnInit, OnDestroy {
export class AppComponent implements OnInit, OnDestroy, AfterViewInit {
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@ViewChild("settings", { read: ViewContainerRef, static: true }) settingsRef: ViewContainerRef;
@@ -140,9 +149,15 @@ export class AppComponent implements OnInit, OnDestroy {
// eslint-disable-next-line @angular-eslint/prefer-signals
@ViewChild("loginApproval", { read: ViewContainerRef, static: true })
loginApprovalModalRef: ViewContainerRef;
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@ViewChild("headerPortal", { read: TemplateRef, static: true })
headerPortalRef: TemplateRef<unknown>;
loading = false;
protected headerInPortal = this.desktopHeaderService.isAttached;
private lastActivity: Date = null;
private modal: ModalRef = null;
private idleTimer: number = null;
@@ -197,6 +212,8 @@ export class AppComponent implements OnInit, OnDestroy {
private readonly tokenService: TokenService,
private desktopAutotypeDefaultSettingPolicy: DesktopAutotypeDefaultSettingPolicy,
private readonly lockService: LockService,
private readonly desktopHeaderService: DesktopHeaderService,
private readonly viewContainerRef: ViewContainerRef,
) {
this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe();
@@ -204,6 +221,11 @@ export class AppComponent implements OnInit, OnDestroy {
this.destroyRef.onDestroy(() => langSubscription.unsubscribe());
}
ngAfterViewInit() {
const headerPortal = new TemplatePortal(this.headerPortalRef, this.viewContainerRef);
this.desktopHeaderService.setHeader(headerPortal);
}
ngOnInit() {
this.accountService.activeAccount$.pipe(takeUntil(this.destroy$)).subscribe((account) => {
this.activeUserId = account?.id;
@@ -519,6 +541,7 @@ export class AppComponent implements OnInit, OnDestroy {
}
ngOnDestroy() {
this.desktopHeaderService.clearHeader();
this.destroy$.next();
this.destroy$.complete();
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);

View File

@@ -0,0 +1,26 @@
import { Portal } from "@angular/cdk/portal";
import { Injectable, signal } from "@angular/core";
@Injectable({ providedIn: "root" })
export class DesktopHeaderService {
private readonly _portal = signal<Portal<unknown> | undefined>(undefined);
private readonly _isAttached = signal(false);
/** The portal to display in the desktop header slot */
portal = this._portal.asReadonly();
/** Whether the header is currently attached to a portal outlet */
isAttached = this._isAttached.asReadonly();
setHeader(portal: Portal<unknown>) {
this._portal.set(portal);
}
clearHeader() {
this._portal.set(undefined);
}
setAttached(attached: boolean) {
this._isAttached.set(attached);
}
}

View File

@@ -1,4 +1,11 @@
<bit-layout>
<ng-template
[cdkPortalOutlet]="headerPortal()"
(attached)="onPortalAttached()"
(detached)="onPortalDetached()"
slot="desktop-header"
></ng-template>
<app-side-nav slot="side-nav">
<bit-nav-logo [openIcon]="logo" route="." [label]="'passwordManager' | i18n"></bit-nav-logo>

View File

@@ -1,18 +1,41 @@
import { ChangeDetectionStrategy, Component } from "@angular/core";
import { PortalModule } from "@angular/cdk/portal";
import { ChangeDetectionStrategy, Component, inject, OnDestroy } from "@angular/core";
import { RouterModule } from "@angular/router";
import { PasswordManagerLogo } from "@bitwarden/assets/svg";
import { LayoutComponent, NavigationModule } from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common";
import { DesktopHeaderService } from "./desktop-header.service";
import { DesktopSideNavComponent } from "./desktop-side-nav.component";
@Component({
selector: "app-layout",
imports: [RouterModule, I18nPipe, LayoutComponent, NavigationModule, DesktopSideNavComponent],
imports: [
RouterModule,
I18nPipe,
LayoutComponent,
NavigationModule,
DesktopSideNavComponent,
PortalModule,
],
templateUrl: "./desktop-layout.component.html",
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DesktopLayoutComponent {
export class DesktopLayoutComponent implements OnDestroy {
protected readonly logo = PasswordManagerLogo;
protected readonly desktopHeaderService = inject(DesktopHeaderService);
protected readonly headerPortal = this.desktopHeaderService.portal;
protected onPortalAttached() {
this.desktopHeaderService.setAttached(true);
}
protected onPortalDetached() {
this.desktopHeaderService.setAttached(false);
}
ngOnDestroy() {
this.desktopHeaderService.setAttached(false);
}
}