Browse Source

Revert "[CL-622][CL-562][CL-621][CL-632] various drawer improvements (#14120)" (#14827)

This reverts commit a0429d7d09.
pull/14832/head
Vicki League 7 months ago committed by GitHub
parent
commit
4b32d1f9dd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      apps/web/src/app/admin-console/organizations/manage/groups.component.html
  2. 2
      apps/web/src/app/admin-console/organizations/members/members.component.html
  3. 2
      apps/web/src/app/admin-console/organizations/members/members.module.ts
  4. 3
      apps/web/src/app/admin-console/organizations/organization.module.ts
  5. 2
      apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.ts
  6. 14
      apps/web/src/app/auth/settings/security/device-management.component.spec.ts
  7. 4
      apps/web/src/app/billing/members/add-sponsorship-dialog.component.ts
  8. 3
      apps/web/src/app/billing/members/free-bitwarden-families.component.ts
  9. 2
      apps/web/src/app/vault/components/vault-items/vault-items.component.html
  10. 3
      apps/web/src/app/vault/components/vault-items/vault-items.module.ts
  11. 12
      apps/web/src/app/vault/components/vault-items/vault-items.stories.ts
  12. 2
      bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.html
  13. 3
      bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts
  14. 4
      bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.ts
  15. 57
      libs/components/src/dialog/dialog.service.stories.ts
  16. 238
      libs/components/src/dialog/dialog.service.ts
  17. 40
      libs/components/src/dialog/dialog/dialog.component.html
  18. 37
      libs/components/src/dialog/dialog/dialog.component.ts
  19. 3
      libs/components/src/dialog/dialogs.mdx
  20. 2
      libs/components/src/dialog/index.ts
  21. 18
      libs/components/src/drawer/drawer-body.component.ts
  22. 4
      libs/components/src/drawer/drawer.component.ts
  23. 2
      libs/components/src/drawer/drawer.mdx
  24. 20
      libs/components/src/drawer/drawer.service.ts
  25. 1
      libs/components/src/layout/index.ts
  26. 89
      libs/components/src/layout/layout.component.html
  27. 26
      libs/components/src/layout/layout.component.ts
  28. 35
      libs/components/src/layout/scroll-layout.directive.ts
  29. 12
      libs/components/src/stories/kitchen-sink/components/dialog-virtual-scroll-block.component.ts
  30. 133
      libs/components/src/stories/kitchen-sink/components/kitchen-sink-main.component.ts
  31. 30
      libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts
  32. 2
      libs/components/src/table/table-scroll.component.html
  33. 3
      libs/components/src/table/table-scroll.component.ts
  34. 12
      libs/components/src/table/table.mdx
  35. 68
      libs/components/src/table/table.stories.ts
  36. 41
      libs/components/src/utils/has-scrolled-from.ts

2
apps/web/src/app/admin-console/organizations/manage/groups.component.html

@ -22,7 +22,7 @@ @@ -22,7 +22,7 @@
<p *ngIf="!dataSource.filteredData.length">{{ "noGroupsInList" | i18n }}</p>
<!-- The padding on the bottom of the cdk-virtual-scroll-viewport element is required to prevent table row content
from overflowing the <main> element. -->
<cdk-virtual-scroll-viewport bitScrollLayout [itemSize]="rowHeight" class="tw-pb-8">
<cdk-virtual-scroll-viewport scrollWindow [itemSize]="rowHeight" class="tw-pb-8">
<bit-table *ngIf="dataSource.filteredData.length" [dataSource]="dataSource">
<ng-container header>
<tr>

2
apps/web/src/app/admin-console/organizations/members/members.component.html

@ -67,7 +67,7 @@ @@ -67,7 +67,7 @@
</bit-callout>
<!-- The padding on the bottom of the cdk-virtual-scroll-viewport element is required to prevent table row content
from overflowing the <main> element. -->
<cdk-virtual-scroll-viewport bitScrollLayout [itemSize]="rowHeight" class="tw-pb-8">
<cdk-virtual-scroll-viewport scrollWindow [itemSize]="rowHeight" class="tw-pb-8">
<bit-table [dataSource]="dataSource">
<ng-container header>
<tr>

2
apps/web/src/app/admin-console/organizations/members/members.module.ts

@ -3,7 +3,6 @@ import { NgModule } from "@angular/core"; @@ -3,7 +3,6 @@ import { NgModule } from "@angular/core";
import { PasswordStrengthV2Component } from "@bitwarden/angular/tools/password-strength/password-strength-v2.component";
import { PasswordCalloutComponent } from "@bitwarden/auth/angular";
import { ScrollLayoutDirective } from "@bitwarden/components";
import { LooseComponentsModule } from "../../../shared";
import { SharedOrganizationModule } from "../shared";
@ -28,7 +27,6 @@ import { MembersComponent } from "./members.component"; @@ -28,7 +27,6 @@ import { MembersComponent } from "./members.component";
PasswordCalloutComponent,
ScrollingModule,
PasswordStrengthV2Component,
ScrollLayoutDirective,
],
declarations: [
BulkConfirmDialogComponent,

3
apps/web/src/app/admin-console/organizations/organization.module.ts

@ -1,8 +1,6 @@ @@ -1,8 +1,6 @@
import { ScrollingModule } from "@angular/cdk/scrolling";
import { NgModule } from "@angular/core";
import { ScrollLayoutDirective } from "@bitwarden/components";
import { LooseComponentsModule } from "../../shared";
import { CoreOrganizationModule } from "./core";
@ -20,7 +18,6 @@ import { AccessSelectorModule } from "./shared/components/access-selector"; @@ -20,7 +18,6 @@ import { AccessSelectorModule } from "./shared/components/access-selector";
OrganizationsRoutingModule,
LooseComponentsModule,
ScrollingModule,
ScrollLayoutDirective,
],
declarations: [GroupsComponent, GroupAddEditComponent],
})

2
apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.ts

