Skip to content

fix(chat): closed drawer sidenav must not intercept clicks on content beneath it#778

Merged
blove merged 1 commit into
mainfrom
fix/chat-sidenav-drawer-pointer-events
Jul 8, 2026
Merged

fix(chat): closed drawer sidenav must not intercept clicks on content beneath it#778
blove merged 1 commit into
mainfrom
fix/chat-sidenav-drawer-pointer-events

Conversation

@blove

@blove blove commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Bug

In App mode (canonical demo), the left thread sidenav covered the content and swallowed clicks — you couldn't click the "Plan a long weekend in Paris" welcome suggestion.

Root cause

chat-sidenav.styles.ts, drawer mode: the host is position: fixed; left:0; width: ~280px; height:100%; z-index:1001. When the drawer is closed, only the inner .chat-sidenav panel slides off-screen (translateX(-100%)) — the host box stays put. With no pointer-events, that transparent host is an invisible click-catcher over the left strip of whatever's beneath it. App mode forces drawer mode on desktop, so it sat over the itinerary overlay + welcome suggestions and ate the clicks. (Latent on mobile too.)

Fix

  • Drawer hostpointer-events: none (inert whether open or closed).
  • .chat-sidenav panelpointer-events: auto (harmless off-screen when closed; interactive when open, where it covers the host box).

Two-line CSS change, scoped to drawer mode; expanded/collapsed modes untouched. Benefits every drawer-mode consumer (incl. the ag-ui demo).

Test

Adds an examples/chat e2e guard: in App mode (/sidebar?appmode=on, drawer closed), a point over the closed-drawer box resolves to the content beneath (not chat-sidenav), and the host computes pointer-events: none. Red before this fix, green after.

Verification note

Local browser verification was blocked in this environment (Chrome-MCP classifier outage + a parallel nx serve holding the serve-target lock), so this relies on the new e2e guard running in clean CI. The fix is a deterministic CSS change traced directly from the source.

🤖 Generated with Claude Code

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 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
threadplane Ready Ready Preview, Comment Jul 8, 2026 12:07am

Request Review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated approval: this PR received an intelligent (AI) code review. See the review comments on this PR.

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Claude finished @blove's task in 1m 41s —— View job


Code Review

  • Gather context
  • Read changed files
  • Review CSS fix
  • Review e2e test
  • Post findings

The fix is correct and minimal. The root cause analysis is accurate: the host element in drawer mode is a fixed, full-height box that sits in place regardless of open/close state — only the inner panel slides off-screen. Without pointer-events: none on the host, that invisible box silently eats clicks in the left strip.

libs/chat/src/lib/styles/chat-sidenav.styles.ts

The CSS pattern applied here — pointer-events: none on the host, pointer-events: auto on the inner panel — is the standard idiom for exactly this situation. The logic holds:

  • Drawer closed: host is transparent and inert; panel is off-screen (translateX(-100%)) with auto on it, which is harmless since it's outside the viewport.
  • Drawer open: panel translates to translateX(0), covering the host's area. pointer-events: auto on the panel ensures all interactive elements inside (buttons, thread rows) remain clickable.

I also checked chat-sidenav-scrim.component.ts — the backdrop is rendered as a sibling of <chat-sidenav> (not inside it), at z-index 1000. The host's pointer-events: none does not affect the scrim, and the scrim's click-to-dismiss behavior remains intact. ✅

The box-shadow applied to the open host (:host([data-mode="drawer"][data-open="true"])) renders correctly even with pointer-events: none — shadows are purely visual and don't extend hit areas. ✅

examples/chat/angular/e2e/app-mode-itinerary.spec.ts

The test is well-structured. elementFromPoint is the correct browser API for hit-testing and correctly respects pointer-events: none. The getComputedStyle check on the host adds a second, independent assertion. Using both is good defensive practice.

One observation: the if (!probe.ok) return guard after expect(probe.ok).toBe(true) is dead code at runtime — if ok is false, the expect already fails the test before the guard is reached. It's purely a TypeScript narrowing aid for the type checker. Harmless, but could be a // TypeScript narrowing comment to signal intent.

Minor gap: libs/chat/src/lib/styles/chat-sidenav.styles.spec.ts has a 'drawer elevation + z-index token' describe block that tests the drawer-mode CSS rules (z-index token, box-shadow). Adding two matchers there for the new pointer-events rules would protect against future regressions without needing e2e:

it('drawer host is pointer-events: none (closed host must not intercept clicks)', () => {
  expect(normalized).toMatch(
    /:host\(\[data-mode="drawer"\]\)\s*\{[^}]*pointer-events:\s*none\s*;/,
  );
});
it('drawer panel restores pointer-events: auto', () => {
  expect(normalized).toMatch(
    /:host\(\[data-mode="drawer"\]\)\s*\.chat-sidenav\s*\{[^}]*pointer-events:\s*auto\s*;/,
  );
});

Fix this →

This is a minor improvement, not a blocker. The e2e guard already catches the regression. The fix itself is correct.

@blove blove merged commit f2c5526 into main Jul 8, 2026
63 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant