mirror of
https://github.com/bitwarden/browser.git
synced 2025-01-01 18:08:19 +01:00
[CL-508] extension width setting (#12040)
This commit is contained in:
parent
820b03dd49
commit
bb0912154d
@ -4888,5 +4888,14 @@
|
||||
},
|
||||
"beta": {
|
||||
"message": "Beta"
|
||||
},
|
||||
"extensionWidth": {
|
||||
"message": "Extension width"
|
||||
},
|
||||
"wide": {
|
||||
"message": "Wide"
|
||||
},
|
||||
"extraWide": {
|
||||
"message": "Extra wide"
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -514,3 +514,25 @@ export const TransparentHeader: Story = {
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const WidthOptions: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: /* HTML */ `
|
||||
<div class="tw-flex tw-flex-col tw-gap-4 tw-text-main">
|
||||
<div>Default:</div>
|
||||
<div class="tw-h-[640px] tw-w-[380px] tw-border tw-border-solid tw-border-secondary-300">
|
||||
<mock-vault-page></mock-vault-page>
|
||||
</div>
|
||||
<div>Wide:</div>
|
||||
<div class="tw-h-[640px] tw-w-[480px] tw-border tw-border-solid tw-border-secondary-300">
|
||||
<mock-vault-page></mock-vault-page>
|
||||
</div>
|
||||
<div>Extra wide:</div>
|
||||
<div class="tw-h-[640px] tw-w-[600px] tw-border tw-border-solid tw-border-secondary-300">
|
||||
<mock-vault-page></mock-vault-page>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
@ -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<PopupWidthOption>(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<PopupWidthOption> = 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);
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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");
|
||||
|
@ -19,7 +19,7 @@ body {
|
||||
}
|
||||
|
||||
body {
|
||||
width: 380px !important;
|
||||
min-width: 380px !important;
|
||||
height: 600px !important;
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
|
@ -18,6 +18,11 @@
|
||||
</bit-select>
|
||||
</bit-form-field>
|
||||
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "extensionWidth" | i18n }}</bit-label>
|
||||
<bit-select formControlName="width" [items]="widthOptions"></bit-select>
|
||||
</bit-form-field>
|
||||
|
||||
<bit-form-control>
|
||||
<input bitCheckbox formControlName="enableCompactMode" type="checkbox" />
|
||||
<bit-label
|
||||
|
@ -16,6 +16,7 @@ import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-stat
|
||||
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 { PopupWidthService } from "../../../platform/popup/layout/popup-width.service";
|
||||
|
||||
import { AppearanceV2Component } from "./appearance-v2.component";
|
||||
|
||||
@ -51,6 +52,11 @@ describe("AppearanceV2Component", () => {
|
||||
const setEnableRoutingAnimation = jest.fn().mockResolvedValue(undefined);
|
||||
const setEnableCompactMode = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
const mockWidthService: Partial<PopupWidthService> = {
|
||||
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",
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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<PopupWidthOption>[] = [
|
||||
{ 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);
|
||||
}
|
||||
}
|
||||
|
@ -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", {
|
||||
|
@ -1,3 +1,4 @@
|
||||
export * from "./select.module";
|
||||
export * from "./select.component";
|
||||
export * from "./option";
|
||||
export * from "./option.component";
|
||||
|
Loading…
Reference in New Issue
Block a user