Merge pull request #4374 from brantsrasmus/fix/session-modal-dismiss-outside-click

fix: dismiss session modals when clicking the side margins
This commit is contained in:
Mike Cao authored and GitHub committed 2026-07-23 14:59:38 -07:00
commit c573a2bdb3
4 files changed
+98 -4

No files matched your search

@@ -0,0 +1,3 @@
.modal {
width: min(calc(100dvw - 48px), 1320px);
}
@@ -2,12 +2,13 @@
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;
}
export function SessionModal({ websiteId, ...props }: SessionModalProps) {
export function SessionModal({ websiteId, className, ...props }: SessionModalProps) {
const {
router,
pathname,
@@ -25,12 +26,13 @@ export function SessionModal({ websiteId, ...props }: SessionModalProps) {
<Modal
placement="bottom"
offset="80px"
className={[styles.modal, className].filter(Boolean).join(' ')}
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} />
+81
View File
@@ -0,0 +1,81 @@
import { type APIRequestContext, expect, type Page, 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).
const VIEWPORT = { width: 1600, height: 900 };
test.use({ viewport: VIEWPORT });
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);
}
async function getSheetBox(page: Page) {
const box = await page.getByRole('dialog').boundingBox();
expect(box).not.toBeNull();
if (!box) throw new Error('dialog has no bounding box');
return box;
}
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 getSheetBox(page);
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 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
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 getSheetBox(page);
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=/);
});
});