From 8dcd2c6190137d26ac9835619510aee73695dac0 Mon Sep 17 00:00:00 2001 From: brantsrasmus Date: Fri, 3 Jul 2026 11:47:20 +0200 Subject: [PATCH] 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=/); + }); +});