mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 21:33:27 +00:00
[CL-622][CL-562][CL-621][CL-632] various drawer improvements (#14120)
- add focus trap to drawers - add config to open existing dialogs as drawers - make drawer take up fill width/height on smaller screens
This commit is contained in:
@@ -22,7 +22,7 @@
|
|||||||
<p *ngIf="!dataSource.filteredData.length">{{ "noGroupsInList" | i18n }}</p>
|
<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
|
<!-- The padding on the bottom of the cdk-virtual-scroll-viewport element is required to prevent table row content
|
||||||
from overflowing the <main> element. -->
|
from overflowing the <main> element. -->
|
||||||
<cdk-virtual-scroll-viewport scrollWindow [itemSize]="rowHeight" class="tw-pb-8">
|
<cdk-virtual-scroll-viewport bitScrollLayout [itemSize]="rowHeight" class="tw-pb-8">
|
||||||
<bit-table *ngIf="dataSource.filteredData.length" [dataSource]="dataSource">
|
<bit-table *ngIf="dataSource.filteredData.length" [dataSource]="dataSource">
|
||||||
<ng-container header>
|
<ng-container header>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -67,7 +67,7 @@
|
|||||||
</bit-callout>
|
</bit-callout>
|
||||||
<!-- The padding on the bottom of the cdk-virtual-scroll-viewport element is required to prevent table row content
|
<!-- The padding on the bottom of the cdk-virtual-scroll-viewport element is required to prevent table row content
|
||||||
from overflowing the <main> element. -->
|
from overflowing the <main> element. -->
|
||||||
<cdk-virtual-scroll-viewport scrollWindow [itemSize]="rowHeight" class="tw-pb-8">
|
<cdk-virtual-scroll-viewport bitScrollLayout [itemSize]="rowHeight" class="tw-pb-8">
|
||||||
<bit-table [dataSource]="dataSource">
|
<bit-table [dataSource]="dataSource">
|
||||||
<ng-container header>
|
<ng-container header>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { NgModule } from "@angular/core";
|
|||||||
|
|
||||||
import { PasswordStrengthV2Component } from "@bitwarden/angular/tools/password-strength/password-strength-v2.component";
|
import { PasswordStrengthV2Component } from "@bitwarden/angular/tools/password-strength/password-strength-v2.component";
|
||||||
import { PasswordCalloutComponent } from "@bitwarden/auth/angular";
|
import { PasswordCalloutComponent } from "@bitwarden/auth/angular";
|
||||||
|
import { ScrollLayoutDirective } from "@bitwarden/components";
|
||||||
|
|
||||||
import { LooseComponentsModule } from "../../../shared";
|
import { LooseComponentsModule } from "../../../shared";
|
||||||
import { SharedOrganizationModule } from "../shared";
|
import { SharedOrganizationModule } from "../shared";
|
||||||
@@ -27,6 +28,7 @@ import { MembersComponent } from "./members.component";
|
|||||||
PasswordCalloutComponent,
|
PasswordCalloutComponent,
|
||||||
ScrollingModule,
|
ScrollingModule,
|
||||||
PasswordStrengthV2Component,
|
PasswordStrengthV2Component,
|
||||||
|
ScrollLayoutDirective,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
BulkConfirmDialogComponent,
|
BulkConfirmDialogComponent,
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { ScrollingModule } from "@angular/cdk/scrolling";
|
import { ScrollingModule } from "@angular/cdk/scrolling";
|
||||||
import { NgModule } from "@angular/core";
|
import { NgModule } from "@angular/core";
|
||||||
|
|
||||||
|
import { ScrollLayoutDirective } from "@bitwarden/components";
|
||||||
|
|
||||||
import { LooseComponentsModule } from "../../shared";
|
import { LooseComponentsModule } from "../../shared";
|
||||||
|
|
||||||
import { CoreOrganizationModule } from "./core";
|
import { CoreOrganizationModule } from "./core";
|
||||||
@@ -18,6 +20,7 @@ import { AccessSelectorModule } from "./shared/components/access-selector";
|
|||||||
OrganizationsRoutingModule,
|
OrganizationsRoutingModule,
|
||||||
LooseComponentsModule,
|
LooseComponentsModule,
|
||||||
ScrollingModule,
|
ScrollingModule,
|
||||||
|
ScrollLayoutDirective,
|
||||||
],
|
],
|
||||||
declarations: [GroupsComponent, GroupAddEditComponent],
|
declarations: [GroupsComponent, GroupAddEditComponent],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -613,5 +613,5 @@ export function openCollectionDialog(
|
|||||||
dialogService: DialogService,
|
dialogService: DialogService,
|
||||||
config: DialogConfig<CollectionDialogParams, DialogRef<CollectionDialogResult>>,
|
config: DialogConfig<CollectionDialogParams, DialogRef<CollectionDialogResult>>,
|
||||||
) {
|
) {
|
||||||
return dialogService.open(CollectionDialogComponent, config);
|
return dialogService.open<CollectionDialogResult>(CollectionDialogComponent, config);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,13 @@ import { DeviceType } from "@bitwarden/common/enums";
|
|||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||||
import { MessageListener } from "@bitwarden/common/platform/messaging";
|
import { MessageListener } from "@bitwarden/common/platform/messaging";
|
||||||
import { DialogService, ToastService, TableModule, PopoverModule } from "@bitwarden/components";
|
import {
|
||||||
|
DialogService,
|
||||||
|
ToastService,
|
||||||
|
TableModule,
|
||||||
|
PopoverModule,
|
||||||
|
LayoutComponent,
|
||||||
|
} from "@bitwarden/components";
|
||||||
|
|
||||||
import { SharedModule } from "../../../shared";
|
import { SharedModule } from "../../../shared";
|
||||||
import { VaultBannersService } from "../../../vault/individual-vault/vault-banners/services/vault-banners.service";
|
import { VaultBannersService } from "../../../vault/individual-vault/vault-banners/services/vault-banners.service";
|
||||||
@@ -115,6 +121,12 @@ describe("DeviceManagementComponent", () => {
|
|||||||
showError: jest.fn(),
|
showError: jest.fn(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: LayoutComponent,
|
||||||
|
useValue: {
|
||||||
|
mainContent: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
|
||||||
import { Component, Inject } from "@angular/core";
|
import { Component, Inject } from "@angular/core";
|
||||||
import {
|
import {
|
||||||
AbstractControl,
|
AbstractControl,
|
||||||
@@ -19,7 +18,10 @@ import { EncryptService } from "@bitwarden/common/key-management/crypto/abstract
|
|||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { OrgKey } from "@bitwarden/common/types/key";
|
import { OrgKey } from "@bitwarden/common/types/key";
|
||||||
import {
|
import {
|
||||||
|
DialogRef,
|
||||||
ButtonModule,
|
ButtonModule,
|
||||||
|
DialogConfig,
|
||||||
|
DIALOG_DATA,
|
||||||
DialogModule,
|
DialogModule,
|
||||||
DialogService,
|
DialogService,
|
||||||
FormFieldModule,
|
FormFieldModule,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { DialogRef } from "@angular/cdk/dialog";
|
|
||||||
import { formatDate } from "@angular/common";
|
import { formatDate } from "@angular/common";
|
||||||
import { Component, OnInit, signal } from "@angular/core";
|
import { Component, OnInit, signal } from "@angular/core";
|
||||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||||
@@ -16,7 +15,7 @@ import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
|||||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||||
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||||
import { OrgKey } from "@bitwarden/common/types/key";
|
import { OrgKey } from "@bitwarden/common/types/key";
|
||||||
import { DialogService, ToastService } from "@bitwarden/components";
|
import { DialogRef, DialogService, ToastService } from "@bitwarden/components";
|
||||||
import { KeyService } from "@bitwarden/key-management";
|
import { KeyService } from "@bitwarden/key-management";
|
||||||
|
|
||||||
import { AddSponsorshipDialogComponent } from "./add-sponsorship-dialog.component";
|
import { AddSponsorshipDialogComponent } from "./add-sponsorship-dialog.component";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<cdk-virtual-scroll-viewport [itemSize]="RowHeight" scrollWindow class="tw-pb-8">
|
<cdk-virtual-scroll-viewport [itemSize]="RowHeight" bitScrollLayout class="tw-pb-8">
|
||||||
<bit-table [dataSource]="dataSource" layout="fixed">
|
<bit-table [dataSource]="dataSource" layout="fixed">
|
||||||
<ng-container header>
|
<ng-container header>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { CommonModule } from "@angular/common";
|
|||||||
import { NgModule } from "@angular/core";
|
import { NgModule } from "@angular/core";
|
||||||
import { RouterModule } from "@angular/router";
|
import { RouterModule } from "@angular/router";
|
||||||
|
|
||||||
import { TableModule } from "@bitwarden/components";
|
import { ScrollLayoutDirective, TableModule } from "@bitwarden/components";
|
||||||
|
|
||||||
import { CollectionNameBadgeComponent } from "../../../admin-console/organizations/collections";
|
import { CollectionNameBadgeComponent } from "../../../admin-console/organizations/collections";
|
||||||
import { GroupBadgeModule } from "../../../admin-console/organizations/collections/group-badge/group-badge.module";
|
import { GroupBadgeModule } from "../../../admin-console/organizations/collections/group-badge/group-badge.module";
|
||||||
@@ -26,6 +26,7 @@ import { VaultItemsComponent } from "./vault-items.component";
|
|||||||
CollectionNameBadgeComponent,
|
CollectionNameBadgeComponent,
|
||||||
GroupBadgeModule,
|
GroupBadgeModule,
|
||||||
PipesModule,
|
PipesModule,
|
||||||
|
ScrollLayoutDirective,
|
||||||
],
|
],
|
||||||
declarations: [VaultItemsComponent, VaultCipherRowComponent, VaultCollectionRowComponent],
|
declarations: [VaultItemsComponent, VaultCipherRowComponent, VaultCollectionRowComponent],
|
||||||
exports: [VaultItemsComponent],
|
exports: [VaultItemsComponent],
|
||||||
|
|||||||
@@ -2,7 +2,13 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { importProvidersFrom } from "@angular/core";
|
import { importProvidersFrom } from "@angular/core";
|
||||||
import { RouterModule } from "@angular/router";
|
import { RouterModule } from "@angular/router";
|
||||||
import { applicationConfig, Meta, moduleMetadata, StoryObj } from "@storybook/angular";
|
import {
|
||||||
|
applicationConfig,
|
||||||
|
componentWrapperDecorator,
|
||||||
|
Meta,
|
||||||
|
moduleMetadata,
|
||||||
|
StoryObj,
|
||||||
|
} from "@storybook/angular";
|
||||||
import { BehaviorSubject, of } from "rxjs";
|
import { BehaviorSubject, of } from "rxjs";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -29,6 +35,7 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
|||||||
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
||||||
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
|
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
|
||||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||||
|
import { LayoutComponent } from "@bitwarden/components";
|
||||||
|
|
||||||
import { GroupView } from "../../../admin-console/organizations/core";
|
import { GroupView } from "../../../admin-console/organizations/core";
|
||||||
import { PreloadedEnglishI18nModule } from "../../../core/tests";
|
import { PreloadedEnglishI18nModule } from "../../../core/tests";
|
||||||
@@ -48,8 +55,9 @@ export default {
|
|||||||
title: "Web/Vault/Items",
|
title: "Web/Vault/Items",
|
||||||
component: VaultItemsComponent,
|
component: VaultItemsComponent,
|
||||||
decorators: [
|
decorators: [
|
||||||
|
componentWrapperDecorator((story) => `<bit-layout>${story}</bit-layout>`),
|
||||||
moduleMetadata({
|
moduleMetadata({
|
||||||
imports: [VaultItemsModule, RouterModule],
|
imports: [VaultItemsModule, RouterModule, LayoutComponent],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
provide: EnvironmentService,
|
provide: EnvironmentService,
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
>
|
>
|
||||||
{{ "providerUsersNeedConfirmed" | i18n }}
|
{{ "providerUsersNeedConfirmed" | i18n }}
|
||||||
</bit-callout>
|
</bit-callout>
|
||||||
<cdk-virtual-scroll-viewport scrollWindow [itemSize]="rowHeight" class="tw-pb-8">
|
<cdk-virtual-scroll-viewport bitScrollLayout [itemSize]="rowHeight" class="tw-pb-8">
|
||||||
<bit-table [dataSource]="dataSource">
|
<bit-table [dataSource]="dataSource">
|
||||||
<ng-container header>
|
<ng-container header>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { NgModule } from "@angular/core";
|
|||||||
import { FormsModule } from "@angular/forms";
|
import { FormsModule } from "@angular/forms";
|
||||||
|
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { CardComponent, SearchModule } from "@bitwarden/components";
|
import { CardComponent, ScrollLayoutDirective, SearchModule } from "@bitwarden/components";
|
||||||
import { DangerZoneComponent } from "@bitwarden/web-vault/app/auth/settings/account/danger-zone.component";
|
import { DangerZoneComponent } from "@bitwarden/web-vault/app/auth/settings/account/danger-zone.component";
|
||||||
import { OrganizationPlansComponent } from "@bitwarden/web-vault/app/billing";
|
import { OrganizationPlansComponent } from "@bitwarden/web-vault/app/billing";
|
||||||
import { PaymentComponent } from "@bitwarden/web-vault/app/billing/shared/payment/payment.component";
|
import { PaymentComponent } from "@bitwarden/web-vault/app/billing/shared/payment/payment.component";
|
||||||
@@ -53,6 +53,7 @@ import { VerifyRecoverDeleteProviderComponent } from "./verify-recover-delete-pr
|
|||||||
ScrollingModule,
|
ScrollingModule,
|
||||||
VerifyBankAccountComponent,
|
VerifyBankAccountComponent,
|
||||||
CardComponent,
|
CardComponent,
|
||||||
|
ScrollLayoutDirective,
|
||||||
PaymentComponent,
|
PaymentComponent,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { BasePortalOutlet } from "@angular/cdk/portal";
|
|
||||||
import { Component, Inject, OnInit } from "@angular/core";
|
import { Component, Inject, OnInit } from "@angular/core";
|
||||||
import { FormControl, FormGroup, Validators } from "@angular/forms";
|
import { FormControl, FormGroup, Validators } from "@angular/forms";
|
||||||
|
|
||||||
@@ -33,8 +32,7 @@ export const openCreateClientDialog = (
|
|||||||
dialogService: DialogService,
|
dialogService: DialogService,
|
||||||
dialogConfig: DialogConfig<
|
dialogConfig: DialogConfig<
|
||||||
CreateClientDialogParams,
|
CreateClientDialogParams,
|
||||||
DialogRef<CreateClientDialogResultType, unknown>,
|
DialogRef<CreateClientDialogResultType, unknown>
|
||||||
BasePortalOutlet
|
|
||||||
>,
|
>,
|
||||||
) =>
|
) =>
|
||||||
dialogService.open<CreateClientDialogResultType, CreateClientDialogParams>(
|
dialogService.open<CreateClientDialogResultType, CreateClientDialogParams>(
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
import { DIALOG_DATA, DialogModule, DialogRef } from "@angular/cdk/dialog";
|
import { DIALOG_DATA, DialogModule, DialogRef } from "@angular/cdk/dialog";
|
||||||
import { Component, Inject } from "@angular/core";
|
import { Component, Inject } from "@angular/core";
|
||||||
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
|
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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
|
||||||
import { ButtonModule } from "../button";
|
import { ButtonModule } from "../button";
|
||||||
import { IconButtonModule } from "../icon-button";
|
import { IconButtonModule } from "../icon-button";
|
||||||
|
import { LayoutComponent } from "../layout";
|
||||||
import { SharedModule } from "../shared";
|
import { SharedModule } from "../shared";
|
||||||
|
import { positionFixedWrapperDecorator } from "../stories/storybook-decorators";
|
||||||
import { I18nMockService } from "../utils/i18n-mock.service";
|
import { I18nMockService } from "../utils/i18n-mock.service";
|
||||||
|
|
||||||
import { DialogComponent } from "./dialog/dialog.component";
|
import { DialogComponent } from "./dialog/dialog.component";
|
||||||
@@ -19,7 +24,12 @@ interface Animal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
template: `<button bitButton type="button" (click)="openDialog()">Open Dialog</button>`,
|
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>
|
||||||
|
`,
|
||||||
})
|
})
|
||||||
class StoryDialogComponent {
|
class StoryDialogComponent {
|
||||||
constructor(public dialogService: DialogService) {}
|
constructor(public dialogService: DialogService) {}
|
||||||
@@ -31,6 +41,14 @@ class StoryDialogComponent {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openDrawer() {
|
||||||
|
this.dialogService.openDrawer(StoryDialogContentComponent, {
|
||||||
|
data: {
|
||||||
|
animal: "panda",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -65,25 +83,37 @@ export default {
|
|||||||
title: "Component Library/Dialogs/Service",
|
title: "Component Library/Dialogs/Service",
|
||||||
component: StoryDialogComponent,
|
component: StoryDialogComponent,
|
||||||
decorators: [
|
decorators: [
|
||||||
|
positionFixedWrapperDecorator(),
|
||||||
moduleMetadata({
|
moduleMetadata({
|
||||||
declarations: [StoryDialogContentComponent],
|
declarations: [StoryDialogContentComponent],
|
||||||
imports: [
|
imports: [
|
||||||
SharedModule,
|
SharedModule,
|
||||||
ButtonModule,
|
ButtonModule,
|
||||||
|
NoopAnimationsModule,
|
||||||
DialogModule,
|
DialogModule,
|
||||||
IconButtonModule,
|
IconButtonModule,
|
||||||
DialogCloseDirective,
|
DialogCloseDirective,
|
||||||
DialogComponent,
|
DialogComponent,
|
||||||
DialogTitleContainerDirective,
|
DialogTitleContainerDirective,
|
||||||
|
RouterTestingModule,
|
||||||
|
LayoutComponent,
|
||||||
],
|
],
|
||||||
|
providers: [DialogService],
|
||||||
|
}),
|
||||||
|
applicationConfig({
|
||||||
providers: [
|
providers: [
|
||||||
DialogService,
|
|
||||||
{
|
{
|
||||||
provide: I18nService,
|
provide: I18nService,
|
||||||
useFactory: () => {
|
useFactory: () => {
|
||||||
return new I18nMockService({
|
return new I18nMockService({
|
||||||
close: "Close",
|
close: "Close",
|
||||||
loading: "Loading",
|
search: "Search",
|
||||||
|
skipToContent: "Skip to content",
|
||||||
|
submenu: "submenu",
|
||||||
|
toggleCollapse: "toggle collapse",
|
||||||
|
toggleSideNavigation: "Toggle side navigation",
|
||||||
|
yes: "Yes",
|
||||||
|
no: "No",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -100,4 +130,21 @@ export default {
|
|||||||
|
|
||||||
type Story = StoryObj<StoryDialogComponent>;
|
type Story = StoryObj<StoryDialogComponent>;
|
||||||
|
|
||||||
export const Default: Story = {};
|
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);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,31 +1,25 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
|
||||||
// @ts-strict-ignore
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_DIALOG_CONFIG,
|
Dialog as CdkDialog,
|
||||||
Dialog,
|
DialogConfig as CdkDialogConfig,
|
||||||
DialogConfig,
|
DialogRef as CdkDialogRefBase,
|
||||||
DialogRef,
|
DIALOG_DATA,
|
||||||
DIALOG_SCROLL_STRATEGY,
|
DialogCloseOptions,
|
||||||
} from "@angular/cdk/dialog";
|
} from "@angular/cdk/dialog";
|
||||||
import { ComponentType, Overlay, OverlayContainer, ScrollStrategy } from "@angular/cdk/overlay";
|
import { ComponentType, ScrollStrategy } from "@angular/cdk/overlay";
|
||||||
import {
|
import { ComponentPortal, Portal } from "@angular/cdk/portal";
|
||||||
Inject,
|
import { Injectable, Injector, TemplateRef, inject } from "@angular/core";
|
||||||
Injectable,
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||||
Injector,
|
|
||||||
OnDestroy,
|
|
||||||
Optional,
|
|
||||||
SkipSelf,
|
|
||||||
TemplateRef,
|
|
||||||
} from "@angular/core";
|
|
||||||
import { NavigationEnd, Router } from "@angular/router";
|
import { NavigationEnd, Router } from "@angular/router";
|
||||||
import { filter, firstValueFrom, Subject, switchMap, takeUntil } from "rxjs";
|
import { filter, firstValueFrom, map, Observable, Subject, switchMap } from "rxjs";
|
||||||
|
|
||||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
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 { SimpleConfigurableDialogComponent } from "./simple-dialog/simple-configurable-dialog/simple-configurable-dialog.component";
|
||||||
import { SimpleDialogOptions, Translation } from "./simple-dialog/types";
|
import { SimpleDialogOptions } from "./simple-dialog/types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default `BlockScrollStrategy` does not work well with virtual scrolling.
|
* The default `BlockScrollStrategy` does not work well with virtual scrolling.
|
||||||
@@ -48,61 +42,163 @@ class CustomBlockScrollStrategy implements ScrollStrategy {
|
|||||||
detach() {}
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
get closed(): Observable<R | undefined> {
|
||||||
|
return this.cdkDialogRefBase.closed;
|
||||||
|
}
|
||||||
|
|
||||||
|
get disableClose(): boolean | undefined {
|
||||||
|
return this.cdkDialogRefBase.disableClose;
|
||||||
|
}
|
||||||
|
set disableClose(value: boolean | undefined) {
|
||||||
|
this.cdkDialogRefBase.disableClose = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delegate the `componentInstance` property to the CDK DialogRef
|
||||||
|
get componentInstance(): C | null {
|
||||||
|
return this.cdkDialogRefBase.componentInstance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DialogService extends Dialog implements OnDestroy {
|
export class DialogService {
|
||||||
private _destroy$ = new Subject<void>();
|
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);
|
||||||
|
|
||||||
private backDropClasses = ["tw-fixed", "tw-bg-black", "tw-bg-opacity-30", "tw-inset-0"];
|
private backDropClasses = ["tw-fixed", "tw-bg-black", "tw-bg-opacity-30", "tw-inset-0"];
|
||||||
|
|
||||||
private defaultScrollStrategy = new CustomBlockScrollStrategy();
|
private defaultScrollStrategy = new CustomBlockScrollStrategy();
|
||||||
|
private activeDrawer: DrawerDialogRef<any, any> | null = null;
|
||||||
|
|
||||||
constructor(
|
constructor() {
|
||||||
/** Parent class constructor */
|
/**
|
||||||
_overlay: Overlay,
|
* TODO: This logic should exist outside of `libs/components`.
|
||||||
_injector: Injector,
|
* @see https://bitwarden.atlassian.net/browse/CL-657
|
||||||
@Optional() @Inject(DEFAULT_DIALOG_CONFIG) _defaultOptions: DialogConfig,
|
**/
|
||||||
@Optional() @SkipSelf() _parentDialog: Dialog,
|
|
||||||
_overlayContainer: OverlayContainer,
|
|
||||||
@Inject(DIALOG_SCROLL_STRATEGY) scrollStrategy: any,
|
|
||||||
|
|
||||||
/** Not in parent class */
|
|
||||||
@Optional() router: Router,
|
|
||||||
@Optional() authService: AuthService,
|
|
||||||
|
|
||||||
protected i18nService: I18nService,
|
|
||||||
) {
|
|
||||||
super(_overlay, _injector, _defaultOptions, _parentDialog, _overlayContainer, scrollStrategy);
|
|
||||||
|
|
||||||
/** Close all open dialogs if the vault locks */
|
/** Close all open dialogs if the vault locks */
|
||||||
if (router && authService) {
|
if (this.router && this.authService) {
|
||||||
router.events
|
this.router.events
|
||||||
.pipe(
|
.pipe(
|
||||||
filter((event) => event instanceof NavigationEnd),
|
filter((event) => event instanceof NavigationEnd),
|
||||||
switchMap(() => authService.getAuthStatus()),
|
switchMap(() => this.authService!.getAuthStatus()),
|
||||||
filter((v) => v !== AuthenticationStatus.Unlocked),
|
filter((v) => v !== AuthenticationStatus.Unlocked),
|
||||||
takeUntil(this._destroy$),
|
takeUntilDestroyed(),
|
||||||
)
|
)
|
||||||
.subscribe(() => this.closeAll());
|
.subscribe(() => this.closeAll());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override ngOnDestroy(): void {
|
open<R = unknown, D = unknown, C = unknown>(
|
||||||
this._destroy$.next();
|
|
||||||
this._destroy$.complete();
|
|
||||||
super.ngOnDestroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
override open<R = unknown, D = unknown, C = unknown>(
|
|
||||||
componentOrTemplateRef: ComponentType<C> | TemplateRef<C>,
|
componentOrTemplateRef: ComponentType<C> | TemplateRef<C>,
|
||||||
config?: DialogConfig<D, DialogRef<R, C>>,
|
config?: DialogConfig<D, DialogRef<R, C>>,
|
||||||
): DialogRef<R, C> {
|
): DialogRef<R, C> {
|
||||||
config = {
|
/**
|
||||||
|
* 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 = {
|
||||||
backdropClass: this.backDropClasses,
|
backdropClass: this.backDropClasses,
|
||||||
scrollStrategy: this.defaultScrollStrategy,
|
scrollStrategy: this.defaultScrollStrategy,
|
||||||
|
injector,
|
||||||
...config,
|
...config,
|
||||||
};
|
};
|
||||||
|
|
||||||
return super.open(componentOrTemplateRef, 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -113,8 +209,7 @@ export class DialogService extends Dialog implements OnDestroy {
|
|||||||
*/
|
*/
|
||||||
async openSimpleDialog(simpleDialogOptions: SimpleDialogOptions): Promise<boolean> {
|
async openSimpleDialog(simpleDialogOptions: SimpleDialogOptions): Promise<boolean> {
|
||||||
const dialogRef = this.openSimpleDialogRef(simpleDialogOptions);
|
const dialogRef = this.openSimpleDialogRef(simpleDialogOptions);
|
||||||
|
return firstValueFrom(dialogRef.closed.pipe(map((v: boolean | undefined) => !!v)));
|
||||||
return firstValueFrom(dialogRef.closed);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -134,20 +229,29 @@ export class DialogService extends Dialog implements OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected translate(translation: string | Translation, defaultKey?: string): string {
|
/** Close all open dialogs */
|
||||||
if (translation == null && defaultKey == null) {
|
closeAll(): void {
|
||||||
return null;
|
return this.dialog.closeAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (translation == null) {
|
/** The injector that is passed to the opened dialog */
|
||||||
return this.i18nService.t(defaultKey);
|
private createInjector(opts: { data: unknown; dialogRef: DialogRef }): Injector {
|
||||||
}
|
return Injector.create({
|
||||||
|
providers: [
|
||||||
// Translation interface use implies we must localize.
|
{
|
||||||
if (typeof translation === "object") {
|
provide: DIALOG_DATA,
|
||||||
return this.i18nService.t(translation.key, ...(translation.placeholders ?? []));
|
useValue: opts.data,
|
||||||
}
|
},
|
||||||
|
{
|
||||||
return translation;
|
provide: DialogRef,
|
||||||
|
useValue: opts.dialogRef,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: CdkDialogRefBase,
|
||||||
|
useValue: opts.dialogRef,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
parent: this.injector,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,22 @@
|
|||||||
|
@let isDrawer = dialogRef?.isDrawer;
|
||||||
<section
|
<section
|
||||||
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"
|
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"
|
[ngClass]="[width, isDrawer ? 'tw-h-screen tw-border-t-0' : 'tw-rounded-xl']"
|
||||||
@fadeIn
|
@fadeIn
|
||||||
|
cdkTrapFocus
|
||||||
|
cdkTrapFocusAutoCapture
|
||||||
>
|
>
|
||||||
|
@let showHeaderBorder = !isDrawer || background === "alt" || bodyHasScrolledFrom().top;
|
||||||
<header
|
<header
|
||||||
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"
|
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,
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<h1
|
<h2
|
||||||
bitDialogTitleContainer
|
bitDialogTitleContainer
|
||||||
bitTypography="h3"
|
bitTypography="h3"
|
||||||
noMargin
|
noMargin
|
||||||
@@ -19,7 +29,7 @@
|
|||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
<ng-content select="[bitDialogTitle]"></ng-content>
|
<ng-content select="[bitDialogTitle]"></ng-content>
|
||||||
</h1>
|
</h2>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
bitIconButton="bwi-close"
|
bitIconButton="bwi-close"
|
||||||
@@ -32,9 +42,11 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="tw-relative tw-flex tw-flex-col tw-overflow-hidden"
|
class="tw-relative tw-flex-1 tw-flex tw-flex-col tw-overflow-hidden"
|
||||||
[ngClass]="{
|
[ngClass]="{
|
||||||
'tw-min-h-60': loading,
|
'tw-min-h-60': loading,
|
||||||
|
'tw-bg-background': background === 'default',
|
||||||
|
'tw-bg-background-alt': background === 'alt',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
@if (loading) {
|
@if (loading) {
|
||||||
@@ -43,20 +55,28 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<div
|
<div
|
||||||
|
cdkScrollable
|
||||||
[ngClass]="{
|
[ngClass]="{
|
||||||
'tw-p-4': !disablePadding,
|
'tw-p-4': !disablePadding && !isDrawer,
|
||||||
|
'tw-px-6 tw-py-4': !disablePadding && isDrawer,
|
||||||
'tw-overflow-y-auto': !loading,
|
'tw-overflow-y-auto': !loading,
|
||||||
'tw-invisible tw-overflow-y-hidden': 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>
|
<ng-content select="[bitDialogContent]"></ng-content>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@let showFooterBorder = !isDrawer || background === "alt" || bodyHasScrolledFrom().bottom;
|
||||||
<footer
|
<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 tw-p-4"
|
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,
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<ng-content select="[bitDialogFooter]"></ng-content>
|
<ng-content select="[bitDialogFooter]"></ng-content>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
|
import { CdkTrapFocus } from "@angular/cdk/a11y";
|
||||||
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||||
|
import { CdkScrollable } from "@angular/cdk/scrolling";
|
||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component, HostBinding, Input } from "@angular/core";
|
import { Component, HostBinding, Input, inject, viewChild } from "@angular/core";
|
||||||
|
|
||||||
import { I18nPipe } from "@bitwarden/ui-common";
|
import { I18nPipe } from "@bitwarden/ui-common";
|
||||||
|
|
||||||
import { BitIconButtonComponent } from "../../icon-button/icon-button.component";
|
import { BitIconButtonComponent } from "../../icon-button/icon-button.component";
|
||||||
import { TypographyDirective } from "../../typography/typography.directive";
|
import { TypographyDirective } from "../../typography/typography.directive";
|
||||||
|
import { hasScrolledFrom } from "../../utils/has-scrolled-from";
|
||||||
import { fadeIn } from "../animations";
|
import { fadeIn } from "../animations";
|
||||||
|
import { DialogRef } from "../dialog.service";
|
||||||
import { DialogCloseDirective } from "../directives/dialog-close.directive";
|
import { DialogCloseDirective } from "../directives/dialog-close.directive";
|
||||||
import { DialogTitleContainerDirective } from "../directives/dialog-title-container.directive";
|
import { DialogTitleContainerDirective } from "../directives/dialog-title-container.directive";
|
||||||
|
|
||||||
@@ -17,6 +21,9 @@ import { DialogTitleContainerDirective } from "../directives/dialog-title-contai
|
|||||||
templateUrl: "./dialog.component.html",
|
templateUrl: "./dialog.component.html",
|
||||||
animations: [fadeIn],
|
animations: [fadeIn],
|
||||||
standalone: true,
|
standalone: true,
|
||||||
|
host: {
|
||||||
|
"(keydown.esc)": "handleEsc($event)",
|
||||||
|
},
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
DialogTitleContainerDirective,
|
DialogTitleContainerDirective,
|
||||||
@@ -24,9 +31,15 @@ import { DialogTitleContainerDirective } from "../directives/dialog-title-contai
|
|||||||
BitIconButtonComponent,
|
BitIconButtonComponent,
|
||||||
DialogCloseDirective,
|
DialogCloseDirective,
|
||||||
I18nPipe,
|
I18nPipe,
|
||||||
|
CdkTrapFocus,
|
||||||
|
CdkScrollable,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class DialogComponent {
|
export class DialogComponent {
|
||||||
|
protected dialogRef = inject(DialogRef, { optional: true });
|
||||||
|
private scrollableBody = viewChild.required(CdkScrollable);
|
||||||
|
protected bodyHasScrolledFrom = hasScrolledFrom(this.scrollableBody);
|
||||||
|
|
||||||
/** Background color */
|
/** Background color */
|
||||||
@Input()
|
@Input()
|
||||||
background: "default" | "alt" = "default";
|
background: "default" | "alt" = "default";
|
||||||
@@ -64,21 +77,31 @@ export class DialogComponent {
|
|||||||
|
|
||||||
@HostBinding("class") get classes() {
|
@HostBinding("class") get classes() {
|
||||||
// `tw-max-h-[90vh]` is needed to prevent dialogs from overlapping the desktop header
|
// `tw-max-h-[90vh]` is needed to prevent dialogs from overlapping the desktop header
|
||||||
return ["tw-flex", "tw-flex-col", "tw-w-screen", "tw-p-4", "tw-max-h-[90vh]"].concat(
|
return ["tw-flex", "tw-flex-col", "tw-w-screen"]
|
||||||
this.width,
|
.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();
|
||||||
}
|
}
|
||||||
|
|
||||||
get width() {
|
get width() {
|
||||||
switch (this.dialogSize) {
|
switch (this.dialogSize) {
|
||||||
case "small": {
|
case "small": {
|
||||||
return "tw-max-w-sm";
|
return "md:tw-max-w-sm";
|
||||||
}
|
}
|
||||||
case "large": {
|
case "large": {
|
||||||
return "tw-max-w-3xl";
|
return "md:tw-max-w-3xl";
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
return "tw-max-w-xl";
|
return "md:tw-max-w-xl";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ 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
|
Dialogs's should be used sparingly as they do call extra attention to themselves and can be
|
||||||
interruptive if overused.
|
interruptive if overused.
|
||||||
|
|
||||||
|
For non-blocking, supplementary content, open dialogs as a
|
||||||
|
[Drawer](?path=/story/component-library-dialogs-service--drawer) (requires `bit-layout`).
|
||||||
|
|
||||||
## Placement
|
## Placement
|
||||||
|
|
||||||
Dialogs should be centered vertically and horizontally on screen. Dialogs height should expand to
|
Dialogs should be centered vertically and horizontally on screen. Dialogs height should expand to
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export * from "./dialog.module";
|
export * from "./dialog.module";
|
||||||
export * from "./simple-dialog/types";
|
export * from "./simple-dialog/types";
|
||||||
export * from "./dialog.service";
|
export * from "./dialog.service";
|
||||||
export { DialogConfig, DIALOG_DATA, DialogRef } from "@angular/cdk/dialog";
|
export { DIALOG_DATA } from "@angular/cdk/dialog";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { CdkScrollable } from "@angular/cdk/scrolling";
|
import { CdkScrollable } from "@angular/cdk/scrolling";
|
||||||
import { ChangeDetectionStrategy, Component, Signal, inject } from "@angular/core";
|
import { ChangeDetectionStrategy, Component } from "@angular/core";
|
||||||
import { toSignal } from "@angular/core/rxjs-interop";
|
|
||||||
import { map } from "rxjs";
|
import { hasScrolledFrom } from "../utils/has-scrolled-from";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Body container for `bit-drawer`
|
* Body container for `bit-drawer`
|
||||||
@@ -14,7 +14,7 @@ import { map } from "rxjs";
|
|||||||
host: {
|
host: {
|
||||||
class:
|
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",
|
"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]": "isScrolled()",
|
"[class.tw-border-t-secondary-300]": "this.hasScrolledFrom().top",
|
||||||
},
|
},
|
||||||
hostDirectives: [
|
hostDirectives: [
|
||||||
{
|
{
|
||||||
@@ -24,13 +24,5 @@ import { map } from "rxjs";
|
|||||||
template: ` <ng-content></ng-content> `,
|
template: ` <ng-content></ng-content> `,
|
||||||
})
|
})
|
||||||
export class DrawerBodyComponent {
|
export class DrawerBodyComponent {
|
||||||
private scrollable = inject(CdkScrollable);
|
protected hasScrolledFrom = hasScrolledFrom();
|
||||||
|
|
||||||
/** 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 },
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
viewChild,
|
viewChild,
|
||||||
} from "@angular/core";
|
} from "@angular/core";
|
||||||
|
|
||||||
import { DrawerHostDirective } from "./drawer-host.directive";
|
import { DrawerService } from "./drawer.service";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A drawer is a panel of supplementary content that is adjacent to the page's main content.
|
* A drawer is a panel of supplementary content that is adjacent to the page's main content.
|
||||||
@@ -25,7 +25,7 @@ import { DrawerHostDirective } from "./drawer-host.directive";
|
|||||||
templateUrl: "drawer.component.html",
|
templateUrl: "drawer.component.html",
|
||||||
})
|
})
|
||||||
export class DrawerComponent {
|
export class DrawerComponent {
|
||||||
private drawerHost = inject(DrawerHostDirective);
|
private drawerHost = inject(DrawerService);
|
||||||
private portal = viewChild.required(CdkPortal);
|
private portal = viewChild.required(CdkPortal);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import { DrawerComponent } from "@bitwarden/components";
|
|||||||
|
|
||||||
# Drawer
|
# 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.
|
A drawer is a panel of supplementary content that is adjacent to the page's main content.
|
||||||
|
|
||||||
<Primary />
|
<Primary />
|
||||||
|
|||||||
20
libs/components/src/drawer/drawer.service.ts
Normal file
20
libs/components/src/drawer/drawer.service.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
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 +1,2 @@
|
|||||||
export * from "./layout.component";
|
export * from "./layout.component";
|
||||||
|
export * from "./scroll-layout.directive";
|
||||||
|
|||||||
@@ -1,43 +1,52 @@
|
|||||||
<div
|
@let mainContentId = "main-content";
|
||||||
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>
|
|
||||||
<div class="tw-flex tw-w-full">
|
<div class="tw-flex tw-w-full">
|
||||||
<ng-content select="bit-side-nav, [slot=side-nav]"></ng-content>
|
<div class="tw-flex tw-w-full" cdkTrapFocus>
|
||||||
<main
|
<div
|
||||||
[id]="mainContentId"
|
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"
|
||||||
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"
|
<nav class="tw-bg-background-alt3 tw-rounded-md tw-rounded-t-none tw-py-2 tw-text-alt2">
|
||||||
>
|
<a
|
||||||
<ng-content></ng-content>
|
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"
|
||||||
|
>
|
||||||
|
<ng-content></ng-content>
|
||||||
|
|
||||||
<!-- overlay backdrop for side-nav -->
|
<!-- overlay backdrop for side-nav -->
|
||||||
@if (
|
@if (
|
||||||
{
|
{
|
||||||
open: sideNavService.open$ | async,
|
open: sideNavService.open$ | async,
|
||||||
};
|
};
|
||||||
as data
|
as data
|
||||||
) {
|
) {
|
||||||
<div
|
<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"
|
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']"
|
[ngClass]="[data.open ? 'tw-bg-opacity-30 md:tw-bg-opacity-0' : 'tw-bg-opacity-0']"
|
||||||
>
|
>
|
||||||
@if (data.open) {
|
@if (data.open) {
|
||||||
<div (click)="sideNavService.toggle()" class="tw-pointer-events-auto tw-size-full"></div>
|
<div
|
||||||
}
|
(click)="sideNavService.toggle()"
|
||||||
</div>
|
class="tw-pointer-events-auto tw-size-full"
|
||||||
}
|
></div>
|
||||||
</main>
|
}
|
||||||
<ng-template [cdkPortalOutlet]="drawerPortal()"></ng-template>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
import { A11yModule, CdkTrapFocus } from "@angular/cdk/a11y";
|
||||||
import { PortalModule } from "@angular/cdk/portal";
|
import { PortalModule } from "@angular/cdk/portal";
|
||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component, inject } from "@angular/core";
|
import { Component, ElementRef, inject, viewChild } from "@angular/core";
|
||||||
import { RouterModule } from "@angular/router";
|
import { RouterModule } from "@angular/router";
|
||||||
|
|
||||||
import { DrawerHostDirective } from "../drawer/drawer-host.directive";
|
import { DrawerService } from "../drawer/drawer.service";
|
||||||
import { LinkModule } from "../link";
|
import { LinkModule } from "../link";
|
||||||
import { SideNavService } from "../navigation/side-nav.service";
|
import { SideNavService } from "../navigation/side-nav.service";
|
||||||
import { SharedModule } from "../shared";
|
import { SharedModule } from "../shared";
|
||||||
@@ -12,16 +13,23 @@ import { SharedModule } from "../shared";
|
|||||||
selector: "bit-layout",
|
selector: "bit-layout",
|
||||||
templateUrl: "layout.component.html",
|
templateUrl: "layout.component.html",
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, SharedModule, LinkModule, RouterModule, PortalModule],
|
imports: [
|
||||||
hostDirectives: [DrawerHostDirective],
|
CommonModule,
|
||||||
|
SharedModule,
|
||||||
|
LinkModule,
|
||||||
|
RouterModule,
|
||||||
|
PortalModule,
|
||||||
|
A11yModule,
|
||||||
|
CdkTrapFocus,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class LayoutComponent {
|
export class LayoutComponent {
|
||||||
protected mainContentId = "main-content";
|
|
||||||
|
|
||||||
protected sideNavService = inject(SideNavService);
|
protected sideNavService = inject(SideNavService);
|
||||||
protected drawerPortal = inject(DrawerHostDirective).portal;
|
protected drawerPortal = inject(DrawerService).portal;
|
||||||
|
|
||||||
focusMainContent() {
|
private mainContent = viewChild.required<ElementRef<HTMLElement>>("main");
|
||||||
document.getElementById(this.mainContentId)?.focus();
|
|
||||||
|
protected focusMainContent() {
|
||||||
|
this.mainContent().nativeElement.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
35
libs/components/src/layout/scroll-layout.directive.ts
Normal file
35
libs/components/src/layout/scroll-layout.directive.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,15 +3,23 @@ import { Component, OnInit } from "@angular/core";
|
|||||||
|
|
||||||
import { DialogModule, DialogService } from "../../../dialog";
|
import { DialogModule, DialogService } from "../../../dialog";
|
||||||
import { IconButtonModule } from "../../../icon-button";
|
import { IconButtonModule } from "../../../icon-button";
|
||||||
|
import { ScrollLayoutDirective } from "../../../layout";
|
||||||
import { SectionComponent } from "../../../section";
|
import { SectionComponent } from "../../../section";
|
||||||
import { TableDataSource, TableModule } from "../../../table";
|
import { TableDataSource, TableModule } from "../../../table";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "dialog-virtual-scroll-block",
|
selector: "dialog-virtual-scroll-block",
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [DialogModule, IconButtonModule, SectionComponent, TableModule, ScrollingModule],
|
imports: [
|
||||||
|
DialogModule,
|
||||||
|
IconButtonModule,
|
||||||
|
SectionComponent,
|
||||||
|
TableModule,
|
||||||
|
ScrollingModule,
|
||||||
|
ScrollLayoutDirective,
|
||||||
|
],
|
||||||
template: /*html*/ `<bit-section>
|
template: /*html*/ `<bit-section>
|
||||||
<cdk-virtual-scroll-viewport scrollWindow itemSize="47">
|
<cdk-virtual-scroll-viewport bitScrollLayout itemSize="63.5">
|
||||||
<bit-table [dataSource]="dataSource">
|
<bit-table [dataSource]="dataSource">
|
||||||
<ng-container header>
|
<ng-container header>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -12,8 +12,69 @@ import { KitchenSinkToggleList } from "./kitchen-sink-toggle-list.component";
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [KitchenSinkSharedModule],
|
imports: [KitchenSinkSharedModule],
|
||||||
template: `
|
template: `
|
||||||
<bit-dialog title="Dialog Title" dialogSize="large">
|
<bit-dialog title="Dialog Title" dialogSize="small">
|
||||||
<span bitDialogContent> Dialog body text goes here. </span>
|
<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>
|
||||||
<ng-container bitDialogFooter>
|
<ng-container bitDialogFooter>
|
||||||
<button type="button" bitButton buttonType="primary" (click)="dialogRef.close()">OK</button>
|
<button type="button" bitButton buttonType="primary" (click)="dialogRef.close()">OK</button>
|
||||||
<button type="button" bitButton buttonType="secondary" bitDialogClose>Cancel</button>
|
<button type="button" bitButton buttonType="secondary" bitDialogClose>Cancel</button>
|
||||||
@@ -90,72 +151,6 @@ class KitchenSinkDialog {
|
|||||||
</bit-section>
|
</bit-section>
|
||||||
</bit-tab>
|
</bit-tab>
|
||||||
</bit-tab-group>
|
</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 {
|
export class KitchenSinkMainComponent {
|
||||||
@@ -168,7 +163,7 @@ export class KitchenSinkMainComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
openDrawer() {
|
openDrawer() {
|
||||||
this.drawerOpen.set(true);
|
this.dialogService.openDrawer(KitchenSinkDialog);
|
||||||
}
|
}
|
||||||
|
|
||||||
navItems = [
|
navItems = [
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
|
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
|
||||||
import { DialogService } from "../../dialog";
|
|
||||||
import { LayoutComponent } from "../../layout";
|
import { LayoutComponent } from "../../layout";
|
||||||
import { I18nMockService } from "../../utils/i18n-mock.service";
|
import { I18nMockService } from "../../utils/i18n-mock.service";
|
||||||
import { positionFixedWrapperDecorator } from "../storybook-decorators";
|
import { positionFixedWrapperDecorator } from "../storybook-decorators";
|
||||||
@@ -39,8 +38,20 @@ export default {
|
|||||||
KitchenSinkTable,
|
KitchenSinkTable,
|
||||||
KitchenSinkToggleList,
|
KitchenSinkToggleList,
|
||||||
],
|
],
|
||||||
|
}),
|
||||||
|
applicationConfig({
|
||||||
providers: [
|
providers: [
|
||||||
DialogService,
|
provideNoopAnimations(),
|
||||||
|
importProvidersFrom(
|
||||||
|
RouterModule.forRoot(
|
||||||
|
[
|
||||||
|
{ path: "", redirectTo: "bitwarden", pathMatch: "full" },
|
||||||
|
{ path: "bitwarden", component: KitchenSinkMainComponent },
|
||||||
|
{ path: "virtual-scroll", component: DialogVirtualScrollBlockComponent },
|
||||||
|
],
|
||||||
|
{ useHash: true },
|
||||||
|
),
|
||||||
|
),
|
||||||
{
|
{
|
||||||
provide: I18nService,
|
provide: I18nService,
|
||||||
useFactory: () => {
|
useFactory: () => {
|
||||||
@@ -58,21 +69,6 @@ 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;
|
} as Meta;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<cdk-virtual-scroll-viewport
|
<cdk-virtual-scroll-viewport
|
||||||
scrollWindow
|
bitScrollLayout
|
||||||
[itemSize]="rowSize"
|
[itemSize]="rowSize"
|
||||||
[ngStyle]="{ paddingBottom: headerHeight + 'px' }"
|
[ngStyle]="{ paddingBottom: headerHeight + 'px' }"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import {
|
|||||||
TrackByFunction,
|
TrackByFunction,
|
||||||
} from "@angular/core";
|
} from "@angular/core";
|
||||||
|
|
||||||
|
import { ScrollLayoutDirective } from "../layout";
|
||||||
|
|
||||||
import { RowDirective } from "./row.directive";
|
import { RowDirective } from "./row.directive";
|
||||||
import { TableComponent } from "./table.component";
|
import { TableComponent } from "./table.component";
|
||||||
|
|
||||||
@@ -58,6 +60,7 @@ export class BitRowDef {
|
|||||||
CdkFixedSizeVirtualScroll,
|
CdkFixedSizeVirtualScroll,
|
||||||
CdkVirtualForOf,
|
CdkVirtualForOf,
|
||||||
RowDirective,
|
RowDirective,
|
||||||
|
ScrollLayoutDirective,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class TableScrollComponent
|
export class TableScrollComponent
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ dataSource.filter = (data) => data.orgType === "family";
|
|||||||
|
|
||||||
Rudimentary string filtering is supported out of the box with `TableDataSource.simpleStringFilter`.
|
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
|
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 convienence, you can also just
|
compared against the filter value using a simple `indexOf` check. For convenience, you can also just
|
||||||
pass a string directly.
|
pass a string directly.
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
@@ -153,7 +153,7 @@ dataSource.filter = "search value";
|
|||||||
|
|
||||||
### Virtual Scrolling
|
### Virtual Scrolling
|
||||||
|
|
||||||
It's heavily adviced to use virtual scrolling if you expect the table to have any significant amount
|
It's heavily advised 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`
|
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
|
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
|
using the `*ngFor` directive to render the rows, you provide a `bitRowDef` template that will be
|
||||||
@@ -178,6 +178,14 @@ height and align vertically.
|
|||||||
</bit-table-scroll>
|
</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
|
## Accessibility
|
||||||
|
|
||||||
- Always include a row or column header with your table; this allows assistive technology to better
|
- Always include a row or column header with your table; this allows assistive technology to better
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
|
import { RouterTestingModule } from "@angular/router/testing";
|
||||||
import { Meta, moduleMetadata, StoryObj } from "@storybook/angular";
|
import { Meta, moduleMetadata, StoryObj } from "@storybook/angular";
|
||||||
|
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
|
||||||
import { countries } from "../form/countries";
|
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 { TableDataSource } from "./table-data-source";
|
||||||
import { TableModule } from "./table.module";
|
import { TableModule } from "./table.module";
|
||||||
@@ -8,8 +15,17 @@ import { TableModule } from "./table.module";
|
|||||||
export default {
|
export default {
|
||||||
title: "Component Library/Table",
|
title: "Component Library/Table",
|
||||||
decorators: [
|
decorators: [
|
||||||
|
positionFixedWrapperDecorator(),
|
||||||
moduleMetadata({
|
moduleMetadata({
|
||||||
imports: [TableModule],
|
imports: [TableModule, LayoutComponent, RouterTestingModule],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: I18nService,
|
||||||
|
useFactory: () => {
|
||||||
|
return new I18nMockService(mockLayoutI18n);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
argTypes: {
|
argTypes: {
|
||||||
@@ -116,18 +132,20 @@ export const Scrollable: Story = {
|
|||||||
trackBy: (index: number, item: any) => item.id,
|
trackBy: (index: number, item: any) => item.id,
|
||||||
},
|
},
|
||||||
template: `
|
template: `
|
||||||
<bit-table-scroll [dataSource]="dataSource" [rowSize]="43">
|
<bit-layout>
|
||||||
<ng-container header>
|
<bit-table-scroll [dataSource]="dataSource" [rowSize]="43">
|
||||||
<th bitCell bitSortable="id" default>Id</th>
|
<ng-container header>
|
||||||
<th bitCell bitSortable="name">Name</th>
|
<th bitCell bitSortable="id" default>Id</th>
|
||||||
<th bitCell bitSortable="other" [fn]="sortFn">Other</th>
|
<th bitCell bitSortable="name">Name</th>
|
||||||
</ng-container>
|
<th bitCell bitSortable="other" [fn]="sortFn">Other</th>
|
||||||
<ng-template bitRowDef let-row>
|
</ng-container>
|
||||||
<td bitCell>{{ row.id }}</td>
|
<ng-template bitRowDef let-row>
|
||||||
<td bitCell>{{ row.name }}</td>
|
<td bitCell>{{ row.id }}</td>
|
||||||
<td bitCell>{{ row.other }}</td>
|
<td bitCell>{{ row.name }}</td>
|
||||||
</ng-template>
|
<td bitCell>{{ row.other }}</td>
|
||||||
</bit-table-scroll>
|
</ng-template>
|
||||||
|
</bit-table-scroll>
|
||||||
|
</bit-layout>
|
||||||
`,
|
`,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
@@ -144,17 +162,19 @@ export const Filterable: Story = {
|
|||||||
sortFn: (a: any, b: any) => a.id - b.id,
|
sortFn: (a: any, b: any) => a.id - b.id,
|
||||||
},
|
},
|
||||||
template: `
|
template: `
|
||||||
<input type="search" placeholder="Search" (input)="dataSource.filter = $event.target.value" />
|
<bit-layout>
|
||||||
<bit-table-scroll [dataSource]="dataSource" [rowSize]="43">
|
<input type="search" placeholder="Search" (input)="dataSource.filter = $event.target.value" />
|
||||||
<ng-container header>
|
<bit-table-scroll [dataSource]="dataSource" [rowSize]="43">
|
||||||
<th bitCell bitSortable="name" default>Name</th>
|
<ng-container header>
|
||||||
<th bitCell bitSortable="value" width="120px">Value</th>
|
<th bitCell bitSortable="name" default>Name</th>
|
||||||
</ng-container>
|
<th bitCell bitSortable="value" width="120px">Value</th>
|
||||||
<ng-template bitRowDef let-row>
|
</ng-container>
|
||||||
<td bitCell>{{ row.name }}</td>
|
<ng-template bitRowDef let-row>
|
||||||
<td bitCell>{{ row.value }}</td>
|
<td bitCell>{{ row.name }}</td>
|
||||||
</ng-template>
|
<td bitCell>{{ row.value }}</td>
|
||||||
</bit-table-scroll>
|
</ng-template>
|
||||||
|
</bit-table-scroll>
|
||||||
|
</bit-layout>
|
||||||
`,
|
`,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|||||||
41
libs/components/src/utils/has-scrolled-from.ts
Normal file
41
libs/components/src/utils/has-scrolled-from.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user