From 8dcd2c6190137d26ac9835619510aee73695dac0 Mon Sep 17 00:00:00 2001 From: brantsrasmus Date: Fri, 3 Jul 2026 11:47:20 +0200 Subject: [PATCH 1/2] fix: dismiss session modals when clicking the side margins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SessionModal and SessionProfileModal set their 1320px max width on an inner Column rather than on the Modal itself. The react-zen Modal box (placement="bottom") therefore stretched to the full viewport width (inset-x-0), so its transparent left/right margins sat on top of the underlay and swallowed outside-clicks. Only the thin offset strip above the sheet was dismissable. Move the width onto the Modal box via a CSS module — matching the existing pattern in ReplayModal — so the click-catcher is the centered ~1320px sheet and the side margins become real overlay again. Adds an e2e regression test covering left/right margin dismissal with an in-sheet control click. --- .../sessions/SessionModal.module.css | 3 + .../[websiteId]/sessions/SessionModal.tsx | 4 +- .../sessions/SessionProfileModal.tsx | 12 +++- tests/e2e/session-modal-dismiss.spec.ts | 72 +++++++++++++++++++ 4 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 src/app/(main)/websites/[websiteId]/sessions/SessionModal.module.css create mode 100644 tests/e2e/session-modal-dismiss.spec.ts diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionModal.module.css b/src/app/(main)/websites/[websiteId]/sessions/SessionModal.module.css new file mode 100644 index 000000000..64d64e6e4 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionModal.module.css @@ -0,0 +1,3 @@ +.modal { + width: min(calc(100dvw - 48px), 1320px); +} diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionModal.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionModal.tsx index d0552f58c..fd63247f0 100644 --- a/src/app/(main)/websites/[websiteId]/sessions/SessionModal.tsx +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionModal.tsx @@ -2,6 +2,7 @@ import { Column, Dialog, Modal, type ModalProps } from '@umami/react-zen'; import { SessionProfile } from '@/app/(main)/websites/[websiteId]/sessions/SessionProfile'; import { useNavigation } from '@/components/hooks'; +import styles from './SessionModal.module.css'; export interface SessionModalProps extends ModalProps { websiteId: string; @@ -25,12 +26,13 @@ export function SessionModal({ websiteId, ...props }: SessionModalProps) { - + {({ close }) => ( diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionProfileModal.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionProfileModal.tsx index 7bcdf0802..fcd18fecd 100644 --- a/src/app/(main)/websites/[websiteId]/sessions/SessionProfileModal.tsx +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionProfileModal.tsx @@ -3,6 +3,7 @@ import { Column, Dialog, Modal } from '@umami/react-zen'; import { useRouter, useSearchParams } from 'next/navigation'; import { buildPath } from '@/lib/url'; import { SessionProfile } from './SessionProfile'; +import styles from './SessionModal.module.css'; export function SessionProfileModal({ websiteId, @@ -28,8 +29,15 @@ export function SessionProfileModal({ }; return ( - - + + diff --git a/tests/e2e/session-modal-dismiss.spec.ts b/tests/e2e/session-modal-dismiss.spec.ts new file mode 100644 index 000000000..ebb7dca16 --- /dev/null +++ b/tests/e2e/session-modal-dismiss.spec.ts @@ -0,0 +1,72 @@ +import { type APIRequestContext, expect, test } from '@playwright/test'; +import { uuid } from '../../src/lib/crypto'; +import { type Auth, authHeaders, deleteWebsite, loginPage, umamiUser } from './helpers'; + +// The session modal is a bottom sheet capped at 1320px and centered, so on a +// wide viewport there are dark side margins. Clicking those margins must +// dismiss the modal (regression test for the full-width click-catcher bug). +test.use({ viewport: { width: 1600, height: 900 } }); + +async function createWebsite(request: APIRequestContext, auth: Auth, websiteId: string) { + const response = await request.post('/api/websites', { + headers: authHeaders(auth), + data: { + id: websiteId, + createdBy: umamiUser.id, + name: 'Modal dismiss test', + domain: 'modal-dismiss-test.com', + }, + }); + expect(response.status()).toBe(200); +} + +test.describe('SessionModal outside-click dismissal', () => { + let auth: Auth; + const websiteId = uuid(); + + test.beforeEach(async ({ page, request }) => { + auth = await loginPage(page, request); + await createWebsite(request, auth, websiteId); + // The modal opens purely from the `session` query param, so a random id is + // enough to render it — no seeded session data required. + await page.goto(`/websites/${websiteId}/sessions?session=${uuid()}`); + await expect(page.getByRole('dialog')).toBeVisible(); + }); + + test.afterEach(async ({ request }) => { + await deleteWebsite(request, auth, websiteId); + }); + + test('clicking in the left side margin dismisses the modal', async ({ page }) => { + const box = await page.getByRole('dialog').boundingBox(); + const marginX = 20; + const midY = box.y + box.height / 2; + expect(marginX).toBeLessThan(box.x); // genuinely outside the sheet + + await page.mouse.click(marginX, midY); + + await expect(page.getByRole('dialog')).toBeHidden(); + await expect(page).not.toHaveURL(/session=/); + }); + + test('clicking in the right side margin dismisses the modal', async ({ page }) => { + const box = await page.getByRole('dialog').boundingBox(); + const marginX = page.viewportSize().width - 20; + const midY = box.y + box.height / 2; + expect(marginX).toBeGreaterThan(box.x + box.width); // genuinely outside the sheet + + await page.mouse.click(marginX, midY); + + await expect(page.getByRole('dialog')).toBeHidden(); + await expect(page).not.toHaveURL(/session=/); + }); + + test('control: clicking inside the sheet keeps the modal open', async ({ page }) => { + const box = await page.getByRole('dialog').boundingBox(); + + await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2); + + await expect(page.getByRole('dialog')).toBeVisible(); + await expect(page).toHaveURL(/session=/); + }); +}); From 3c467e54d57607b4af1f474b28d9d6814cd6e188 Mon Sep 17 00:00:00 2001 From: brantsrasmus Date: Fri, 3 Jul 2026 11:56:13 +0200 Subject: [PATCH 2/2] address review feedback - SessionModal: merge caller-provided className with styles.modal instead of letting it be clobbered by the {...props} spread (matches ReplayModal). - e2e test: narrow boundingBox()/viewport nullability without non-null assertions (keeps biome's noNonNullAssertion happy) via a getSheetBox helper and a shared VIEWPORT constant. --- .../[websiteId]/sessions/SessionModal.tsx | 4 ++-- tests/e2e/session-modal-dismiss.spec.ts | 21 +++++++++++++------ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionModal.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionModal.tsx index fd63247f0..1723763ad 100644 --- a/src/app/(main)/websites/[websiteId]/sessions/SessionModal.tsx +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionModal.tsx @@ -8,7 +8,7 @@ export interface SessionModalProps extends ModalProps { websiteId: string; } -export function SessionModal({ websiteId, ...props }: SessionModalProps) { +export function SessionModal({ websiteId, className, ...props }: SessionModalProps) { const { router, pathname, @@ -26,7 +26,7 @@ export function SessionModal({ websiteId, ...props }: SessionModalProps) { { let auth: Auth; const websiteId = uuid(); @@ -38,7 +47,7 @@ test.describe('SessionModal outside-click dismissal', () => { }); test('clicking in the left side margin dismisses the modal', async ({ page }) => { - const box = await page.getByRole('dialog').boundingBox(); + const box = await getSheetBox(page); const marginX = 20; const midY = box.y + box.height / 2; expect(marginX).toBeLessThan(box.x); // genuinely outside the sheet @@ -50,8 +59,8 @@ test.describe('SessionModal outside-click dismissal', () => { }); test('clicking in the right side margin dismisses the modal', async ({ page }) => { - const box = await page.getByRole('dialog').boundingBox(); - const marginX = page.viewportSize().width - 20; + const box = await getSheetBox(page); + const marginX = VIEWPORT.width - 20; const midY = box.y + box.height / 2; expect(marginX).toBeGreaterThan(box.x + box.width); // genuinely outside the sheet @@ -62,7 +71,7 @@ test.describe('SessionModal outside-click dismissal', () => { }); test('control: clicking inside the sheet keeps the modal open', async ({ page }) => { - const box = await page.getByRole('dialog').boundingBox(); + const box = await getSheetBox(page); await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2);