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

@@ -9,7 +9,7 @@ import { ChangeDetectionStrategy, Component } from "@angular/core";
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
class:
"tw-box-border tw-block tw-bg-background tw-text-main tw-border-solid tw-border-b tw-border-0 tw-border-b-secondary-300 [&:not(bit-layout_*)]:tw-rounded-lg [&:not(bit-layout_*)]:tw-border-b-shadow tw-py-4 tw-px-3",
"tw-box-border tw-block tw-bg-background tw-text-main tw-border-solid tw-border-b tw-border-0 tw-border-b-secondary-300 [&:not(bit-layout_*)]:tw-rounded-lg [&:not(bit-layout_*)]:tw-border-b-shadow tw-py-4 bit-compact:tw-py-3 tw-px-3 bit-compact:tw-px-2",
},
})
export class CardComponent {}

View File

@@ -70,7 +70,7 @@ export class BitFormFieldComponent implements AfterContentChecked {
@HostBinding("class")
get classList() {
return ["tw-block"]
.concat(this.disableMargin ? [] : ["tw-mb-4"])
.concat(this.disableMargin ? [] : ["tw-mb-4", "bit-compact:tw-mb-3"])
.concat(this.readOnly ? [] : "tw-pt-2");
}

View File

@@ -31,6 +31,7 @@ export * from "./radio-button";
export * from "./search";
export * from "./section";
export * from "./select";
export * from "./shared/compact-mode.service";
export * from "./table";
export * from "./tabs";
export * from "./toast";

View File

@@ -1,3 +1 @@
export * from "./item.module";
export { BitItemHeight, BitItemHeightClass } from "./item.component";

View File

@@ -17,7 +17,7 @@ import { TypographyModule } from "../typography";
templateUrl: `item-content.component.html`,
host: {
class:
"fvw-target tw-outline-none tw-text-main hover:tw-text-main tw-no-underline hover:tw-no-underline tw-text-base tw-py-2 tw-px-4 tw-bg-transparent tw-w-full tw-border-none tw-flex tw-gap-4 tw-items-center tw-justify-between",
"fvw-target tw-outline-none tw-text-main hover:tw-text-main tw-no-underline hover:tw-no-underline tw-text-base tw-py-2 tw-px-4 bit-compact:tw-py-1.5 bit-compact:tw-px-2 tw-bg-transparent tw-w-full tw-border-none tw-flex tw-gap-4 tw-items-center tw-justify-between",
},
changeDetection: ChangeDetectionStrategy.OnPush,
})

View File

@@ -1,20 +1,11 @@
<div
class="tw-box-border tw-overflow-auto tw-flex tw-bg-background [&:has(.item-main-content_button:hover,.item-main-content_a:hover)]:tw-cursor-pointer [&:has(.item-main-content_button:hover,.item-main-content_a:hover)]:tw-bg-primary-100 tw-text-main tw-border-solid tw-border-b tw-border-0 [&:not(bit-layout_*)]:tw-rounded-lg tw-mb-1.5"
[ngClass]="
focusVisibleWithin()
? 'tw-z-10 tw-rounded tw-outline-none tw-ring-2 tw-ring-primary-600 tw-border-transparent'
: 'tw-border-b-shadow'
"
>
<bit-item-action class="item-main-content tw-block tw-flex-1 tw-overflow-hidden">
<ng-content></ng-content>
</bit-item-action>
<bit-item-action class="item-main-content tw-flex tw-flex-1 tw-overflow-hidden">
<ng-content></ng-content>
</bit-item-action>
<div
#endSlot
class="tw-p-2 tw-flex tw-gap-1 tw-items-center"
[hidden]="endSlot.childElementCount === 0"
>
<ng-content select="[slot=end]"></ng-content>
</div>
<div
#endSlot
class="tw-p-2 tw-flex tw-gap-1 tw-items-center"
[hidden]="endSlot.childElementCount === 0"
>
<ng-content select="[slot=end]"></ng-content>
</div>

