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:
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} />
|
||||
|
||||
@@ -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=/);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user