From fdeac584697f40fd6fb68bf1175dbe8fd83179fc Mon Sep 17 00:00:00 2001 From: Will Martin Date: Wed, 4 Sep 2024 12:12:47 -0400 Subject: [PATCH] [CL-312] fix dialog scroll blocking + virtual scroll (#9606) --- libs/components/src/dialog/dialog.service.ts | 26 +++++++- .../dialog-virtual-scroll-block.component.ts | 61 +++++++++++++++++++ .../kitchen-sink/kitchen-sink.stories.ts | 33 +++++++++- 3 files changed, 117 insertions(+), 3 deletions(-) create mode 100644 libs/components/src/stories/kitchen-sink/components/dialog-virtual-scroll-block.component.ts diff --git a/libs/components/src/dialog/dialog.service.ts b/libs/components/src/dialog/dialog.service.ts index 9488da4ac6..62a56d20af 100644 --- a/libs/components/src/dialog/dialog.service.ts +++ b/libs/components/src/dialog/dialog.service.ts @@ -5,7 +5,7 @@ import { DialogRef, DIALOG_SCROLL_STRATEGY, } from "@angular/cdk/dialog"; -import { ComponentType, Overlay, OverlayContainer } from "@angular/cdk/overlay"; +import { ComponentType, Overlay, OverlayContainer, ScrollStrategy } from "@angular/cdk/overlay"; import { Inject, Injectable, @@ -25,12 +25,35 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { SimpleConfigurableDialogComponent } from "./simple-dialog/simple-configurable-dialog/simple-configurable-dialog.component"; import { SimpleDialogOptions, Translation } from "./simple-dialog/types"; +/** + * The default `BlockScrollStrategy` does not work well with virtual scrolling. + * + * https://github.com/angular/components/issues/7390 + */ +class CustomBlockScrollStrategy implements ScrollStrategy { + enable() { + document.body.classList.add("tw-overflow-hidden"); + } + + disable() { + document.body.classList.remove("tw-overflow-hidden"); + } + + /** Noop */ + attach() {} + + /** Noop */ + detach() {} +} + @Injectable() export class DialogService extends Dialog implements OnDestroy { private _destroy$ = new Subject(); private backDropClasses = ["tw-fixed", "tw-bg-black", "tw-bg-opacity-30", "tw-inset-0"]; + private defaultScrollStrategy = new CustomBlockScrollStrategy(); + constructor( /** Parent class constructor */ _overlay: Overlay, @@ -73,6 +96,7 @@ export class DialogService extends Dialog implements OnDestroy { ): DialogRef { config = { backdropClass: this.backDropClasses, + scrollStrategy: this.defaultScrollStrategy, ...config, }; diff --git a/libs/components/src/stories/kitchen-sink/components/dialog-virtual-scroll-block.component.ts b/libs/components/src/stories/kitchen-sink/components/dialog-virtual-scroll-block.component.ts new file mode 100644 index 0000000000..a867d9cdf5 --- /dev/null +++ b/libs/components/src/stories/kitchen-sink/components/dialog-virtual-scroll-block.component.ts @@ -0,0 +1,61 @@ +import { ScrollingModule } from "@angular/cdk/scrolling"; +import { Component, OnInit } from "@angular/core"; + +import { DialogModule, DialogService } from "../../../dialog"; +import { IconButtonModule } from "../../../icon-button"; +import { SectionComponent } from "../../../section"; +import { TableDataSource, TableModule } from "../../../table"; + +@Component({ + selector: "dialog-virtual-scroll-block", + standalone: true, + imports: [DialogModule, IconButtonModule, SectionComponent, TableModule, ScrollingModule], + template: ` + + + + + Id + Name + Options + + + + + {{ r.id }} + {{ r.name }} + + + + + + + + `, +}) +export class DialogVirtualScrollBlockComponent implements OnInit { + constructor(public dialogService: DialogService) {} + + protected dataSource = new TableDataSource<{ id: number; name: string; other: string }>(); + + ngOnInit(): void { + this.dataSource.data = [...Array(100).keys()].map((i) => ({ + id: i, + name: `name-${i}`, + other: `other-${i}`, + })); + } + + async openDefaultDialog() { + await this.dialogService.openSimpleDialog({ + type: "info", + title: "Foo", + content: "Bar", + }); + } +} diff --git a/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts b/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts index fa78f04d23..203c510f81 100644 --- a/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts +++ b/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts @@ -8,7 +8,15 @@ import { componentWrapperDecorator, moduleMetadata, } from "@storybook/angular"; -import { userEvent, getAllByRole, getByRole, getByLabelText, fireEvent } from "@storybook/test"; +import { + userEvent, + getAllByRole, + getByRole, + getByLabelText, + fireEvent, + getByText, + getAllByLabelText, +} from "@storybook/test"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -16,6 +24,7 @@ import { DialogService } from "../../dialog"; import { LayoutComponent } from "../../layout"; import { I18nMockService } from "../../utils/i18n-mock.service"; +import { DialogVirtualScrollBlockComponent } from "./components/dialog-virtual-scroll-block.component"; import { KitchenSinkForm } from "./components/kitchen-sink-form.component"; import { KitchenSinkMainComponent } from "./components/kitchen-sink-main.component"; import { KitchenSinkTable } from "./components/kitchen-sink-table.component"; @@ -64,7 +73,9 @@ export default { skipToContent: "Skip to content", submenu: "submenu", toggleCollapse: "toggle collapse", - toggleSideNavigation: "toggle side navigation", + toggleSideNavigation: "Toggle side navigation", + yes: "Yes", + no: "No", }); }, }, @@ -78,6 +89,7 @@ export default { [ { path: "", redirectTo: "bitwarden", pathMatch: "full" }, { path: "bitwarden", component: KitchenSinkMainComponent }, + { path: "virtual-scroll", component: DialogVirtualScrollBlockComponent }, ], { useHash: true }, ), @@ -100,6 +112,7 @@ export const Default: Story = { + @@ -165,3 +178,19 @@ export const EmptyTab: Story = { await userEvent.click(emptyTab); }, }; + +export const VirtualScrollBlockingDialog: Story = { + ...Default, + play: async (context) => { + const canvas = context.canvasElement; + const navItem = getByText(canvas, "Virtual Scroll"); + await userEvent.click(navItem); + + const htmlEl = canvas.ownerDocument.documentElement; + htmlEl.scrollTop = 2000; + + const dialogButton = getAllByLabelText(canvas, "Options")[0]; + + await userEvent.click(dialogButton); + }, +};