@ -613,5 +613,5 @@ export function openCollectionDialog( @@ -613,5 +613,5 @@ export function openCollectionDialog(
dialogService: DialogService,
config: DialogConfig<CollectionDialogParams, DialogRef<CollectionDialogResult>>,
) {
return dialogService.open<CollectionDialogResult>(CollectionDialogComponent, config);
return dialogService.open(CollectionDialogComponent, config);
}

14
apps/web/src/app/auth/settings/security/device-management.component.spec.ts

@ -9,13 +9,7 @@ import { DeviceType } from "@bitwarden/common/enums"; @@ -9,13 +9,7 @@ import { DeviceType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { MessageListener } from "@bitwarden/common/platform/messaging";
import {
DialogService,
ToastService,
TableModule,
PopoverModule,
LayoutComponent,
} from "@bitwarden/components";
import { DialogService, ToastService, TableModule, PopoverModule } from "@bitwarden/components";
import { SharedModule } from "../../../shared";
import { VaultBannersService } from "../../../vault/individual-vault/vault-banners/services/vault-banners.service";
@ -121,12 +115,6 @@ describe("DeviceManagementComponent", () => { @@ -121,12 +115,6 @@ describe("DeviceManagementComponent", () => {
showError: jest.fn(),
},
},
{
provide: LayoutComponent,
useValue: {
mainContent: jest.fn(),
},
},
],
}).compileComponents();

4
apps/web/src/app/billing/members/add-sponsorship-dialog.component.ts

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
import { Component, Inject } from "@angular/core";
import {
AbstractControl,
@ -18,10 +19,7 @@ import { EncryptService } from "@bitwarden/common/key-management/crypto/abstract @@ -18,10 +19,7 @@ import { EncryptService } from "@bitwarden/common/key-management/crypto/abstract
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { OrgKey } from "@bitwarden/common/types/key";
import {
DialogRef,
ButtonModule,
DialogConfig,
DIALOG_DATA,
DialogModule,
DialogService,
FormFieldModule,

3
apps/web/src/app/billing/members/free-bitwarden-families.component.ts

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
import { DialogRef } from "@angular/cdk/dialog";
import { formatDate } from "@angular/common";
import { Component, OnInit, signal } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
@ -15,7 +16,7 @@ import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; @@ -15,7 +16,7 @@ import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { StateProvider } from "@bitwarden/common/platform/state";
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { OrgKey } from "@bitwarden/common/types/key";
import { DialogRef, DialogService, ToastService } from "@bitwarden/components";
import { DialogService, ToastService } from "@bitwarden/components";
import { KeyService } from "@bitwarden/key-management";
import { AddSponsorshipDialogComponent } from "./add-sponsorship-dialog.component";

2
apps/web/src/app/vault/components/vault-items/vault-items.component.html

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
<cdk-virtual-scroll-viewport [itemSize]="RowHeight" bitScrollLayout class="tw-pb-8">
<cdk-virtual-scroll-viewport [itemSize]="RowHeight" scrollWindow class="tw-pb-8">
<bit-table [dataSource]="dataSource" layout="fixed">
<ng-container header>
<tr>

3
apps/web/src/app/vault/components/vault-items/vault-items.module.ts

@ -3,7 +3,7 @@ import { CommonModule } from "@angular/common"; @@ -3,7 +3,7 @@ import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { RouterModule } from "@angular/router";
import { ScrollLayoutDirective, TableModule } from "@bitwarden/components";
import { TableModule } from "@bitwarden/components";
import { CollectionNameBadgeComponent } from "../../../admin-console/organizations/collections";
import { GroupBadgeModule } from "../../../admin-console/organizations/collections/group-badge/group-badge.module";
@ -26,7 +26,6 @@ import { VaultItemsComponent } from "./vault-items.component"; @@ -26,7 +26,6 @@ import { VaultItemsComponent } from "./vault-items.component";
CollectionNameBadgeComponent,
GroupBadgeModule,
PipesModule,
ScrollLayoutDirective,
],
declarations: [VaultItemsComponent, VaultCipherRowComponent, VaultCollectionRowComponent],
exports: [VaultItemsComponent],

12
apps/web/src/app/vault/components/vault-items/vault-items.stories.ts

@ -2,13 +2,7 @@ @@ -2,13 +2,7 @@
// @ts-strict-ignore
import { importProvidersFrom } from "@angular/core";
import { RouterModule } from "@angular/router";
import {
applicationConfig,
componentWrapperDecorator,
Meta,
moduleMetadata,
StoryObj,
} from "@storybook/angular";
import { applicationConfig, Meta, moduleMetadata, StoryObj } from "@storybook/angular";
import { BehaviorSubject, of } from "rxjs";
import {
@ -35,7 +29,6 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -35,7 +29,6 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import { LayoutComponent } from "@bitwarden/components";
import { GroupView } from "../../../admin-console/organizations/core";
import { PreloadedEnglishI18nModule } from "../../../core/tests";
@ -55,9 +48,8 @@ export default { @@ -55,9 +48,8 @@ export default {
title: "Web/Vault/Items",
component: VaultItemsComponent,
decorators: [
componentWrapperDecorator((story) => `<bit-layout>${story}</bit-layout>`),
moduleMetadata({
imports: [VaultItemsModule, RouterModule, LayoutComponent],
imports: [VaultItemsModule, RouterModule],
providers: [
{
provide: EnvironmentService,

2
bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.html

@ -55,7 +55,7 @@ @@ -55,7 +55,7 @@
>
{{ "providerUsersNeedConfirmed" | i18n }}
</bit-callout>
<cdk-virtual-scroll-viewport bitScrollLayout [itemSize]="rowHeight" class="tw-pb-8">
<cdk-virtual-scroll-viewport scrollWindow [itemSize]="rowHeight" class="tw-pb-8">
<bit-table [dataSource]="dataSource">
<ng-container header>
<tr>

3
bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts

@ -4,7 +4,7 @@ import { NgModule } from "@angular/core"; @@ -4,7 +4,7 @@ import { NgModule } from "@angular/core";
import { FormsModule } from "@angular/forms";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { CardComponent, ScrollLayoutDirective, SearchModule } from "@bitwarden/components";
import { CardComponent, SearchModule } from "@bitwarden/components";
import { DangerZoneComponent } from "@bitwarden/web-vault/app/auth/settings/account/danger-zone.component";
import { OrganizationPlansComponent } from "@bitwarden/web-vault/app/billing";
import { PaymentComponent } from "@bitwarden/web-vault/app/billing/shared/payment/payment.component";
@ -53,7 +53,6 @@ import { VerifyRecoverDeleteProviderComponent } from "./verify-recover-delete-pr @@ -53,7 +53,6 @@ import { VerifyRecoverDeleteProviderComponent } from "./verify-recover-delete-pr
ScrollingModule,
VerifyBankAccountComponent,
CardComponent,
ScrollLayoutDirective,
PaymentComponent,
],
declarations: [

4
bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.ts

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
import { BasePortalOutlet } from "@angular/cdk/portal";
import { Component, Inject, OnInit } from "@angular/core";
import { FormControl, FormGroup, Validators } from "@angular/forms";
@ -32,7 +33,8 @@ export const openCreateClientDialog = ( @@ -32,7 +33,8 @@ export const openCreateClientDialog = (
dialogService: DialogService,
dialogConfig: DialogConfig<
CreateClientDialogParams,
DialogRef<CreateClientDialogResultType, unknown>
DialogRef<CreateClientDialogResultType, unknown>,
BasePortalOutlet
>,
) =>
dialogService.open<CreateClientDialogResultType, CreateClientDialogParams>(

57
libs/components/src/dialog/dialog.service.stories.ts

@ -1,17 +1,12 @@ @@ -1,17 +1,12 @@
import { DIALOG_DATA, DialogModule, DialogRef } from "@angular/cdk/dialog";
import { Component, Inject } from "@angular/core";
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
import { RouterTestingModule } from "@angular/router/testing";
import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular";
import { getAllByRole, userEvent } from "@storybook/test";
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { ButtonModule } from "../button";
import { IconButtonModule } from "../icon-button";
import { LayoutComponent } from "../layout";
import { SharedModule } from "../shared";
import { positionFixedWrapperDecorator } from "../stories/storybook-decorators";
import { I18nMockService } from "../utils/i18n-mock.service";
import { DialogComponent } from "./dialog/dialog.component";
@ -24,12 +19,7 @@ interface Animal { @@ -24,12 +19,7 @@ interface Animal {
}
@Component({
template: `
<bit-layout>
<button class="tw-mr-2" bitButton type="button" (click)="openDialog()">Open Dialog</button>
<button bitButton type="button" (click)="openDrawer()">Open Drawer</button>
</bit-layout>
`,
template: `<button bitButton type="button" (click)="openDialog()">Open Dialog</button>`,
})
class StoryDialogComponent {
constructor(public dialogService: DialogService) {}
@ -41,14 +31,6 @@ class StoryDialogComponent { @@ -41,14 +31,6 @@ class StoryDialogComponent {
},
});
}
openDrawer() {
this.dialogService.openDrawer(StoryDialogContentComponent, {
data: {
animal: "panda",
},
});
}
}
@Component({
@ -83,37 +65,25 @@ export default { @@ -83,37 +65,25 @@ export default {
title: "Component Library/Dialogs/Service",
component: StoryDialogComponent,
decorators: [
positionFixedWrapperDecorator(),
moduleMetadata({
declarations: [StoryDialogContentComponent],
imports: [
SharedModule,
ButtonModule,
NoopAnimationsModule,
DialogModule,
IconButtonModule,
DialogCloseDirective,
DialogComponent,
DialogTitleContainerDirective,
RouterTestingModule,
LayoutComponent,
],
providers: [DialogService],
}),
applicationConfig({
providers: [
DialogService,
{
provide: I18nService,
useFactory: () => {
return new I18nMockService({
close: "Close",
search: "Search",
skipToContent: "Skip to content",
submenu: "submenu",
toggleCollapse: "toggle collapse",
toggleSideNavigation: "Toggle side navigation",
yes: "Yes",
no: "No",
loading: "Loading",
});
},
},
@ -130,21 +100,4 @@ export default { @@ -130,21 +100,4 @@ export default {
type Story = StoryObj<StoryDialogComponent>;
export const Default: Story = {
play: async (context) => {
const canvas = context.canvasElement;
const button = getAllByRole(canvas, "button")[0];
await userEvent.click(button);
},
};
/** Drawers must be a descendant of `bit-layout`. */
export const Drawer: Story = {
play: async (context) => {
const canvas = context.canvasElement;
const button = getAllByRole(canvas, "button")[1];
await userEvent.click(button);
},
};
export const Default: Story = {};

238
libs/components/src/dialog/dialog.service.ts

@ -1,25 +1,31 @@ @@ -1,25 +1,31 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import {
Dialog as CdkDialog,
DialogConfig as CdkDialogConfig,
DialogRef as CdkDialogRefBase,
DIALOG_DATA,
DialogCloseOptions,
DEFAULT_DIALOG_CONFIG,
Dialog,
DialogConfig,
DialogRef,
DIALOG_SCROLL_STRATEGY,
} from "@angular/cdk/dialog";
import { ComponentType, ScrollStrategy } from "@angular/cdk/overlay";
import { ComponentPortal, Portal } from "@angular/cdk/portal";
import { Injectable, Injector, TemplateRef, inject } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ComponentType, Overlay, OverlayContainer, ScrollStrategy } from "@angular/cdk/overlay";
import {
Inject,
Injectable,
Injector,
OnDestroy,
Optional,
SkipSelf,
TemplateRef,
} from "@angular/core";
import { NavigationEnd, Router } from "@angular/router";
import { filter, firstValueFrom, map, Observable, Subject, switchMap } from "rxjs";
import { filter, firstValueFrom, Subject, switchMap, takeUntil } from "rxjs";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { DrawerService } from "../drawer/drawer.service";
import { SimpleConfigurableDialogComponent } from "./simple-dialog/simple-configurable-dialog/simple-configurable-dialog.component";
import { SimpleDialogOptions } from "./simple-dialog/types";
import { SimpleDialogOptions, Translation } from "./simple-dialog/types";
/**
* The default `BlockScrollStrategy` does not work well with virtual scrolling.
@ -42,163 +48,61 @@ class CustomBlockScrollStrategy implements ScrollStrategy { @@ -42,163 +48,61 @@ class CustomBlockScrollStrategy implements ScrollStrategy {
detach() {}
}
export abstract class DialogRef<R = unknown, C = unknown>
implements Pick<CdkDialogRef<R, C>, "close" | "closed" | "disableClose" | "componentInstance">
{
abstract readonly isDrawer?: boolean;
// --- From CdkDialogRef ---
abstract close(result?: R, options?: DialogCloseOptions): void;
abstract readonly closed: Observable<R | undefined>;
abstract disableClose: boolean | undefined;
/**
* @deprecated
* Does not work with drawer dialogs.
**/
abstract componentInstance: C | null;
}
export type DialogConfig<D = unknown, R = unknown> = Pick<
CdkDialogConfig<D, R>,
"data" | "disableClose" | "ariaModal" | "positionStrategy" | "height" | "width"
>;
class DrawerDialogRef<R = unknown, C = unknown> implements DialogRef<R, C> {
readonly isDrawer = true;
private _closed = new Subject<R | undefined>();
closed = this._closed.asObservable();
disableClose = false;
/** The portal containing the drawer */
portal?: Portal<unknown>;
constructor(private drawerService: DrawerService) {}
close(result?: R, _options?: DialogCloseOptions): void {
if (this.disableClose) {
return;
}
this.drawerService.close(this.portal!);
this._closed.next(result);
this._closed.complete();
}
componentInstance: C | null = null;
}
/**
* DialogRef that delegates functionality to the CDK implementation
**/
export class CdkDialogRef<R = unknown, C = unknown> implements DialogRef<R, C> {
readonly isDrawer = false;
/** This is not available until after construction, @see DialogService.open. */
cdkDialogRefBase!: CdkDialogRefBase<R, C>;
// --- Delegated to CdkDialogRefBase ---
close(result?: R, options?: DialogCloseOptions): void {
this.cdkDialogRefBase.close(result, options);
}
@Injectable()
export class DialogService extends Dialog implements OnDestroy {
private _destroy$ = new Subject<void>();
get closed(): Observable<R | undefined> {
return this.cdkDialogRefBase.closed;
}
private backDropClasses = ["tw-fixed", "tw-bg-black", "tw-bg-opacity-30", "tw-inset-0"];
get disableClose(): boolean | undefined {
return this.cdkDialogRefBase.disableClose;
}
set disableClose(value: boolean | undefined) {
this.cdkDialogRefBase.disableClose = value;
}
private defaultScrollStrategy = new CustomBlockScrollStrategy();
// Delegate the `componentInstance` property to the CDK DialogRef
get componentInstance(): C | null {
return this.cdkDialogRefBase.componentInstance;
}
}
constructor(
/** Parent class constructor */
_overlay: Overlay,
_injector: Injector,
@Optional() @Inject(DEFAULT_DIALOG_CONFIG) _defaultOptions: DialogConfig,
@Optional() @SkipSelf() _parentDialog: Dialog,
_overlayContainer: OverlayContainer,
@Inject(DIALOG_SCROLL_STRATEGY) scrollStrategy: any,
@Injectable()
export class DialogService {
private dialog = inject(CdkDialog);
private drawerService = inject(DrawerService);
private injector = inject(Injector);
private router = inject(Router, { optional: true });
private authService = inject(AuthService, { optional: true });
private i18nService = inject(I18nService);
/** Not in parent class */
@Optional() router: Router,
@Optional() authService: AuthService,
private backDropClasses = ["tw-fixed", "tw-bg-black", "tw-bg-opacity-30", "tw-inset-0"];
private defaultScrollStrategy = new CustomBlockScrollStrategy();
private activeDrawer: DrawerDialogRef<any, any> | null = null;
protected i18nService: I18nService,
) {
super(_overlay, _injector, _defaultOptions, _parentDialog, _overlayContainer, scrollStrategy);
constructor() {
/**
* TODO: This logic should exist outside of `libs/components`.
* @see https://bitwarden.atlassian.net/browse/CL-657
**/
/** Close all open dialogs if the vault locks */
if (this.router && this.authService) {
this.router.events
if (router && authService) {
router.events
.pipe(
filter((event) => event instanceof NavigationEnd),
switchMap(() => this.authService!.getAuthStatus()),
switchMap(() => authService.getAuthStatus()),
filter((v) => v !== AuthenticationStatus.Unlocked),
takeUntilDestroyed(),
takeUntil(this._destroy$),
)
.subscribe(() => this.closeAll());
}
}
open<R = unknown, D = unknown, C = unknown>(
override ngOnDestroy(): void {
this._destroy$.next();
this._destroy$.complete();
super.ngOnDestroy();
}
override open<R = unknown, D = unknown, C = unknown>(
componentOrTemplateRef: ComponentType<C> | TemplateRef<C>,
config?: DialogConfig<D, DialogRef<R, C>>,
): DialogRef<R, C> {
/**
* This is a bit circular in nature:
* We need the DialogRef instance for the DI injector that is passed *to* `Dialog.open`,
* but we get the base CDK DialogRef instance *from* `Dialog.open`.
*
* To break the circle, we define CDKDialogRef as a wrapper for the CDKDialogRefBase.
* This allows us to create the class instance and provide the base instance later, almost like "deferred inheritance".
**/
const ref = new CdkDialogRef<R, C>();
const injector = this.createInjector({
data: config?.data,
dialogRef: ref,
});
// Merge the custom config with the default config
const _config = {
config = {
backdropClass: this.backDropClasses,
scrollStrategy: this.defaultScrollStrategy,
injector,
...config,
};
ref.cdkDialogRefBase = this.dialog.open<R, D, C>(componentOrTemplateRef, _config);
return ref;
}
/** Opens a dialog in the side drawer */
openDrawer<R = unknown, D = unknown, C = unknown>(
component: ComponentType<C>,
config?: DialogConfig<D, DialogRef<R, C>>,
): DialogRef<R, C> {
this.activeDrawer?.close();
/**
* This is also circular. When creating the DrawerDialogRef, we do not yet have a portal instance to provide to the injector.
* Similar to `this.open`, we get around this with mutability.
*/
this.activeDrawer = new DrawerDialogRef(this.drawerService);
const portal = new ComponentPortal(
component,
null,
this.createInjector({ data: config?.data, dialogRef: this.activeDrawer }),
);
this.activeDrawer.portal = portal;
this.drawerService.open(portal);
return this.activeDrawer;
return super.open(componentOrTemplateRef, config);
}
/**
@ -209,7 +113,8 @@ export class DialogService { @@ -209,7 +113,8 @@ export class DialogService {
*/
async openSimpleDialog(simpleDialogOptions: SimpleDialogOptions): Promise<boolean> {
const dialogRef = this.openSimpleDialogRef(simpleDialogOptions);
return firstValueFrom(dialogRef.closed.pipe(map((v: boolean | undefined) => !!v)));
return firstValueFrom(dialogRef.closed);
}
/**
@ -229,29 +134,20 @@ export class DialogService { @@ -229,29 +134,20 @@ export class DialogService {
});
}
/** Close all open dialogs */
closeAll(): void {
return this.dialog.closeAll();
}
protected translate(translation: string | Translation, defaultKey?: string): string {
if (translation == null && defaultKey == null) {
return null;
}
/** The injector that is passed to the opened dialog */
private createInjector(opts: { data: unknown; dialogRef: DialogRef }): Injector {
return Injector.create({
providers: [
{
provide: DIALOG_DATA,
useValue: opts.data,
},
{
provide: DialogRef,
useValue: opts.dialogRef,
},
{
provide: CdkDialogRefBase,
useValue: opts.dialogRef,
},
],
parent: this.injector,
});
if (translation == null) {
return this.i18nService.t(defaultKey);
}
// Translation interface use implies we must localize.
if (typeof translation === "object") {
return this.i18nService.t(translation.key, ...(translation.placeholders ?? []));
}
return translation;
}
}

40
libs/components/src/dialog/dialog/dialog.component.html

@ -1,22 +1,12 @@ @@ -1,22 +1,12 @@
@let isDrawer = dialogRef?.isDrawer;
<section
class="tw-flex tw-w-full tw-flex-col tw-self-center tw-overflow-hidden tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-text-main"
[ngClass]="[width, isDrawer ? 'tw-h-screen tw-border-t-0' : 'tw-rounded-xl']"
class="tw-flex tw-w-full tw-flex-col tw-self-center tw-overflow-hidden tw-rounded-xl tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-text-main"
[ngClass]="width"
@fadeIn
cdkTrapFocus
cdkTrapFocusAutoCapture
>
@let showHeaderBorder = !isDrawer || background === "alt" || bodyHasScrolledFrom().top;
<header
class="tw-flex tw-justify-between tw-items-center tw-gap-4 tw-border-0 tw-border-b tw-border-solid"
[ngClass]="{
'tw-p-4': !isDrawer,
'tw-p-6 tw-pb-4': isDrawer,
'tw-border-secondary-300': showHeaderBorder,
'tw-border-transparent': !showHeaderBorder,
}"
class="tw-flex tw-justify-between tw-items-center tw-gap-4 tw-border-0 tw-border-b tw-border-solid tw-border-secondary-300 tw-p-4"
>
<h2
<h1
bitDialogTitleContainer
bitTypography="h3"
noMargin
@ -29,7 +19,7 @@ @@ -29,7 +19,7 @@
</span>
}
<ng-content select="[bitDialogTitle]"></ng-content>
</h2>
</h1>
<button
type="button"
bitIconButton="bwi-close"
@ -42,11 +32,9 @@ @@ -42,11 +32,9 @@
</header>
<div
class="tw-relative tw-flex-1 tw-flex tw-flex-col tw-overflow-hidden"
class="tw-relative tw-flex tw-flex-col tw-overflow-hidden"
[ngClass]="{
'tw-min-h-60': loading,
'tw-bg-background': background === 'default',
'tw-bg-background-alt': background === 'alt',
}"
>
@if (loading) {
@ -55,28 +43,20 @@ @@ -55,28 +43,20 @@
</div>
}
<div
cdkScrollable
[ngClass]="{
'tw-p-4': !disablePadding && !isDrawer,
'tw-px-6 tw-py-4': !disablePadding && isDrawer,
'tw-p-4': !disablePadding,
'tw-overflow-y-auto': !loading,
'tw-invisible tw-overflow-y-hidden': loading,
'tw-bg-background': background === 'default',
'tw-bg-background-alt': background === 'alt',
}"
>
<ng-content select="[bitDialogContent]"></ng-content>
</div>
</div>
@let showFooterBorder = !isDrawer || background === "alt" || bodyHasScrolledFrom().bottom;
<footer
class="tw-flex tw-flex-row tw-items-center tw-gap-2 tw-border-0 tw-border-t tw-border-solid tw-border-secondary-300 tw-bg-background"
[ngClass]="[isDrawer ? 'tw-px-6 tw-py-4' : 'tw-p-4']"
[ngClass]="{
'tw-px-6 tw-py-4': isDrawer,
'tw-p-4': !isDrawer,
'tw-border-secondary-300': showFooterBorder,
'tw-border-transparent': !showFooterBorder,
}"
class="tw-flex tw-flex-row tw-items-center tw-gap-2 tw-border-0 tw-border-t tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-4"
>
<ng-content select="[bitDialogFooter]"></ng-content>
</footer>

37
libs/components/src/dialog/dialog/dialog.component.ts

@ -1,18 +1,14 @@ @@ -1,18 +1,14 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CdkTrapFocus } from "@angular/cdk/a11y";
import { coerceBooleanProperty } from "@angular/cdk/coercion";
import { CdkScrollable } from "@angular/cdk/scrolling";
import { CommonModule } from "@angular/common";
import { Component, HostBinding, Input, inject, viewChild } from "@angular/core";
import { Component, HostBinding, Input } from "@angular/core";
import { I18nPipe } from "@bitwarden/ui-common";
import { BitIconButtonComponent } from "../../icon-button/icon-button.component";
import { TypographyDirective } from "../../typography/typography.directive";
import { hasScrolledFrom } from "../../utils/has-scrolled-from";
import { fadeIn } from "../animations";
import { DialogRef } from "../dialog.service";
import { DialogCloseDirective } from "../directives/dialog-close.directive";
import { DialogTitleContainerDirective } from "../directives/dialog-title-container.directive";
@ -21,9 +17,6 @@ import { DialogTitleContainerDirective } from "../directives/dialog-title-contai @@ -21,9 +17,6 @@ import { DialogTitleContainerDirective } from "../directives/dialog-title-contai
templateUrl: "./dialog.component.html",
animations: [fadeIn],
standalone: true,
host: {
"(keydown.esc)": "handleEsc($event)",
},
imports: [
CommonModule,
DialogTitleContainerDirective,
@ -31,15 +24,9 @@ import { DialogTitleContainerDirective } from "../directives/dialog-title-contai @@ -31,15 +24,9 @@ import { DialogTitleContainerDirective } from "../directives/dialog-title-contai
BitIconButtonComponent,
DialogCloseDirective,
I18nPipe,
CdkTrapFocus,
CdkScrollable,
],
})
export class DialogComponent {
protected dialogRef = inject(DialogRef, { optional: true });
private scrollableBody = viewChild.required(CdkScrollable);
protected bodyHasScrolledFrom = hasScrolledFrom(this.scrollableBody);
/** Background color */
@Input()
background: "default" | "alt" = "default";
@ -77,31 +64,21 @@ export class DialogComponent { @@ -77,31 +64,21 @@ export class DialogComponent {
@HostBinding("class") get classes() {
// `tw-max-h-[90vh]` is needed to prevent dialogs from overlapping the desktop header
return ["tw-flex", "tw-flex-col", "tw-w-screen"]
.concat(
this.width,
this.dialogRef?.isDrawer
? ["tw-min-h-screen", "md:tw-w-[23rem]"]
: ["tw-p-4", "tw-w-screen", "tw-max-h-[90vh]"],
)
.flat();
}
handleEsc(event: Event) {
this.dialogRef?.close();
event.stopPropagation();
return ["tw-flex", "tw-flex-col", "tw-w-screen", "tw-p-4", "tw-max-h-[90vh]"].concat(
this.width,
);
}
get width() {
switch (this.dialogSize) {
case "small": {
return "md:tw-max-w-sm";
return "tw-max-w-sm";
}
case "large": {
return "md:tw-max-w-3xl";
return "tw-max-w-3xl";
}
default: {
return "md:tw-max-w-xl";
return "tw-max-w-xl";
}
}
}

3
libs/components/src/dialog/dialogs.mdx

@ -22,9 +22,6 @@ For alerts or simple confirmation actions, like speedbumps, use the @@ -22,9 +22,6 @@ For alerts or simple confirmation actions, like speedbumps, use the
Dialogs's should be used sparingly as they do call extra attention to themselves and can be
interruptive if overused.
For non-blocking, supplementary content, open dialogs as a
[Drawer](?path=/story/component-library-dialogs-service--drawer) (requires `bit-layout`).
## Placement
Dialogs should be centered vertically and horizontally on screen. Dialogs height should expand to

2
libs/components/src/dialog/index.ts

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
export * from "./dialog.module";
export * from "./simple-dialog/types";
export * from "./dialog.service";
export { DIALOG_DATA } from "@angular/cdk/dialog";
export { DialogConfig, DIALOG_DATA, DialogRef } from "@angular/cdk/dialog";

18
libs/components/src/drawer/drawer-body.component.ts

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
import { CdkScrollable } from "@angular/cdk/scrolling";
import { ChangeDetectionStrategy, Component } from "@angular/core";
import { hasScrolledFrom } from "../utils/has-scrolled-from";
import { ChangeDetectionStrategy, Component, Signal, inject } from "@angular/core";
import { toSignal } from "@angular/core/rxjs-interop";
import { map } from "rxjs";
/**
* Body container for `bit-drawer`
@ -14,7 +14,7 @@ import { hasScrolledFrom } from "../utils/has-scrolled-from"; @@ -14,7 +14,7 @@ import { hasScrolledFrom } from "../utils/has-scrolled-from";
host: {
class:
"tw-p-4 tw-pt-0 tw-block tw-overflow-auto tw-border-solid tw-border tw-border-transparent tw-transition-colors tw-duration-200",
"[class.tw-border-t-secondary-300]": "this.hasScrolledFrom().top",
"[class.tw-border-t-secondary-300]": "isScrolled()",
},
hostDirectives: [
{
@ -24,5 +24,13 @@ import { hasScrolledFrom } from "../utils/has-scrolled-from"; @@ -24,5 +24,13 @@ import { hasScrolledFrom } from "../utils/has-scrolled-from";
template: ` <ng-content></ng-content> `,
})
export class DrawerBodyComponent {
protected hasScrolledFrom = hasScrolledFrom();
private scrollable = inject(CdkScrollable);
/** TODO: share this utility with browser popup header? */
protected isScrolled: Signal<boolean> = toSignal(
this.scrollable
.elementScrolled()
.pipe(map(() => this.scrollable.measureScrollOffset("top") > 0)),
{ initialValue: false },
);
}

4
libs/components/src/drawer/drawer.component.ts

@ -10,7 +10,7 @@ import { @@ -10,7 +10,7 @@ import {
viewChild,
} from "@angular/core";
import { DrawerService } from "./drawer.service";
import { DrawerHostDirective } from "./drawer-host.directive";
/**
* A drawer is a panel of supplementary content that is adjacent to the page's main content.
@ -25,7 +25,7 @@ import { DrawerService } from "./drawer.service"; @@ -25,7 +25,7 @@ import { DrawerService } from "./drawer.service";
templateUrl: "drawer.component.html",
})
export class DrawerComponent {
private drawerHost = inject(DrawerService);
private drawerHost = inject(DrawerHostDirective);
private portal = viewChild.required(CdkPortal);
/**

2
libs/components/src/drawer/drawer.mdx

@ -12,8 +12,6 @@ import { DrawerComponent } from "@bitwarden/components"; @@ -12,8 +12,6 @@ import { DrawerComponent } from "@bitwarden/components";
# Drawer
**Note: `bit-drawer` is deprecated. Use `bit-dialog` and `DialogService.openDrawer(...)` instead.**
A drawer is a panel of supplementary content that is adjacent to the page's main content.
<Primary />

20
libs/components/src/drawer/drawer.service.ts

@ -1,20 +0,0 @@ @@ -1,20 +0,0 @@
import { Portal } from "@angular/cdk/portal";
import { Injectable, signal } from "@angular/core";
@Injectable({ providedIn: "root" })
export class DrawerService {
private _portal = signal<Portal<unknown> | undefined>(undefined);
/** The portal to display */
portal = this._portal.asReadonly();
open(portal: Portal<unknown>) {
this._portal.set(portal);
}
close(portal: Portal<unknown>) {
if (portal === this.portal()) {
this._portal.set(undefined);
}
}
}

1
libs/components/src/layout/index.ts

@ -1,2 +1 @@ @@ -1,2 +1 @@
export * from "./layout.component";
export * from "./scroll-layout.directive";

89
libs/components/src/layout/layout.component.html

@ -1,52 +1,43 @@ @@ -1,52 +1,43 @@
@let mainContentId = "main-content";
<div class="tw-flex tw-w-full">
<div class="tw-flex tw-w-full" cdkTrapFocus>
<div
class="tw-fixed tw-z-50 tw-w-full tw-flex tw-justify-center tw-opacity-0 focus-within:tw-opacity-100 tw-pointer-events-none focus-within:tw-pointer-events-auto"
>
<nav class="tw-bg-background-alt3 tw-rounded-md tw-rounded-t-none tw-py-2 tw-text-alt2">
<a
bitLink
class="tw-mx-6 focus-visible:before:!tw-ring-0"
[fragment]="mainContentId"
[routerLink]="[]"
(click)="focusMainContent()"
linkType="light"
>{{ "skipToContent" | i18n }}</a
>
</nav>
</div>
<ng-content select="bit-side-nav, [slot=side-nav]"></ng-content>
<main
#main
[id]="mainContentId"
tabindex="-1"
class="tw-overflow-auto tw-max-h-screen tw-min-w-0 tw-flex-1 tw-bg-background tw-p-6 md:tw-ml-0 tw-ml-16"
<div
class="tw-fixed tw-z-50 tw-w-full tw-flex tw-justify-center tw-opacity-0 focus-within:tw-opacity-100 tw-pointer-events-none focus-within:tw-pointer-events-auto"
>
<nav class="tw-bg-background-alt3 tw-rounded-md tw-rounded-t-none tw-py-2 tw-text-alt2">
<a
bitLink
class="tw-mx-6 focus-visible:before:!tw-ring-0"
[fragment]="mainContentId"
[routerLink]="[]"
(click)="focusMainContent()"
linkType="light"
>{{ "skipToContent" | i18n }}</a
>
<ng-content></ng-content>
</nav>
</div>
<div class="tw-flex tw-w-full">
<ng-content select="bit-side-nav, [slot=side-nav]"></ng-content>
<main
[id]="mainContentId"
tabindex="-1"
class="tw-overflow-auto tw-min-w-0 tw-flex-1 tw-bg-background tw-p-6 md:tw-ml-0 tw-ml-16"
>
<ng-content></ng-content>
<!-- overlay backdrop for side-nav -->
@if (
{
open: sideNavService.open$ | async,
};
as data
) {
<div
class="tw-pointer-events-none tw-fixed tw-inset-0 tw-z-10 tw-bg-black tw-bg-opacity-0 motion-safe:tw-transition-colors md:tw-hidden"
[ngClass]="[data.open ? 'tw-bg-opacity-30 md:tw-bg-opacity-0' : 'tw-bg-opacity-0']"
>
@if (data.open) {
<div
(click)="sideNavService.toggle()"
class="tw-pointer-events-auto tw-size-full"
></div>
}
</div>
}
</main>
</div>
<div class="tw-absolute tw-z-50 tw-left-0 md:tw-sticky tw-top-0 tw-h-screen md:tw-w-auto">
<ng-template [cdkPortalOutlet]="drawerPortal()"></ng-template>
</div>
<!-- overlay backdrop for side-nav -->
@if (
{
open: sideNavService.open$ | async,
};
as data
) {
<div
class="tw-pointer-events-none tw-fixed tw-inset-0 tw-z-10 tw-bg-black tw-bg-opacity-0 motion-safe:tw-transition-colors md:tw-hidden"
[ngClass]="[data.open ? 'tw-bg-opacity-30 md:tw-bg-opacity-0' : 'tw-bg-opacity-0']"
>
@if (data.open) {
<div (click)="sideNavService.toggle()" class="tw-pointer-events-auto tw-size-full"></div>
}
</div>
}
</main>
<ng-template [cdkPortalOutlet]="drawerPortal()"></ng-template>
</div>

26
libs/components/src/layout/layout.component.ts

@ -1,10 +1,9 @@ @@ -1,10 +1,9 @@
import { A11yModule, CdkTrapFocus } from "@angular/cdk/a11y";
import { PortalModule } from "@angular/cdk/portal";
import { CommonModule } from "@angular/common";
import { Component, ElementRef, inject, viewChild } from "@angular/core";
import { Component, inject } from "@angular/core";
import { RouterModule } from "@angular/router";
import { DrawerService } from "../drawer/drawer.service";
import { DrawerHostDirective } from "../drawer/drawer-host.directive";
import { LinkModule } from "../link";
import { SideNavService } from "../navigation/side-nav.service";
import { SharedModule } from "../shared";
@ -13,23 +12,16 @@ import { SharedModule } from "../shared"; @@ -13,23 +12,16 @@ import { SharedModule } from "../shared";
selector: "bit-layout",
templateUrl: "layout.component.html",
standalone: true,
imports: [
CommonModule,
SharedModule,
LinkModule,
RouterModule,
PortalModule,
A11yModule,
CdkTrapFocus,
],
imports: [CommonModule, SharedModule, LinkModule, RouterModule, PortalModule],
hostDirectives: [DrawerHostDirective],
})
export class LayoutComponent {
protected sideNavService = inject(SideNavService);
protected drawerPortal = inject(DrawerService).portal;
protected mainContentId = "main-content";
private mainContent = viewChild.required<ElementRef<HTMLElement>>("main");
protected sideNavService = inject(SideNavService);
protected drawerPortal = inject(DrawerHostDirective).portal;
protected focusMainContent() {
this.mainContent().nativeElement.focus();
focusMainContent() {
document.getElementById(this.mainContentId)?.focus();
}
}

35
libs/components/src/layout/scroll-layout.directive.ts

@ -1,35 +0,0 @@ @@ -1,35 +0,0 @@
import { Directionality } from "@angular/cdk/bidi";
import { CdkVirtualScrollable, ScrollDispatcher, VIRTUAL_SCROLLABLE } from "@angular/cdk/scrolling";
import { Directive, ElementRef, NgZone, Optional } from "@angular/core";
@Directive({
selector: "cdk-virtual-scroll-viewport[bitScrollLayout]",
standalone: true,
providers: [{ provide: VIRTUAL_SCROLLABLE, useExisting: ScrollLayoutDirective }],
})
export class ScrollLayoutDirective extends CdkVirtualScrollable {
private mainRef: ElementRef<HTMLElement>;
constructor(scrollDispatcher: ScrollDispatcher, ngZone: NgZone, @Optional() dir: Directionality) {
const mainEl = document.querySelector("main")!;
if (!mainEl) {
// eslint-disable-next-line no-console
console.error("HTML main element must be an ancestor of [bitScrollLayout]");
}
const mainRef = new ElementRef(mainEl);
super(mainRef, scrollDispatcher, ngZone, dir);
this.mainRef = mainRef;
}
override getElementRef(): ElementRef<HTMLElement> {
return this.mainRef;
}
override measureBoundingClientRectWithScrollOffset(
from: "left" | "top" | "right" | "bottom",
): number {
return (
this.mainRef.nativeElement.getBoundingClientRect()[from] - this.measureScrollOffset(from)
);
}
}

12
libs/components/src/stories/kitchen-sink/components/dialog-virtual-scroll-block.component.ts

@ -3,23 +3,15 @@ import { Component, OnInit } from "@angular/core"; @@ -3,23 +3,15 @@ import { Component, OnInit } from "@angular/core";
import { DialogModule, DialogService } from "../../../dialog";
import { IconButtonModule } from "../../../icon-button";
import { ScrollLayoutDirective } from "../../../layout";
import { SectionComponent } from "../../../section";
import { TableDataSource, TableModule } from "../../../table";
@Component({
selector: "dialog-virtual-scroll-block",
standalone: true,
imports: [
DialogModule,
IconButtonModule,
SectionComponent,
TableModule,
ScrollingModule,
ScrollLayoutDirective,
],
imports: [DialogModule, IconButtonModule, SectionComponent, TableModule, ScrollingModule],
template: /*html*/ `<bit-section>
<cdk-virtual-scroll-viewport bitScrollLayout itemSize="63.5">
<cdk-virtual-scroll-viewport scrollWindow itemSize="47">
<bit-table [dataSource]="dataSource">
<ng-container header>
<tr>

133
libs/components/src/stories/kitchen-sink/components/kitchen-sink-main.component.ts

@ -12,69 +12,8 @@ import { KitchenSinkToggleList } from "./kitchen-sink-toggle-list.component"; @@ -12,69 +12,8 @@ import { KitchenSinkToggleList } from "./kitchen-sink-toggle-list.component";
standalone: true,
imports: [KitchenSinkSharedModule],
template: `
<bit-dialog title="Dialog Title" dialogSize="small">
<ng-container bitDialogContent>
<p bitTypography="body1">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt
ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation
ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur
sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id
est laborum.
</p>
<bit-form-field>
<bit-label>What did foo say to bar?</bit-label>
<input bitInput value="Baz" />
</bit-form-field>
<p bitTypography="body1">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt
ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation
ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur
sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id
est laborum.
</p>
<p bitTypography="body1">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt
ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation
ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur
sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id
est laborum.
</p>
<p bitTypography="body1">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt
ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation
ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur
sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id
est laborum.
</p>
<p bitTypography="body1">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt
ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation
ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur
sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id
est laborum.
</p>
<p bitTypography="body1">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt
ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation
ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur
sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id
est laborum.
</p>
<p bitTypography="body1">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt
ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation
ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur
sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id
est laborum.
</p>
</ng-container>
<bit-dialog title="Dialog Title" dialogSize="large">
<span bitDialogContent> Dialog body text goes here. </span>
<ng-container bitDialogFooter>
<button type="button" bitButton buttonType="primary" (click)="dialogRef.close()">OK</button>
<button type="button" bitButton buttonType="secondary" bitDialogClose>Cancel</button>
@ -151,6 +90,72 @@ class KitchenSinkDialog { @@ -151,6 +90,72 @@ class KitchenSinkDialog {
</bit-section>
</bit-tab>
</bit-tab-group>
<bit-drawer [(open)]="drawerOpen">
<bit-drawer-header title="Foo ipsum"></bit-drawer-header>
<bit-drawer-body>
<p bitTypography="body1">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt
ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation
ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur
sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id
est laborum.
</p>
<bit-form-field>
<bit-label>What did foo say to bar?</bit-label>
<input bitInput value="Baz" />
</bit-form-field>
<p bitTypography="body1">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt
ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation
ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur
sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id
est laborum.
</p>
<p bitTypography="body1">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt
ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation
ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur
sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id
est laborum.
</p>
<p bitTypography="body1">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt
ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation
ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur
sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id
est laborum.
</p>
<p bitTypography="body1">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt
ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation
ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur
sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id
est laborum.
</p>
<p bitTypography="body1">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt
ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation
ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur
sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id
est laborum.
</p>
<p bitTypography="body1">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt
ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation
ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur
sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id
est laborum.
</p>
</bit-drawer-body>
</bit-drawer>
`,
})
export class KitchenSinkMainComponent {
@ -163,7 +168,7 @@ export class KitchenSinkMainComponent { @@ -163,7 +168,7 @@ export class KitchenSinkMainComponent {
}
openDrawer() {
this.dialogService.openDrawer(KitchenSinkDialog);
this.drawerOpen.set(true);
}
navItems = [

30
libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts

@ -14,6 +14,7 @@ import { @@ -14,6 +14,7 @@ import {
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { DialogService } from "../../dialog";
import { LayoutComponent } from "../../layout";
import { I18nMockService } from "../../utils/i18n-mock.service";
import { positionFixedWrapperDecorator } from "../storybook-decorators";
@ -38,20 +39,8 @@ export default { @@ -38,20 +39,8 @@ export default {
KitchenSinkTable,
KitchenSinkToggleList,
],
}),
applicationConfig({
providers: [
provideNoopAnimations(),
importProvidersFrom(
RouterModule.forRoot(
[
{ path: "", redirectTo: "bitwarden", pathMatch: "full" },
{ path: "bitwarden", component: KitchenSinkMainComponent },
{ path: "virtual-scroll", component: DialogVirtualScrollBlockComponent },
],
{ useHash: true },
),
),
DialogService,
{
provide: I18nService,
useFactory: () => {
@ -69,6 +58,21 @@ export default { @@ -69,6 +58,21 @@ export default {
},
],
}),
applicationConfig({
providers: [
provideNoopAnimations(),
importProvidersFrom(
RouterModule.forRoot(
[
{ path: "", redirectTo: "bitwarden", pathMatch: "full" },
{ path: "bitwarden", component: KitchenSinkMainComponent },
{ path: "virtual-scroll", component: DialogVirtualScrollBlockComponent },
],
{ useHash: true },
),
),
],
}),
],
} as Meta;

2
libs/components/src/table/table-scroll.component.html

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
<cdk-virtual-scroll-viewport
bitScrollLayout
scrollWindow
[itemSize]="rowSize"
[ngStyle]="{ paddingBottom: headerHeight + 'px' }"
>

3
libs/components/src/table/table-scroll.component.ts

@ -20,8 +20,6 @@ import { @@ -20,8 +20,6 @@ import {
TrackByFunction,
} from "@angular/core";
import { ScrollLayoutDirective } from "../layout";
import { RowDirective } from "./row.directive";
import { TableComponent } from "./table.component";
@ -58,7 +56,6 @@ export class BitRowDef { @@ -58,7 +56,6 @@ export class BitRowDef {
CdkFixedSizeVirtualScroll,
CdkVirtualForOf,
RowDirective,
ScrollLayoutDirective,
],
})
export class TableScrollComponent

12
libs/components/src/table/table.mdx

@ -142,7 +142,7 @@ dataSource.filter = (data) => data.orgType === "family"; @@ -142,7 +142,7 @@ dataSource.filter = (data) => data.orgType === "family";
Rudimentary string filtering is supported out of the box with `TableDataSource.simpleStringFilter`.
It works by converting each entry into a string of it's properties. The provided string is then
compared against the filter value using a simple `indexOf` check. For convenience, you can also just
compared against the filter value using a simple `indexOf` check. For convienence, you can also just
pass a string directly.
```ts
@ -153,7 +153,7 @@ dataSource.filter = "search value"; @@ -153,7 +153,7 @@ dataSource.filter = "search value";
### Virtual Scrolling
It's heavily advised to use virtual scrolling if you expect the table to have any significant amount
It's heavily adviced to use virtual scrolling if you expect the table to have any significant amount
of data. This is done by using the `bit-table-scroll` component instead of the `bit-table`
component. This component behaves slightly different from the `bit-table` component. Instead of
using the `*ngFor` directive to render the rows, you provide a `bitRowDef` template that will be
@ -178,14 +178,6 @@ height and align vertically. @@ -178,14 +178,6 @@ height and align vertically.
</bit-table-scroll>
```
#### Deprecated approach
Before `bit-table-scroll` was introduced, virtual scroll in tables was implemented manually via
constructs from Angular CDK. This included wrapping the table with a `cdk-virtual-scroll-viewport`
and targeting with `bit-layout`'s scroll container with the `bitScrollLayout` directive.
This pattern is deprecated in favor of `bit-table-scroll`.
## Accessibility
- Always include a row or column header with your table; this allows assistive technology to better

68
libs/components/src/table/table.stories.ts

@ -1,13 +1,6 @@ @@ -1,13 +1,6 @@
import { RouterTestingModule } from "@angular/router/testing";
import { Meta, moduleMetadata, StoryObj } from "@storybook/angular";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { countries } from "../form/countries";
import { LayoutComponent } from "../layout";
import { mockLayoutI18n } from "../layout/mocks";
import { positionFixedWrapperDecorator } from "../stories/storybook-decorators";
import { I18nMockService } from "../utils";
import { TableDataSource } from "./table-data-source";
import { TableModule } from "./table.module";
@ -15,17 +8,8 @@ import { TableModule } from "./table.module"; @@ -15,17 +8,8 @@ import { TableModule } from "./table.module";
export default {
title: "Component Library/Table",
decorators: [
positionFixedWrapperDecorator(),
moduleMetadata({
imports: [TableModule, LayoutComponent, RouterTestingModule],
providers: [
{
provide: I18nService,
useFactory: () => {
return new I18nMockService(mockLayoutI18n);
},
},
],
imports: [TableModule],
}),
],
argTypes: {
@ -132,20 +116,18 @@ export const Scrollable: Story = { @@ -132,20 +116,18 @@ export const Scrollable: Story = {
trackBy: (index: number, item: any) => item.id,
},
template: `
<bit-layout>
<bit-table-scroll [dataSource]="dataSource" [rowSize]="43">
<ng-container header>
<th bitCell bitSortable="id" default>Id</th>
<th bitCell bitSortable="name">Name</th>
<th bitCell bitSortable="other" [fn]="sortFn">Other</th>
</ng-container>
<ng-template bitRowDef let-row>
<td bitCell>{{ row.id }}</td>
<td bitCell>{{ row.name }}</td>
<td bitCell>{{ row.other }}</td>
</ng-template>
</bit-table-scroll>
</bit-layout>
<bit-table-scroll [dataSource]="dataSource" [rowSize]="43">
<ng-container header>
<th bitCell bitSortable="id" default>Id</th>
<th bitCell bitSortable="name">Name</th>
<th bitCell bitSortable="other" [fn]="sortFn">Other</th>
</ng-container>
<ng-template bitRowDef let-row>
<td bitCell>{{ row.id }}</td>
<td bitCell>{{ row.name }}</td>
<td bitCell>{{ row.other }}</td>
</ng-template>
</bit-table-scroll>
`,
}),
};
@ -162,19 +144,17 @@ export const Filterable: Story = { @@ -162,19 +144,17 @@ export const Filterable: Story = {
sortFn: (a: any, b: any) => a.id - b.id,
},
template: `
<bit-layout>
<input type="search" placeholder="Search" (input)="dataSource.filter = $event.target.value" />
<bit-table-scroll [dataSource]="dataSource" [rowSize]="43">
<ng-container header>
<th bitCell bitSortable="name" default>Name</th>
<th bitCell bitSortable="value" width="120px">Value</th>
</ng-container>
<ng-template bitRowDef let-row>
<td bitCell>{{ row.name }}</td>
<td bitCell>{{ row.value }}</td>
</ng-template>
</bit-table-scroll>
</bit-layout>
<input type="search" placeholder="Search" (input)="dataSource.filter = $event.target.value" />
<bit-table-scroll [dataSource]="dataSource" [rowSize]="43">
<ng-container header>
<th bitCell bitSortable="name" default>Name</th>
<th bitCell bitSortable="value" width="120px">Value</th>
</ng-container>
<ng-template bitRowDef let-row>
<td bitCell>{{ row.name }}</td>
<td bitCell>{{ row.value }}</td>
</ng-template>
</bit-table-scroll>
`,
}),
};

41
libs/components/src/utils/has-scrolled-from.ts

@ -1,41 +0,0 @@ @@ -1,41 +0,0 @@
import { CdkScrollable } from "@angular/cdk/scrolling";
import { Signal, inject, signal } from "@angular/core";
import { toObservable, toSignal } from "@angular/core/rxjs-interop";
import { map, startWith, switchMap } from "rxjs";
export type ScrollState = {
/** `true` when the scrollbar is not at the top-most position */
top: boolean;
/** `true` when the scrollbar is not at the bottom-most position */
bottom: boolean;
};
/**
* Check if a `CdkScrollable` instance has been scrolled
* @param scrollable The instance to check, defaults to the one provided by the current injector
* @returns {Signal<ScrollState>}
*/
export const hasScrolledFrom = (scrollable?: Signal<CdkScrollable>): Signal<ScrollState> => {
const _scrollable = scrollable ?? signal(inject(CdkScrollable));
const scrollable$ = toObservable(_scrollable);
const scrollState$ = scrollable$.pipe(
switchMap((_scrollable) =>
_scrollable.elementScrolled().pipe(
startWith(null),
map(() => ({
top: _scrollable.measureScrollOffset("top") > 0,
bottom: _scrollable.measureScrollOffset("bottom") > 0,
})),
),
),
);
return toSignal(scrollState$, {
initialValue: {
top: false,
bottom: false,
},
});
};
Loading…
Cancel
Save