[CL-312] fix dialog scroll blocking + virtual scroll (#9606)
This commit is contained in:
parent
c73ee88126
commit
fdeac58469
|
@ -5,7 +5,7 @@ import {
|
||||||
DialogRef,
|
DialogRef,
|
||||||
DIALOG_SCROLL_STRATEGY,
|
DIALOG_SCROLL_STRATEGY,
|
||||||
} from "@angular/cdk/dialog";
|
} from "@angular/cdk/dialog";
|
||||||
import { ComponentType, Overlay, OverlayContainer } from "@angular/cdk/overlay";
|
import { ComponentType, Overlay, OverlayContainer, ScrollStrategy } from "@angular/cdk/overlay";
|
||||||
import {
|
import {
|
||||||
Inject,
|
Inject,
|
||||||
Injectable,
|
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 { SimpleConfigurableDialogComponent } from "./simple-dialog/simple-configurable-dialog/simple-configurable-dialog.component";
|
||||||
import { SimpleDialogOptions, Translation } from "./simple-dialog/types";
|
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()
|
@Injectable()
|
||||||
export class DialogService extends Dialog implements OnDestroy {
|
export class DialogService extends Dialog implements OnDestroy {
|
||||||
private _destroy$ = new Subject<void>();
|
private _destroy$ = new Subject<void>();
|
||||||
|
|
||||||
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();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
/** Parent class constructor */
|
/** Parent class constructor */
|
||||||
_overlay: Overlay,
|
_overlay: Overlay,
|
||||||
|
@ -73,6 +96,7 @@ export class DialogService extends Dialog implements OnDestroy {
|
||||||
): DialogRef<R, C> {
|
): DialogRef<R, C> {
|
||||||
config = {
|
config = {
|
||||||
backdropClass: this.backDropClasses,
|
backdropClass: this.backDropClasses,
|
||||||
|
scrollStrategy: this.defaultScrollStrategy,
|
||||||
...config,
|
...config,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,7 +8,15 @@ import {
|
||||||
componentWrapperDecorator,
|
componentWrapperDecorator,
|
||||||
moduleMetadata,
|
moduleMetadata,
|
||||||
} from "@storybook/angular";
|
} 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";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
|
||||||
|
@ -16,6 +24,7 @@ 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 { DialogVirtualScrollBlockComponent } from "./components/dialog-virtual-scroll-block.component";
|
||||||
import { KitchenSinkForm } from "./components/kitchen-sink-form.component";
|
import { KitchenSinkForm } from "./components/kitchen-sink-form.component";
|
||||||
import { KitchenSinkMainComponent } from "./components/kitchen-sink-main.component";
|
import { KitchenSinkMainComponent } from "./components/kitchen-sink-main.component";
|
||||||
import { KitchenSinkTable } from "./components/kitchen-sink-table.component";
|
import { KitchenSinkTable } from "./components/kitchen-sink-table.component";
|
||||||
|
@ -64,7 +73,9 @@ export default {
|
||||||
skipToContent: "Skip to content",
|
skipToContent: "Skip to content",
|
||||||
submenu: "submenu",
|
submenu: "submenu",
|
||||||
toggleCollapse: "toggle collapse",
|
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: "", redirectTo: "bitwarden", pathMatch: "full" },
|
||||||
{ path: "bitwarden", component: KitchenSinkMainComponent },
|
{ path: "bitwarden", component: KitchenSinkMainComponent },
|
||||||
|
{ path: "virtual-scroll", component: DialogVirtualScrollBlockComponent },
|
||||||
],
|
],
|
||||||
{ useHash: true },
|
{ useHash: true },
|
||||||
),
|
),
|
||||||
|
@ -100,6 +112,7 @@ export const Default: Story = {
|
||||||
<bit-nav-item text="Bitwarden" route="bitwarden"></bit-nav-item>
|
<bit-nav-item text="Bitwarden" route="bitwarden"></bit-nav-item>
|
||||||
<bit-nav-divider></bit-nav-divider>
|
<bit-nav-divider></bit-nav-divider>
|
||||||
</bit-nav-group>
|
</bit-nav-group>
|
||||||
|
<bit-nav-item text="Virtual Scroll" route="virtual-scroll"></bit-nav-item>
|
||||||
</bit-nav-group>
|
</bit-nav-group>
|
||||||
</bit-side-nav>
|
</bit-side-nav>
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
|
@ -165,3 +178,19 @@ export const EmptyTab: Story = {
|
||||||
await userEvent.click(emptyTab);
|
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);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in New Issue