[CL-312] fix dialog scroll blocking + virtual scroll (#9606)

This commit is contained in:
Will Martin 2024-09-04 12:12:47 -04:00 committed by GitHub
parent c73ee88126
commit fdeac58469
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 117 additions and 3 deletions

View File

@ -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<void>();
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<R, C> {
config = {
backdropClass: this.backDropClasses,
scrollStrategy: this.defaultScrollStrategy,
...config,
};

View File

@ -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: ` <bit-section>
<cdk-virtual-scroll-viewport scrollWindow itemSize="47">
<bit-table [dataSource]="dataSource">
<ng-container header>
<tr>
<th bitCell bitSortable="id" default>Id</th>
<th bitCell bitSortable="name">Name</th>
<th bitCell>Options</th>
</tr>
</ng-container>
<ng-template body let-rows$>
<tr bitRow *cdkVirtualFor="let r of rows$">
<td bitCell>{{ r.id }}</td>
<td bitCell>{{ r.name }}</td>
<td bitCell>
<button
bitIconButton="bwi-ellipsis-v"
type="button"
aria-label="Options"
(click)="openDefaultDialog()"
></button>
</td>
</tr>
</ng-template>
</bit-table>
</cdk-virtual-scroll-viewport>
</bit-section>`,
})
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",
});
}
}

View File

@ -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 = {
<bit-nav-item text="Bitwarden" route="bitwarden"></bit-nav-item>
<bit-nav-divider></bit-nav-divider>
</bit-nav-group>
<bit-nav-item text="Virtual Scroll" route="virtual-scroll"></bit-nav-item>
</bit-nav-group>
</bit-side-nav>
<router-outlet></router-outlet>
@ -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);
},
};