From bb0912154dfcfc9e5d11f53c547aa87613c47ba7 Mon Sep 17 00:00:00 2001 From: Will Martin Date: Mon, 25 Nov 2024 14:43:31 -0500 Subject: [PATCH] [CL-508] extension width setting (#12040) --- apps/browser/src/_locales/en/messages.json | 9 +++ .../src/platform/popup/browser-popup-utils.ts | 3 +- .../popup/layout/popup-layout.stories.ts | 22 +++++++ .../popup/layout/popup-width.service.ts | 63 +++++++++++++++++++ apps/browser/src/popup/app.component.ts | 3 + apps/browser/src/popup/main.ts | 4 +- apps/browser/src/popup/scss/base.scss | 2 +- .../settings/appearance-v2.component.html | 5 ++ .../settings/appearance-v2.component.spec.ts | 11 ++++ .../popup/settings/appearance-v2.component.ts | 28 ++++++++- .../src/platform/state/state-definitions.ts | 4 ++ libs/components/src/select/index.ts | 1 + 12 files changed, 151 insertions(+), 4 deletions(-) create mode 100644 apps/browser/src/platform/popup/layout/popup-width.service.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 316bb23c4f8..ab1c6377e6a 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -4888,5 +4888,14 @@ }, "beta": { "message": "Beta" + }, + "extensionWidth": { + "message": "Extension width" + }, + "wide": { + "message": "Wide" + }, + "extraWide": { + "message": "Extra wide" } } diff --git a/apps/browser/src/platform/popup/browser-popup-utils.ts b/apps/browser/src/platform/popup/browser-popup-utils.ts index fb53d3451f2..83fde24ac6f 100644 --- a/apps/browser/src/platform/popup/browser-popup-utils.ts +++ b/apps/browser/src/platform/popup/browser-popup-utils.ts @@ -1,6 +1,7 @@ import { BrowserApi } from "../browser/browser-api"; import { ScrollOptions } from "./abstractions/browser-popup-utils.abstractions"; +import { PopupWidthOptions } from "./layout/popup-width.service"; class BrowserPopupUtils { /** @@ -108,7 +109,7 @@ class BrowserPopupUtils { const defaultPopoutWindowOptions: chrome.windows.CreateData = { type: "popup", focused: true, - width: 380, + width: Math.max(PopupWidthOptions.default, document.body.clientWidth), height: 630, }; const offsetRight = 15; diff --git a/apps/browser/src/platform/popup/layout/popup-layout.stories.ts b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts index e80ac249ac1..1aaea85e4a1 100644 --- a/apps/browser/src/platform/popup/layout/popup-layout.stories.ts +++ b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts @@ -514,3 +514,25 @@ export const TransparentHeader: Story = { `, }), }; + +export const WidthOptions: Story = { + render: (args) => ({ + props: args, + template: /* HTML */ ` +
+
Default:
+
+ +
+
Wide:
+
+ +
+
Extra wide:
+
+ +
+
+ `, + }), +}; diff --git a/apps/browser/src/platform/popup/layout/popup-width.service.ts b/apps/browser/src/platform/popup/layout/popup-width.service.ts new file mode 100644 index 00000000000..cc41b1d9d4a --- /dev/null +++ b/apps/browser/src/platform/popup/layout/popup-width.service.ts @@ -0,0 +1,63 @@ +import { inject, Injectable } from "@angular/core"; +import { map, Observable } from "rxjs"; + +import { + GlobalStateProvider, + KeyDefinition, + POPUP_STYLE_DISK, +} from "@bitwarden/common/platform/state"; + +/** + * + * Value represents width in pixels + */ +export const PopupWidthOptions = Object.freeze({ + default: 380, + wide: 480, + "extra-wide": 600, +}); + +type PopupWidthOptions = typeof PopupWidthOptions; +export type PopupWidthOption = keyof PopupWidthOptions; + +const POPUP_WIDTH_KEY_DEF = new KeyDefinition(POPUP_STYLE_DISK, "popup-width", { + deserializer: (s) => s, +}); + +/** + * Updates the extension popup width based on a user setting + **/ +@Injectable({ providedIn: "root" }) +export class PopupWidthService { + private static readonly LocalStorageKey = "bw-popup-width"; + private readonly state = inject(GlobalStateProvider).get(POPUP_WIDTH_KEY_DEF); + + readonly width$: Observable = this.state.state$.pipe( + map((state) => state ?? "default"), + ); + + async setWidth(width: PopupWidthOption) { + await this.state.update(() => width); + } + + /** Begin listening for state changes */ + init() { + this.width$.subscribe((width: PopupWidthOption) => { + PopupWidthService.setStyle(width); + localStorage.setItem(PopupWidthService.LocalStorageKey, width); + }); + } + + private static setStyle(width: PopupWidthOption) { + const pxWidth = PopupWidthOptions[width] ?? PopupWidthOptions.default; + document.body.style.width = `${pxWidth}px`; + } + + /** + * To keep the popup size from flickering on bootstrap, we store the width in `localStorage` so we can quickly & synchronously reference it. + **/ + static initBodyWidthFromLocalStorage() { + const storedValue = localStorage.getItem(PopupWidthService.LocalStorageKey); + this.setStyle(storedValue as any); + } +} diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts index c23da5ca7ce..31f610b7e74 100644 --- a/apps/browser/src/popup/app.component.ts +++ b/apps/browser/src/popup/app.component.ts @@ -25,6 +25,7 @@ import { import { flagEnabled } from "../platform/flags"; import { PopupCompactModeService } from "../platform/popup/layout/popup-compact-mode.service"; +import { PopupWidthService } from "../platform/popup/layout/popup-width.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"; @@ -44,6 +45,7 @@ import { DesktopSyncVerificationDialogComponent } from "./components/desktop-syn export class AppComponent implements OnInit, OnDestroy { private viewCacheService = inject(PopupViewCacheService); private compactModeService = inject(PopupCompactModeService); + private widthService = inject(PopupWidthService); private lastActivity: Date; private activeUserId: UserId; @@ -99,6 +101,7 @@ export class AppComponent implements OnInit, OnDestroy { await this.viewCacheService.init(); this.compactModeService.init(); + this.widthService.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 diff --git a/apps/browser/src/popup/main.ts b/apps/browser/src/popup/main.ts index c98e30e6bca..db634ea2e2c 100644 --- a/apps/browser/src/popup/main.ts +++ b/apps/browser/src/popup/main.ts @@ -1,6 +1,7 @@ import { enableProdMode } from "@angular/core"; import { platformBrowserDynamic } from "@angular/platform-browser-dynamic"; +import { PopupWidthService } from "../platform/popup/layout/popup-width.service"; import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service"; require("./scss/popup.scss"); @@ -8,7 +9,8 @@ require("./scss/tailwind.css"); import { AppModule } from "./app.module"; -// We put this first to minimize the delay in window changing. +// We put these first to minimize the delay in window changing. +PopupWidthService.initBodyWidthFromLocalStorage(); // Should be removed once we deprecate support for Safari 16.0 and older. See Jira ticket [PM-1861] if (BrowserPlatformUtilsService.shouldApplySafariHeightFix(window)) { document.documentElement.classList.add("safari_height_fix"); diff --git a/apps/browser/src/popup/scss/base.scss b/apps/browser/src/popup/scss/base.scss index 89b8816567d..9412b911ee2 100644 --- a/apps/browser/src/popup/scss/base.scss +++ b/apps/browser/src/popup/scss/base.scss @@ -19,7 +19,7 @@ body { } body { - width: 380px !important; + min-width: 380px !important; height: 600px !important; position: relative; min-height: 100vh; 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 466cffa216a..0fc6297b503 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,11 @@ + + {{ "extensionWidth" | i18n }} + + + { const setEnableRoutingAnimation = jest.fn().mockResolvedValue(undefined); const setEnableCompactMode = jest.fn().mockResolvedValue(undefined); + const mockWidthService: Partial = { + width$: new BehaviorSubject("default"), + setWidth: jest.fn().mockResolvedValue(undefined), + }; + beforeEach(async () => { setSelectedTheme.mockClear(); setShowFavicons.mockClear(); @@ -78,6 +84,10 @@ describe("AppearanceV2Component", () => { provide: PopupCompactModeService, useValue: { enabled$: enableCompactMode$, setEnabled: setEnableCompactMode }, }, + { + provide: PopupWidthService, + useValue: mockWidthService, + }, ], }) .overrideComponent(AppearanceV2Component, { @@ -102,6 +112,7 @@ describe("AppearanceV2Component", () => { enableBadgeCounter: true, theme: ThemeType.Nord, enableCompactMode: false, + width: "default", }); }); 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 c20bc48bfea..31f7f31148f 100644 --- a/apps/browser/src/vault/popup/settings/appearance-v2.component.ts +++ b/apps/browser/src/vault/popup/settings/appearance-v2.component.ts @@ -12,7 +12,7 @@ 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 { BadgeModule, CheckboxModule } from "@bitwarden/components"; +import { BadgeModule, CheckboxModule, Option } from "@bitwarden/components"; import { CardComponent } from "../../../../../../libs/components/src/card/card.component"; import { FormFieldModule } from "../../../../../../libs/components/src/form-field/form-field.module"; @@ -21,6 +21,10 @@ import { PopOutComponent } from "../../../platform/popup/components/pop-out.comp 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"; +import { + PopupWidthOption, + PopupWidthService, +} from "../../../platform/popup/layout/popup-width.service"; @Component({ standalone: true, @@ -41,6 +45,8 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co }) export class AppearanceV2Component implements OnInit { private compactModeService = inject(PopupCompactModeService); + private popupWidthService = inject(PopupWidthService); + private i18nService = inject(I18nService); appearanceForm = this.formBuilder.group({ enableFavicon: false, @@ -48,6 +54,7 @@ export class AppearanceV2Component implements OnInit { theme: ThemeType.System, enableAnimations: true, enableCompactMode: false, + width: "default" as PopupWidthOption, }); /** To avoid flashes of inaccurate values, only show the form after the entire form is populated. */ @@ -56,6 +63,13 @@ export class AppearanceV2Component implements OnInit { /** Available theme options */ themeOptions: { name: string; value: ThemeType }[]; + /** Available width options */ + protected readonly widthOptions: Option[] = [ + { label: this.i18nService.t("default"), value: "default" }, + { label: this.i18nService.t("wide"), value: "wide" }, + { label: this.i18nService.t("extraWide"), value: "extra-wide" }, + ]; + constructor( private messagingService: MessagingService, private domainSettingsService: DomainSettingsService, @@ -81,6 +95,7 @@ export class AppearanceV2Component implements OnInit { this.animationControlService.enableRoutingAnimation$, ); const enableCompactMode = await firstValueFrom(this.compactModeService.enabled$); + const width = await firstValueFrom(this.popupWidthService.width$); // Set initial values for the form this.appearanceForm.setValue({ @@ -89,6 +104,7 @@ export class AppearanceV2Component implements OnInit { theme, enableAnimations, enableCompactMode, + width, }); this.formLoading = false; @@ -122,6 +138,12 @@ export class AppearanceV2Component implements OnInit { .subscribe((enableCompactMode) => { void this.updateCompactMode(enableCompactMode); }); + + this.appearanceForm.controls.width.valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((width) => { + void this.updateWidth(width); + }); } async updateFavicon(enableFavicon: boolean) { @@ -144,4 +166,8 @@ export class AppearanceV2Component implements OnInit { async updateCompactMode(enableCompactMode: boolean) { await this.compactModeService.setEnabled(enableCompactMode); } + + async updateWidth(width: PopupWidthOption) { + await this.popupWidthService.setWidth(width); + } } diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index 0779d80982b..a600901df4f 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -121,6 +121,10 @@ export const TRANSLATION_DISK = new StateDefinition("translation", "disk", { web export const ANIMATION_DISK = new StateDefinition("animation", "disk"); export const TASK_SCHEDULER_DISK = new StateDefinition("taskScheduler", "disk"); +// Design System + +export const POPUP_STYLE_DISK = new StateDefinition("popupStyle", "disk"); + // Secrets Manager export const SM_ONBOARDING_DISK = new StateDefinition("smOnboarding", "disk", { diff --git a/libs/components/src/select/index.ts b/libs/components/src/select/index.ts index faebfee9bb8..8ef9993efda 100644 --- a/libs/components/src/select/index.ts +++ b/libs/components/src/select/index.ts @@ -1,3 +1,4 @@ export * from "./select.module"; export * from "./select.component"; +export * from "./option"; export * from "./option.component";