mirror of
https://github.com/bitwarden/browser
synced 2026-02-12 14:34:02 +00:00
use client abstraction pattern; add settings toggle; use observable instead of signal due to state provider
This commit is contained in:
@@ -4776,5 +4776,11 @@
|
||||
},
|
||||
"generatedPassword": {
|
||||
"message": "Generated password"
|
||||
},
|
||||
"compactMode": {
|
||||
"message": "Compact mode"
|
||||
},
|
||||
"beta": {
|
||||
"message": "Beta"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<boolean>(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<boolean> = 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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
</div>
|
||||
<bit-item-group>
|
||||
<cdk-virtual-scroll-viewport
|
||||
[itemSize]="ItemHeight()"
|
||||
[itemSize]="itemHeight$ | async"
|
||||
class="tw-overflow-visible [&>.cdk-virtual-scroll-content-wrapper]:[contain:layout_style]"
|
||||
>
|
||||
<bit-item *cdkVirtualFor="let cipher of ciphers">
|
||||
@@ -30,7 +30,7 @@
|
||||
(click)="onViewCipher(cipher)"
|
||||
(dblclick)="launchCipher(cipher)"
|
||||
[appA11yTitle]="'viewItemTitle' | i18n: cipher.name"
|
||||
class="{{ ItemHeightClass }}"
|
||||
class="{{ itemHeightClass }}"
|
||||
>
|
||||
<app-vault-icon slot="start" [cipher]="cipher"></app-vault-icon>
|
||||
<span data-testid="item-name">{{ cipher.name }}</span>
|
||||
|
||||
@@ -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)),
|
||||
);
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,6 +18,14 @@
|
||||
</bit-select>
|
||||
</bit-form-field>
|
||||
|
||||
<bit-form-control>
|
||||
<input bitCheckbox formControlName="enableCompactMode" type="checkbox" />
|
||||
<bit-label
|
||||
>{{ "compactMode" | i18n }}
|
||||
<span bitBadge variant="warning">{{ "beta" | i18n }}</span></bit-label
|
||||
>
|
||||
</bit-form-control>
|
||||
|
||||
<bit-form-control>
|
||||
<input bitCheckbox formControlName="enableBadgeCounter" type="checkbox" />
|
||||
<bit-label>{{ "showNumberOfAutofillSuggestions" | i18n }}</bit-label>
|
||||
|
||||
@@ -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<boolean>(true);
|
||||
const selectedTheme$ = new BehaviorSubject<ThemeType>(ThemeType.Nord);
|
||||
const enableRoutingAnimation$ = new BehaviorSubject<boolean>(true);
|
||||
const enableCompactMode$ = new BehaviorSubject<boolean>(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,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
11
libs/components/src/shared/compact-mode.service.ts
Normal file
11
libs/components/src/shared/compact-mode.service.ts
Normal file
@@ -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<boolean>;
|
||||
}
|
||||
@@ -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<boolean> = signal(false);
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
this.compactMode()
|
||||
? document.body.classList.add("tw-bit-compact")
|
||||
: document.body.classList.remove("tw-bit-compact");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user