From 5906cdeb77c35e92cfaba46401fcea5b9b418fda Mon Sep 17 00:00:00 2001
From: Kalle <38327916+Sendouc@users.noreply.github.com>
Date: Sun, 20 Sep 2026 12:07:43 +0300
Subject: [PATCH] Dialog scroll lock
---
.../elements/Dialog.browser.test.tsx | 48 +++++++++++
app/components/elements/Dialog.tsx | 85 ++++++++++++++++++-
changelog/2026-09-20-dialog-scroll-lock.md | 4 +
e2e/pages/sendouq/sendouq-match-page.ts | 2 +-
4 files changed, 137 insertions(+), 2 deletions(-)
create mode 100644 changelog/2026-09-20-dialog-scroll-lock.md
diff --git a/app/components/elements/Dialog.browser.test.tsx b/app/components/elements/Dialog.browser.test.tsx
index 1d073b907..10b0d737c 100644
--- a/app/components/elements/Dialog.browser.test.tsx
+++ b/app/components/elements/Dialog.browser.test.tsx
@@ -120,6 +120,54 @@ describe("SendouDialog", () => {
expect(openDialog().open).toBe(true);
});
+ test("locks page scrolling while open without changing the page width", async () => {
+ const tall = document.createElement("div");
+ tall.style.height = "300vh";
+ document.body.appendChild(tall);
+ cleanupFns.push(() => tall.remove());
+ const root = document.documentElement;
+ const widthBefore = root.clientWidth;
+
+ const screen = await render(
+ withRouter(
+ Open}
+ showCloseButton
+ >
+ Content
+ ,
+ ),
+ );
+
+ await screen.getByRole("button", { name: "Open" }).click();
+ await expect.element(screen.getByText("Content")).toBeVisible();
+ await vi.waitFor(() => expect(root.style.overflow).toBe("hidden"));
+ expect(root.clientWidth).toBe(widthBefore);
+
+ await screen.getByRole("button", { name: "Close" }).click();
+ await vi.waitFor(() => expect(root.style.overflow).toBe(""));
+ expect(root.style.scrollbarGutter).toBe("");
+ expect(root.clientWidth).toBe(widthBefore);
+ });
+
+ test("releases the scroll lock when an open dialog unmounts", async () => {
+ const screen = await render(
+ withRouter(
+ {}}>
+ Content
+ ,
+ ),
+ );
+ await expect.element(screen.getByText("Content")).toBeVisible();
+ await vi.waitFor(() =>
+ expect(document.documentElement.style.overflow).toBe("hidden"),
+ );
+
+ await screen.unmount();
+ expect(document.documentElement.style.overflow).toBe("");
+ });
+
test("focuses the dialog itself instead of the close button on open", async () => {
await render(
withRouter(
diff --git a/app/components/elements/Dialog.tsx b/app/components/elements/Dialog.tsx
index 5f6df54a7..3bbdd53c3 100644
--- a/app/components/elements/Dialog.tsx
+++ b/app/components/elements/Dialog.tsx
@@ -67,9 +67,19 @@ function DialogElement({
children,
ref,
}: DialogElementProps) {
+ const dialogRef = React.useRef(null);
+ useScrollLockWhileOpen(dialogRef);
+
return (