From aa2b21f99e4c37d468bc41c37ef914d9276cc104 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 7 Jul 2026 17:04:57 -0700 Subject: [PATCH] fix(chat): closed drawer sidenav must not intercept clicks on content MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In drawer mode the chat-sidenav host is a fixed, drawer-width, full-height, z-index:1001 box that stays put whether open or closed — only the inner .chat-sidenav PANEL slides off-screen (translateX(-100%)) when closed. The transparent closed host had no pointer-events:none, so it silently swallowed clicks over the left strip of the content beneath it. App mode forces drawer mode on desktop, so it covered the itinerary overlay + welcome suggestions ('Plan a long weekend in Paris' was unclickable). Fix: the drawer host is pointer-events:none (inert); the .chat-sidenav panel re-enables pointer-events:auto (harmless off-screen when closed, interactive when open). Adds an examples/chat e2e guard: in App mode a point over the closed drawer box resolves to the content beneath, not the sidenav. Co-Authored-By: Claude Opus 4.8 --- .../angular/e2e/app-mode-itinerary.spec.ts | 31 +++++++++++++++++++ .../src/lib/styles/chat-sidenav.styles.ts | 12 +++++++ 2 files changed, 43 insertions(+) diff --git a/examples/chat/angular/e2e/app-mode-itinerary.spec.ts b/examples/chat/angular/e2e/app-mode-itinerary.spec.ts index c6c5c3660..76db1bc17 100644 --- a/examples/chat/angular/e2e/app-mode-itinerary.spec.ts +++ b/examples/chat/angular/e2e/app-mode-itinerary.spec.ts @@ -23,6 +23,37 @@ test.describe('App mode — itinerary cockpit', () => { await expect(page.locator('.demo-shell__hamburger')).toBeVisible(); }); + test('the closed thread drawer does not intercept clicks on the cockpit content', async ({ page }) => { + // Regression: in App mode the thread sidenav is forced to drawer mode on + // desktop. Its host stays a fixed, drawer-width, full-height, z-index:1001 + // box even when closed (only the inner panel slides off-screen), so without + // pointer-events:none it swallowed clicks on the itinerary overlay + welcome + // suggestions beneath its left strip. + await openDemo(page, '/sidebar?appmode=on'); + await expect(page.locator('.demo-shell__hamburger')).toBeVisible(); // drawer closed + + const probe = await page.evaluate(() => { + const host = document.querySelector('chat-sidenav') as HTMLElement | null; + if (!host) return { ok: false as const }; + const r = host.getBoundingClientRect(); + const x = r.left + r.width / 2; + const y = r.top + Math.min(r.height / 2, 180); + const hit = document.elementFromPoint(x, y) as HTMLElement | null; + return { + ok: true as const, + pointerEvents: getComputedStyle(host).pointerEvents, + // Does a click over the (transparent) closed-drawer box land on the + // sidenav, or pass through to the content beneath it? + hitInsideSidenav: !!hit && !!hit.closest('chat-sidenav'), + }; + }); + + expect(probe.ok).toBe(true); + if (!probe.ok) return; + expect(probe.pointerEvents).toBe('none'); // the fix: closed host is inert + expect(probe.hitInsideSidenav).toBe(false); // clicks reach the content + }); + test('selecting Embed turns App mode off (coercion)', async ({ page }) => { await openDemo(page, '/sidebar?appmode=on'); await expect(page.locator('app-map-canvas')).toBeAttached(); diff --git a/libs/chat/src/lib/styles/chat-sidenav.styles.ts b/libs/chat/src/lib/styles/chat-sidenav.styles.ts index e5376bfde..cc8c63bb1 100644 --- a/libs/chat/src/lib/styles/chat-sidenav.styles.ts +++ b/libs/chat/src/lib/styles/chat-sidenav.styles.ts @@ -39,6 +39,14 @@ export const CHAT_SIDENAV_STYLES = ` bottom: 0; width: var(--tplane-chat-sidenav-width-drawer); z-index: var(--tplane-chat-z-drawer, 1001); + /* The host is a fixed, drawer-width, full-height box at z-index 1001 that + * stays put whether or not the drawer is open — only the inner .chat-sidenav + * PANEL slides off-screen (translateX(-100%)) when closed. Without this, the + * transparent closed host still intercepts clicks over the left strip of the + * content beneath it (e.g. App-mode forces drawer on desktop, so it covered + * the itinerary overlay + welcome suggestions). Make the host inert; the + * on-screen panel re-enables pointer events below. */ + pointer-events: none; } :host([data-mode="drawer"][data-open="true"]) { box-shadow: 8px 0 32px rgba(0, 0, 0, 0.18); @@ -57,6 +65,10 @@ export const CHAT_SIDENAV_STYLES = ` height: 100%; transition: transform 200ms ease; transform: translateX(-100%); + /* Re-enable pointer events on the panel itself (host is inert above). When + * closed the panel is translated off-screen so this is harmless; when open + * it covers the host box and stays fully interactive. */ + pointer-events: auto; } :host([data-mode="drawer"][data-open="true"]) .chat-sidenav { transform: translateX(0);