fix: dismiss session modals when clicking the side margins

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.
This commit is contained in:
brantsrasmus committed 2026-07-03 11:47:20 +02:00
1 parent 735d85c7af
commit 8dcd2c6190
4 files changed
+88 -3

No files matched your search

@@ -0,0 +1,3 @@
.modal {
width: min(calc(100dvw - 48px), 1320px);
}
@@ -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) {
<Modal
placement="bottom"
offset="80px"
className={styles.modal}
isOpen={!!session}
onOpenChange={handleOpenChange}
isDismissable
{...props}
>
<Column height="100%" maxWidth="1320px" style={{ margin: '0 auto' }}>
<Column height="100%">
<Dialog variant="sheet" className="rounded-lg">
{({ close }) => (
<Column padding="10">
@@ -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 (
<Modal placement="bottom" offset="80px" isOpen onOpenChange={handleOpenChange} isDismissable>
<Column height="100%" maxWidth="1320px" style={{ margin: '0 auto' }}>
<Modal
placement="bottom"
offset="80px"
className={styles.modal}
isOpen
onOpenChange={handleOpenChange}
isDismissable
>
<Column height="100%">
<Dialog variant="sheet" className="rounded-lg">
<Column padding="10">
<SessionProfile websiteId={websiteId} sessionId={sessionId} onClose={closeModal} />
+72
View File
@@ -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=/);
});
});