From 48402886409e35ef7ed6fa211ce3f54da8030822 Mon Sep 17 00:00:00 2001 From: William Martin Date: Wed, 13 Nov 2024 12:53:40 -0500 Subject: [PATCH] use client abstraction pattern; add settings toggle; use observable instead of signal due to state provider --- apps/browser/src/_locales/en/messages.json | 6 ++++ .../layout/popup-compact-mode.service.ts | 31 +++++++++++++++++++ apps/browser/src/popup/app.component.ts | 4 +++ .../src/popup/services/services.module.ts | 8 ++++- .../vault-list-items-container.component.html | 4 +-- .../vault-list-items-container.component.ts | 21 +++++-------- .../settings/appearance-v2.component.html | 8 +++++ .../settings/appearance-v2.component.spec.ts | 8 +++++ .../popup/settings/appearance-v2.component.ts | 21 +++++++++++-- libs/components/src/index.ts | 2 +- .../src/shared/compact-mode.service.ts | 11 +++++++ .../src/shared/design-system.service.ts | 20 ------------ libs/components/src/stories/compact-mode.mdx | 4 +-- 13 files changed, 106 insertions(+), 42 deletions(-) create mode 100644 apps/browser/src/platform/popup/layout/popup-compact-mode.service.ts create mode 100644 libs/components/src/shared/compact-mode.service.ts delete mode 100644 libs/components/src/shared/design-system.service.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 8e52b1ba006..2ce31f933bc 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -4776,5 +4776,11 @@ }, "generatedPassword": { "message": "Generated password" + }, + "compactMode": { + "message": "Compact mode" + }, + "beta": { + "message": "Beta" } } diff --git a/apps/browser/src/platform/popup/layout/popup-compact-mode.service.ts b/apps/browser/src/platform/popup/layout/popup-compact-mode.service.ts new file mode 100644 index 00000000000..ffcd2321b15 --- /dev/null +++ b/apps/browser/src/platform/popup/layout/popup-compact-mode.service.ts @@ -0,0 +1,31 @@ +import { inject, Injectable } from "@angular/core"; +import { map, Observable } from "rxjs"; + +import { GlobalStateProvider, KeyDefinition, THEMING_DISK } from "@bitwarden/common/platform/state"; +import { CompactModeService } from "@bitwarden/components"; + +const COMPACT_MODE = new KeyDefinition(THEMING_DISK, "compactMode", { + deserializer: (s) => s, +}); + +/** + * Service to persist Compact Mode to state / user settings. + **/ +@Injectable({ providedIn: "root" }) +export class PopupCompactModeService implements CompactModeService { + private state = inject(GlobalStateProvider).get(COMPACT_MODE); + + enabled$: Observable = this.state.state$.pipe(map((state) => state ?? false)); + + init() { + this.enabled$.subscribe((enabled) => { + enabled + ? document.body.classList.add("tw-bit-compact") + : document.body.classList.remove("tw-bit-compact"); + }); + } + + async setEnabled(enabled: boolean) { + await this.state.update(() => enabled); + } +} diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts index 3c8752f3a76..f12815b917c 100644 --- a/apps/browser/src/popup/app.component.ts +++ b/apps/browser/src/popup/app.component.ts @@ -24,6 +24,7 @@ import { } from "@bitwarden/components"; import { flagEnabled } from "../platform/flags"; +import { PopupCompactModeService } from "../platform/popup/layout/popup-compact-mode.service"; import { PopupViewCacheService } from "../platform/popup/view-cache/popup-view-cache.service"; import { initPopupClosedListener } from "../platform/services/popup-view-cache-background.service"; import { BrowserSendStateService } from "../tools/popup/services/browser-send-state.service"; @@ -42,6 +43,7 @@ import { DesktopSyncVerificationDialogComponent } from "./components/desktop-syn }) export class AppComponent implements OnInit, OnDestroy { private viewCacheService = inject(PopupViewCacheService); + private compactModeService = inject(PopupCompactModeService); private lastActivity: Date; private activeUserId: UserId; @@ -93,6 +95,8 @@ export class AppComponent implements OnInit, OnDestroy { initPopupClosedListener(); await this.viewCacheService.init(); + this.compactModeService.init(); + // Component states must not persist between closing and reopening the popup, otherwise they become dead objects // Clear them aggressively to make sure this doesn't occur await this.clearComponentStates(); diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 14ebfb4a175..91a13f4c0f0 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -97,7 +97,7 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi import { FolderService as FolderServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service"; import { TotpService } from "@bitwarden/common/vault/services/totp.service"; -import { DialogService, ToastService } from "@bitwarden/components"; +import { CompactModeService, DialogService, ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { BiometricStateService, BiometricsService, KeyService } from "@bitwarden/key-management"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -118,6 +118,7 @@ import { ChromeMessageSender } from "../../platform/messaging/chrome-message.sen import { OffscreenDocumentService } from "../../platform/offscreen-document/abstractions/offscreen-document"; import { DefaultOffscreenDocumentService } from "../../platform/offscreen-document/offscreen-document.service"; import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; +import { PopupCompactModeService } from "../../platform/popup/layout/popup-compact-mode.service"; import { BrowserFileDownloadService } from "../../platform/popup/services/browser-file-download.service"; import { PopupViewCacheService } from "../../platform/popup/view-cache/popup-view-cache.service"; import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service"; @@ -618,6 +619,11 @@ const safeProviders: SafeProvider[] = [ useClass: ExtensionAnonLayoutWrapperDataService, deps: [], }), + safeProvider({ + provide: CompactModeService, + useExisting: PopupCompactModeService, + deps: [], + }), ]; @NgModule({ diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html index cf2a0ad0d33..a493ea2171c 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html @@ -20,7 +20,7 @@ @@ -30,7 +30,7 @@ (click)="onViewCipher(cipher)" (dblclick)="launchCipher(cipher)" [appA11yTitle]="'viewItemTitle' | i18n: cipher.name" - class="{{ ItemHeightClass }}" + class="{{ itemHeightClass }}" > {{ cipher.name }} diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts index 1f3de5ecff1..fa2c8a440e0 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts @@ -1,15 +1,8 @@ import { ScrollingModule } from "@angular/cdk/scrolling"; import { CommonModule } from "@angular/common"; -import { - booleanAttribute, - Component, - computed, - EventEmitter, - inject, - Input, - Output, -} from "@angular/core"; +import { booleanAttribute, Component, EventEmitter, inject, Input, Output } from "@angular/core"; import { Router, RouterLink } from "@angular/router"; +import { map } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -18,7 +11,7 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { BadgeModule, ButtonModule, - DesignSystemService, + CompactModeService, IconButtonModule, ItemModule, SectionComponent, @@ -56,12 +49,12 @@ import { ItemMoreOptionsComponent } from "../item-more-options/item-more-options standalone: true, }) export class VaultListItemsContainerComponent { - private designSystemService = inject(DesignSystemService); + private compactModeService = inject(CompactModeService); /** * The class used to set the height of a bit item's inner content. */ - protected readonly ItemHeightClass = `tw-h-[52px]`; + protected readonly itemHeightClass = `tw-h-[52px]`; /** * The height of a bit item in pixels. Includes any margin, padding, or border. Used by the virtual scroll @@ -72,8 +65,8 @@ export class VaultListItemsContainerComponent { * * Compact mode: 52px + 1px border = 53px */ - protected readonly ItemHeight = computed(() => - this.designSystemService.compactMode() ? 53 : 59, + protected readonly itemHeight$ = this.compactModeService.enabled$.pipe( + map((enabled) => (enabled ? 53 : 59)), ); /** diff --git a/apps/browser/src/vault/popup/settings/appearance-v2.component.html b/apps/browser/src/vault/popup/settings/appearance-v2.component.html index b267e1c5cb2..466cffa216a 100644 --- a/apps/browser/src/vault/popup/settings/appearance-v2.component.html +++ b/apps/browser/src/vault/popup/settings/appearance-v2.component.html @@ -18,6 +18,14 @@ + + + {{ "compactMode" | i18n }} + {{ "beta" | i18n }} + + {{ "showNumberOfAutofillSuggestions" | i18n }} diff --git a/apps/browser/src/vault/popup/settings/appearance-v2.component.spec.ts b/apps/browser/src/vault/popup/settings/appearance-v2.component.spec.ts index bbd210b65a3..b2da3245b5c 100644 --- a/apps/browser/src/vault/popup/settings/appearance-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/settings/appearance-v2.component.spec.ts @@ -13,6 +13,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { ThemeType } from "@bitwarden/common/platform/enums"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; +import { PopupCompactModeService } from "../../../platform/popup/layout/popup-compact-mode.service"; import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; @@ -43,10 +44,12 @@ describe("AppearanceV2Component", () => { const enableBadgeCounter$ = new BehaviorSubject(true); const selectedTheme$ = new BehaviorSubject(ThemeType.Nord); const enableRoutingAnimation$ = new BehaviorSubject(true); + const enableCompactMode$ = new BehaviorSubject(false); const setSelectedTheme = jest.fn().mockResolvedValue(undefined); const setShowFavicons = jest.fn().mockResolvedValue(undefined); const setEnableBadgeCounter = jest.fn().mockResolvedValue(undefined); const setEnableRoutingAnimation = jest.fn().mockResolvedValue(undefined); + const setEnableCompactMode = jest.fn().mockResolvedValue(undefined); beforeEach(async () => { setSelectedTheme.mockClear(); @@ -71,6 +74,10 @@ describe("AppearanceV2Component", () => { provide: BadgeSettingsServiceAbstraction, useValue: { enableBadgeCounter$, setEnableBadgeCounter }, }, + { + provide: PopupCompactModeService, + useValue: { enabled$: enableCompactMode$, setEnabled: setEnableCompactMode }, + }, ], }) .overrideComponent(AppearanceV2Component, { @@ -94,6 +101,7 @@ describe("AppearanceV2Component", () => { enableFavicon: true, enableBadgeCounter: true, theme: ThemeType.Nord, + enableCompactMode: false, }); }); diff --git a/apps/browser/src/vault/popup/settings/appearance-v2.component.ts b/apps/browser/src/vault/popup/settings/appearance-v2.component.ts index 9d600ec83e8..c20bc48bfea 100644 --- a/apps/browser/src/vault/popup/settings/appearance-v2.component.ts +++ b/apps/browser/src/vault/popup/settings/appearance-v2.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from "@angular/common"; -import { Component, DestroyRef, OnInit } from "@angular/core"; +import { Component, DestroyRef, inject, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormBuilder, ReactiveFormsModule } from "@angular/forms"; import { firstValueFrom } from "rxjs"; @@ -12,12 +12,13 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { ThemeType } from "@bitwarden/common/platform/enums"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; -import { CheckboxModule } from "@bitwarden/components"; +import { BadgeModule, CheckboxModule } from "@bitwarden/components"; import { CardComponent } from "../../../../../../libs/components/src/card/card.component"; import { FormFieldModule } from "../../../../../../libs/components/src/form-field/form-field.module"; import { SelectModule } from "../../../../../../libs/components/src/select/select.module"; import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; +import { PopupCompactModeService } from "../../../platform/popup/layout/popup-compact-mode.service"; import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; @@ -35,14 +36,18 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co SelectModule, ReactiveFormsModule, CheckboxModule, + BadgeModule, ], }) export class AppearanceV2Component implements OnInit { + private compactModeService = inject(PopupCompactModeService); + appearanceForm = this.formBuilder.group({ enableFavicon: false, enableBadgeCounter: true, theme: ThemeType.System, enableAnimations: true, + enableCompactMode: false, }); /** To avoid flashes of inaccurate values, only show the form after the entire form is populated. */ @@ -75,6 +80,7 @@ export class AppearanceV2Component implements OnInit { const enableAnimations = await firstValueFrom( this.animationControlService.enableRoutingAnimation$, ); + const enableCompactMode = await firstValueFrom(this.compactModeService.enabled$); // Set initial values for the form this.appearanceForm.setValue({ @@ -82,6 +88,7 @@ export class AppearanceV2Component implements OnInit { enableBadgeCounter, theme, enableAnimations, + enableCompactMode, }); this.formLoading = false; @@ -109,6 +116,12 @@ export class AppearanceV2Component implements OnInit { .subscribe((enableBadgeCounter) => { void this.updateAnimations(enableBadgeCounter); }); + + this.appearanceForm.controls.enableCompactMode.valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((enableCompactMode) => { + void this.updateCompactMode(enableCompactMode); + }); } async updateFavicon(enableFavicon: boolean) { @@ -127,4 +140,8 @@ export class AppearanceV2Component implements OnInit { async updateAnimations(enableAnimations: boolean) { await this.animationControlService.setEnableRoutingAnimation(enableAnimations); } + + async updateCompactMode(enableCompactMode: boolean) { + await this.compactModeService.setEnabled(enableCompactMode); + } } diff --git a/libs/components/src/index.ts b/libs/components/src/index.ts index 526b2001b0f..551b91c3f41 100644 --- a/libs/components/src/index.ts +++ b/libs/components/src/index.ts @@ -30,7 +30,7 @@ export * from "./radio-button"; export * from "./search"; export * from "./section"; export * from "./select"; -export * from "./shared/design-system.service"; +export * from "./shared/compact-mode.service"; export * from "./table"; export * from "./tabs"; export * from "./toast"; diff --git a/libs/components/src/shared/compact-mode.service.ts b/libs/components/src/shared/compact-mode.service.ts new file mode 100644 index 00000000000..e6cafb12d1a --- /dev/null +++ b/libs/components/src/shared/compact-mode.service.ts @@ -0,0 +1,11 @@ +import { Observable } from "rxjs"; + +/** Global config for the Bitwarden Design System */ +export abstract class CompactModeService { + /** + * When true, enables "compact mode". + * + * Component authors can also hook into compact mode with the `bit-compact:` Tailwind variant. + **/ + enabled$: Observable; +} diff --git a/libs/components/src/shared/design-system.service.ts b/libs/components/src/shared/design-system.service.ts deleted file mode 100644 index 8ac9b011846..00000000000 --- a/libs/components/src/shared/design-system.service.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { effect, Injectable, signal, WritableSignal } from "@angular/core"; - -/** Global config for the Bitwarden Design System */ -@Injectable({ providedIn: "root" }) -export class DesignSystemService { - /** - * When true, enables "compact mode". - * - * Component authors can hook into compact mode with the `bit-compact:` Tailwind variant. - **/ - compactMode: WritableSignal = signal(false); - - constructor() { - effect(() => { - this.compactMode() - ? document.body.classList.add("tw-bit-compact") - : document.body.classList.remove("tw-bit-compact"); - }); - } -} diff --git a/libs/components/src/stories/compact-mode.mdx b/libs/components/src/stories/compact-mode.mdx index f5b77bdccad..800bc34cf6a 100644 --- a/libs/components/src/stories/compact-mode.mdx +++ b/libs/components/src/stories/compact-mode.mdx @@ -31,8 +31,8 @@ following example, the paragraph's padding is reduced when compact mode is enabl ## Service -To get/set compact mode in TypeScript, the `DesignSystemService` exposes a `compactMode` signal. -However, using the Tailwind variant should be preferred as it is more performant. +To get/set compact mode in TypeScript, the `CompactModeService` exposes a `enabled$` observable. +However, styling with the Tailwind variant should be used when possible as it is more performant. ## Examples