1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-11 22:03:36 +00:00

[CL-499][PM-14020] compact mode (#11796)

This commit is contained in:
Will Martin
2024-11-18 16:35:49 -05:00
committed by GitHub
parent 3521c54672
commit a07b072196
28 changed files with 392 additions and 190 deletions

View File

@@ -4843,5 +4843,11 @@
},
"generatedPassword": {
"message": "Generated password"
},
"compactMode": {
"message": "Compact mode"
},
"beta": {
"message": "Beta"
}
}

View File

@@ -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);
}
}

View File

@@ -1,7 +1,7 @@
<footer
class="tw-p-4 tw-border-0 tw-border-solid tw-border-t tw-border-secondary-300 tw-bg-background"
class="tw-p-3 bit-compact:tw-p-2 tw-border-0 tw-border-solid tw-border-t tw-border-secondary-300 tw-bg-background"
>
<div class="tw-max-w-screen-sm tw-mx-auto tw-flex tw-justify-between tw-w-full">
<div class="tw-max-w-screen-sm tw-mx-auto tw-flex tw-justify-between tw-w-full tw-items-center">
<div class="tw-flex tw-justify-start tw-gap-2">
<ng-content></ng-content>
</div>

View File

@@ -1,5 +1,5 @@
<header
class="tw-px-4 tw-py-3 tw-transition-colors tw-duration-200 tw-border-0 tw-border-b tw-border-solid"
class="tw-p-3 bit-compact:tw-p-2 tw-pl-4 bit-compact:tw-pl-3 tw-transition-colors tw-duration-200 tw-border-0 tw-border-b tw-border-solid"
[ngClass]="{
'tw-bg-background-alt tw-border-transparent':
this.background === 'alt' && !pageContentScrolled(),
@@ -10,6 +10,7 @@
<div class="tw-max-w-screen-sm tw-mx-auto tw-flex tw-justify-between tw-w-full">
<div class="tw-inline-flex tw-items-center tw-gap-2 tw-h-9">
<button
class="-tw-ml-1"
bitIconButton="bwi-back"
type="button"
*ngIf="showBackButton"

View File