View File

@@ -1,24 +1,16 @@
import { CommonModule } from "@angular/common";
import { ChangeDetectionStrategy, Component, HostListener, signal } from "@angular/core";
import {
ChangeDetectionStrategy,
Component,
HostBinding,
HostListener,
signal,
} from "@angular/core";
import { A11yRowDirective } from "../a11y/a11y-row.directive";
import { ItemActionComponent } from "./item-action.component";
/**
* The class used to set the height of a bit item's inner content.
*/
export const BitItemHeightClass = `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.
*
* 52px + 6px bottom margin + 1px border = 59px
*/
export const BitItemHeight = 59;
@Component({
selector: "bit-item",
standalone: true,
@@ -26,6 +18,10 @@ export const BitItemHeight = 59;
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: "item.component.html",
providers: [{ provide: A11yRowDirective, useExisting: ItemComponent }],
host: {
class:
"tw-block tw-box-border tw-overflow-auto tw-flex tw-bg-background [&:has(.item-main-content_button:hover,.item-main-content_a:hover)]:tw-cursor-pointer [&:has(.item-main-content_button:hover,.item-main-content_a:hover)]:tw-bg-primary-100 tw-text-main tw-border-solid tw-border-b tw-border-0 [&:not(bit-layout_*)]:tw-rounded-lg bit-compact:[&:not(bit-layout_*)]:tw-rounded-none bit-compact:[&:not(bit-layout_*)]:last-of-type:tw-rounded-b-lg bit-compact:[&:not(bit-layout_*)]:first-of-type:tw-rounded-t-lg tw-min-h-9 tw-mb-1.5 bit-compact:tw-mb-0",
},
})
export class ItemComponent extends A11yRowDirective {
/**
@@ -40,4 +36,14 @@ export class ItemComponent extends A11yRowDirective {
onFocusOut() {
this.focusVisibleWithin.set(false);
}
@HostBinding("class") get classList(): string[] {
return [
this.focusVisibleWithin()
? "tw-z-10 tw-rounded tw-outline-none tw-ring-2 bit-compact:tw-ring-inset tw-ring-primary-600 tw-border-transparent".split(
" ",
)
: "tw-border-b-shadow",
].flat();
}
}

View File

@@ -16,7 +16,7 @@ import { I18nMockService } from "../utils/i18n-mock.service";
import { ItemActionComponent } from "./item-action.component";
import { ItemContentComponent } from "./item-content.component";
import { ItemGroupComponent } from "./item-group.component";
import { ItemComponent, BitItemHeight, BitItemHeightClass } from "./item.component";
import { ItemComponent } from "./item.component";
export default {
title: "Component Library/Item",
@@ -152,127 +152,129 @@ export const TextOverflow: Story = {
}),
};
const multipleActionListTemplate = /*html*/ `
<bit-item-group aria-label="Multiple Action List">
<bit-item>
<button bit-item-content>
<i slot="start" class="bwi bwi-globe tw-text-2xl tw-text-muted" aria-hidden="true"></i>
Foo
<span slot="secondary">Bar</span>
</button>
<ng-container slot="end">
<bit-item-action>
<button type="button" bitBadge variant="primary">Auto-fill</button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-clone" size="small"></button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-ellipsis-v" size="small"></button>
</bit-item-action>
</ng-container>
</bit-item>
<bit-item>
<button bit-item-content>
<i slot="start" class="bwi bwi-globe tw-text-2xl tw-text-muted" aria-hidden="true"></i>
Foo
<span slot="secondary">Bar</span>
</button>
<ng-container slot="end">
<bit-item-action>
<button type="button" bitBadge variant="primary">Auto-fill</button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-clone" size="small"></button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-ellipsis-v" size="small"></button>
</bit-item-action>
</ng-container>
</bit-item>
<bit-item>
<button bit-item-content>
<i slot="start" class="bwi bwi-globe tw-text-2xl tw-text-muted" aria-hidden="true"></i>
Foo
<span slot="secondary">Bar</span>
</button>
<ng-container slot="end">
<bit-item-action>
<button type="button" bitBadge variant="primary">Auto-fill</button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-clone" size="small"></button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-ellipsis-v" size="small"></button>
</bit-item-action>
</ng-container>
</bit-item>
<bit-item>
<button bit-item-content>
<i slot="start" class="bwi bwi-globe tw-text-2xl tw-text-muted" aria-hidden="true"></i>
Foo
<span slot="secondary">Bar</span>
</button>
<ng-container slot="end">
<bit-item-action>
<button type="button" bitBadge variant="primary">Auto-fill</button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-clone" size="small"></button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-ellipsis-v" size="small"></button>
</bit-item-action>
</ng-container>
</bit-item>
<bit-item>
<button bit-item-content>
<i slot="start" class="bwi bwi-globe tw-text-2xl tw-text-muted" aria-hidden="true"></i>
Foo
<span slot="secondary">Bar</span>
</button>
<ng-container slot="end">
<bit-item-action>
<button type="button" bitBadge variant="primary">Auto-fill</button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-clone" size="small"></button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-ellipsis-v" size="small"></button>
</bit-item-action>
</ng-container>
</bit-item>
<bit-item>
<button bit-item-content>
<i slot="start" class="bwi bwi-globe tw-text-2xl tw-text-muted" aria-hidden="true"></i>
Foo
<span slot="secondary">Bar</span>
</button>
<ng-container slot="end">
<bit-item-action>
<button type="button" bitBadge variant="primary">Auto-fill</button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-clone" size="small"></button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-ellipsis-v" size="small"></button>
</bit-item-action>
</ng-container>
</bit-item>
</bit-item-group>
`;
export const MultipleActionList: Story = {
render: (args) => ({
props: args,
template: /*html*/ `
<bit-item-group aria-label="Multiple Action List">
<bit-item>
<button bit-item-content>
<i slot="start" class="bwi bwi-globe tw-text-2xl tw-text-muted" aria-hidden="true"></i>
Foo
<span slot="secondary">Bar</span>
</button>
<ng-container slot="end">
<bit-item-action>
<button type="button" bitBadge variant="primary">Auto-fill</button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-clone" size="small"></button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-ellipsis-v" size="small"></button>
</bit-item-action>
</ng-container>
</bit-item>
<bit-item>
<button bit-item-content>
<i slot="start" class="bwi bwi-globe tw-text-2xl tw-text-muted" aria-hidden="true"></i>
Foo
<span slot="secondary">Bar</span>
</button>
<ng-container slot="end">
<bit-item-action>
<button type="button" bitBadge variant="primary">Auto-fill</button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-clone" size="small"></button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-ellipsis-v" size="small"></button>
</bit-item-action>
</ng-container>
</bit-item>
<bit-item>
<button bit-item-content>
<i slot="start" class="bwi bwi-globe tw-text-2xl tw-text-muted" aria-hidden="true"></i>
Foo
<span slot="secondary">Bar</span>
</button>
<ng-container slot="end">
<bit-item-action>
<button type="button" bitBadge variant="primary">Auto-fill</button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-clone" size="small"></button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-ellipsis-v" size="small"></button>
</bit-item-action>
</ng-container>
</bit-item>
<bit-item>
<button bit-item-content>
<i slot="start" class="bwi bwi-globe tw-text-2xl tw-text-muted" aria-hidden="true"></i>
Foo
<span slot="secondary">Bar</span>
</button>
<ng-container slot="end">
<bit-item-action>
<button type="button" bitBadge variant="primary">Auto-fill</button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-clone" size="small"></button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-ellipsis-v" size="small"></button>
</bit-item-action>
</ng-container>
</bit-item>
<bit-item>
<button bit-item-content>
<i slot="start" class="bwi bwi-globe tw-text-2xl tw-text-muted" aria-hidden="true"></i>
Foo
<span slot="secondary">Bar</span>
</button>
<ng-container slot="end">
<bit-item-action>
<button type="button" bitBadge variant="primary">Auto-fill</button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-clone" size="small"></button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-ellipsis-v" size="small"></button>
</bit-item-action>
</ng-container>
</bit-item>
<bit-item>
<button bit-item-content>
<i slot="start" class="bwi bwi-globe tw-text-2xl tw-text-muted" aria-hidden="true"></i>
Foo
<span slot="secondary">Bar</span>
</button>
<ng-container slot="end">
<bit-item-action>
<button type="button" bitBadge variant="primary">Auto-fill</button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-clone" size="small"></button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-ellipsis-v" size="small"></button>
</bit-item-action>
</ng-container>
</bit-item>
</bit-item-group>
`,
template: multipleActionListTemplate,
}),
};
@@ -346,18 +348,27 @@ export const SingleActionWithBadge: Story = {
}),
};
export const CompactMode: Story = {
render: (args) => ({
props: args,
template: /*html*/ `
<div class="tw-bit-compact">
${multipleActionListTemplate}
</div>
`,
}),
};
export const VirtualScrolling: Story = {
render: (_args) => ({
props: {
data: Array.from(Array(100000).keys()),
itemSize: BitItemHeight,
itemClass: BitItemHeightClass,
},
template: /*html*/ `
<cdk-virtual-scroll-viewport [itemSize]="itemSize" class="tw-h-[500px]">
<cdk-virtual-scroll-viewport [itemSize]="59" class="tw-h-[500px]">
<bit-item-group aria-label="Virtual Scrolling">
<bit-item *cdkVirtualFor="let item of data">
<button bit-item-content [ngClass]="itemClass">
<button bit-item-content>
<i slot="start" class="bwi bwi-globe tw-text-2xl tw-text-muted" aria-hidden="true"></i>
{{ item }}
</button>

View File

@@ -9,7 +9,8 @@ import { Component, Input } from "@angular/core";
template: `
<section
[ngClass]="{
'tw-mb-6 [&:not(bit-dialog_*):not(popup-page_*)]:md:tw-mb-12': !disableMargin,
'tw-mb-5 bit-compact:tw-mb-4 [&:not(bit-dialog_*):not(popup-page_*)]:md:tw-mb-12':
!disableMargin,
}"
>
<ng-content></ng-content>

View 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>;
}

View File

@@ -0,0 +1,45 @@
import { Meta, Story } from "@storybook/addon-docs";
import * as itemStories from "../item/item.stories";
import * as popupLayoutStories from "../../../../apps/browser/src/platform/popup/layout/popup-layout.stories";
<Meta title="Documentation/Compact Mode" />
# Compact Mode
The Bitwarden browser extension has a global compact mode setting for users who prefer a more
information-dense UI.
## Tailwind
Component authors can hook into this setting with the `bit-compact` Tailwind variant. In the
following example, the paragraph's padding is reduced when compact mode is enabled.
```html
<p class="tw-bg-primary-100 tw-p-4 bit-compact:tw-p-0.5">Lorem impsum doggo dolor...</p>
<div class="tw-bit-compact">
<p class="tw-bg-primary-100 tw-p-4 bit-compact:tw-p-0.5">Lorem impsum doggo dolor...</p>
</div>
```
<p class="tw-bg-primary-100 tw-p-4 bit-compact:tw-p-0.5">Lorem impsum doggo dolor...</p>
<div class="tw-bit-compact">
<p class="tw-bg-primary-100 tw-p-4 bit-compact:tw-p-0.5">Lorem impsum doggo dolor...</p>
</div>
## Service
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
### [Popup Layout](?path=/story/browser-popup-layout--compact-mode)
<Story autoplay={true} of={popupLayoutStories.CompactMode} />
### [Item](?path=/story/component-library-item--compact-mode)
<Story of={itemStories.CompactMode} />