@@ -117,11 +117,7 @@ class MockCurrentAccountComponent {}
@Component({
selector: "mock-search",
template: `
<div class="tw-p-4">
<bit-search placeholder="Search"> </bit-search>
</div>
`,
template: ` <bit-search placeholder="Search"> </bit-search> `,
standalone: true,
imports: [SearchModule],
})
@@ -410,6 +406,46 @@ export const PopupPageWithFooter: Story = {
}),
};
export const CompactMode: Story = {
render: (args) => ({
props: args,
template: /* HTML */ `
<div class="tw-flex tw-gap-6 tw-text-main">
<div id="regular-example">
<p>Relaxed</p>
<p class="example-label"></p>
<extension-container>
<mock-vault-subpage></mock-vault-subpage>
</extension-container>
</div>
<div id="compact-example" class="tw-bit-compact">
<p>Compact</p>
<p class="example-label"></p>
<extension-container>
<mock-vault-subpage></mock-vault-subpage>
</extension-container>
</div>
</div>
`,
}),
play: async (context) => {
const canvasEl = context.canvasElement;
const updateLabel = (containerId: string) => {
const compact = canvasEl.querySelector(
`#${containerId} [data-testid=popup-layout-scroll-region]`,
);
const label = canvasEl.querySelector(`#${containerId} .example-label`);
const percentVisible =
100 -
Math.round((100 * (compact.scrollHeight - compact.clientHeight)) / compact.scrollHeight);
label.textContent = `${percentVisible}% above the fold`;
};
updateLabel("compact-example");
updateLabel("regular-example");
},
};
export const PoppedOut: Story = {
render: (args) => ({
props: args,

View File

@@ -2,9 +2,9 @@
<main class="tw-flex-1 tw-overflow-hidden tw-flex tw-flex-col tw-relative tw-bg-background-alt">
<div
#nonScrollable
class="tw-transition-colors tw-duration-200 tw-border-0 tw-border-b tw-border-solid"
class="tw-transition-colors tw-duration-200 tw-border-0 tw-border-b tw-border-solid tw-p-3 bit-compact:tw-p-2"
[ngClass]="{
'tw-invisible': loading || nonScrollable.childElementCount === 0,
'tw-invisible !tw-p-0': loading || nonScrollable.childElementCount === 0,
'tw-border-secondary-300': scrolled(),
'tw-border-transparent': !scrolled(),
}"
@@ -13,12 +13,13 @@
</div>
<div
class="tw-max-w-screen-sm tw-mx-auto tw-overflow-y-auto tw-flex tw-flex-col tw-w-full tw-h-full tw-styled-scrollbar"
data-testid="popup-layout-scroll-region"
(scroll)="handleScroll($event)"
[ngClass]="{ 'tw-invisible': loading }"
>
<div
class="tw-max-w-screen-sm tw-mx-auto tw-flex-1 tw-flex tw-flex-col tw-h-full tw-w-full"
[ngClass]="{ 'tw-p-3': !disablePadding }"
[ngClass]="{ 'tw-p-3 bit-compact:tw-p-2': !disablePadding }"
>
<ng-content></ng-content>
</div>

View File

@@ -7,7 +7,7 @@
<ul class="tw-flex tw-flex-1 tw-mb-0 tw-p-0">
<li *ngFor="let button of navButtons" class="tw-flex-1 tw-list-none">
<button
class="tw-w-full tw-flex tw-flex-col tw-items-center tw-gap-1 tw-px-0.5 tw-pb-2 tw-pt-3 tw-bg-transparent tw-no-underline hover:tw-no-underline hover:tw-text-primary-600 hover:tw-bg-primary-100 tw-border-2 tw-border-solid tw-border-transparent focus-visible:tw-rounded-lg focus-visible:tw-border-primary-600"
class="tw-w-full tw-flex tw-flex-col tw-items-center tw-gap-1 tw-px-0.5 tw-pb-2 bit-compact:tw-pb-1 tw-pt-3 bit-compact:tw-pt-2 tw-bg-transparent tw-no-underline hover:tw-no-underline hover:tw-text-primary-600 hover:tw-bg-primary-100 tw-border-2 tw-border-solid tw-border-transparent focus-visible:tw-rounded-lg focus-visible:tw-border-primary-600"
[ngClass]="rla.isActive ? 'tw-font-bold tw-text-primary-600' : 'tw-text-muted'"
[title]="button.label"
[routerLink]="button.page"

View File

@@ -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;
@@ -96,6 +98,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();

View File

@@ -103,7 +103,7 @@ import {
} 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";
@@ -123,6 +123,7 @@ import { ChromeMessageSender } from "../../platform/messaging/chrome-message.sen
/* eslint-enable no-restricted-imports */
import { OffscreenDocumentService } from "../../platform/offscreen-document/abstractions/offscreen-document";
import { DefaultOffscreenDocumentService } from "../../platform/offscreen-document/offscreen-document.service";
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";
@@ -581,6 +582,11 @@ const safeProviders: SafeProvider[] = [
useClass: ExtensionAnonLayoutWrapperDataService,
deps: [],
}),
safeProvider({
provide: CompactModeService,
useExisting: PopupCompactModeService,
deps: [],
}),
];
@NgModule({

View File

@@ -6,7 +6,7 @@
<app-current-account></app-current-account>
</ng-container>
</popup-header>
<div slot="above-scroll-area" class="tw-p-4" *ngIf="!(sendsLoading$ | async)">
<ng-container slot="above-scroll-area" *ngIf="!(sendsLoading$ | async)">
<bit-callout *ngIf="sendsDisabled" [title]="'sendDisabled' | i18n">
{{ "sendDisabledWarning" | i18n }}
</bit-callout>
@@ -14,7 +14,7 @@
<tools-send-search></tools-send-search>
<app-send-list-filters></app-send-list-filters>
</ng-container>
</div>
</ng-container>
<div
*ngIf="listState === sendState.Empty"

View File

@@ -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>

View File

@@ -1,7 +1,8 @@
import { ScrollingModule } from "@angular/cdk/scrolling";
import { CommonModule } from "@angular/common";
import { booleanAttribute, Component, EventEmitter, 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";
@@ -9,9 +10,8 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import {
BadgeModule,
BitItemHeight,
BitItemHeightClass,
ButtonModule,
CompactModeService,
IconButtonModule,
ItemModule,
SectionComponent,
@@ -49,8 +49,25 @@ import { ItemMoreOptionsComponent } from "../item-more-options/item-more-options
standalone: true,
})
export class VaultListItemsContainerComponent {
protected ItemHeightClass = BitItemHeightClass;
protected ItemHeight = BitItemHeight;
private compactModeService = inject(CompactModeService);
/**
* The class used to set the height of a bit item's inner content.
*/
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
* to estimate how many items can be displayed at once and how large the virtual container should be.
* Needs to be updated if the item height or spacing changes.
*
* Default: 52px + 1px border + 6px bottom margin = 59px
*
* Compact mode: 52px + 1px border = 53px
*/
protected readonly itemHeight$ = this.compactModeService.enabled$.pipe(
map((enabled) => (enabled ? 53 : 59)),
);
/**
* Timeout used to add a small delay when selecting a cipher to allow for double click to launch

View File

@@ -23,14 +23,13 @@
</div>
<!-- Show search & filters outside of the scroll area of the page -->
<div
<ng-container
slot="above-scroll-area"
class="tw-p-4"
*ngIf="vaultState !== VaultStateEnum.Empty && !(loading$ | async)"
>
<app-vault-v2-search> </app-vault-v2-search>
<app-vault-list-filters></app-vault-list-filters>
</div>
</ng-container>
<ng-container *ngIf="vaultState !== VaultStateEnum.Empty">
<div
@@ -61,7 +60,7 @@
<div
*ngIf="vaultState === null"
cdkVirtualScrollingElement
class="tw-h-full tw-p-3 tw-styled-scrollbar"
class="tw-h-full tw-p-3 bit-compact:tw-p-2 tw-styled-scrollbar"
>
<app-autofill-vault-list-items></app-autofill-vault-list-items>
<app-vault-list-items-container

View File

@@ -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>

View File

@@ -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,
});
});

View File

@@ -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);
}
}