Skip to content

feat(app): close search & session drawers on outside click - #2682

Merged
kodiakhq[bot] merged 6 commits into
mainfrom
elizabet/close-drawer-on-outside-click
Jul 21, 2026
Merged

feat(app): close search & session drawers on outside click#2682
kodiakhq[bot] merged 6 commits into
mainfrom
elizabet/close-drawer-on-outside-click

Conversation

@elizabetdev

@elizabetdev elizabetdev commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Why

When a detail drawer is open on the Search page, clicking outside of the logs table should dismiss it. Same for the session drawer on the Sessions page. Previously these drawers only closed via the X button, Esc, or breadcrumb navigation.

These drawers are Mantine Drawers rendered with withOverlay={false} (so the page stays interactive behind them), which means there is no overlay to catch outside clicks — hence Mantine's built-in closeOnClickOutside does nothing.

What

Added a small reusable hook, useCloseOnClickOutside, and wired it into the row-table / session detail drawers. It's on by default (opt out with closeOnClickOutside={false}).

  • Search: DBSqlRowTableWithSidebar (row detail drawer) and DirectTraceSidePanel. Keep-open zone = the results panel ([data-testid="search-results-panel"]).
  • Sessions: SessionSidePanel. Keep-open zone = the session list ([data-testid="session-card-list"]). Gated to the root view (!selectedEvent), mirroring the existing Esc behavior so deep in-panel navigation isn't skipped.
  • Default keep-open zone: DBSqlRowTableWithSidebar defaults its keep-open zone to its own results table ([data-testid="search-results-table"]), so every consumer gets clean row-switching without a page needing to opt in.

Behavior

  • Clicking a different row/session switches the panel.
  • Clicking inside the drawer or any popup it spawns (Select dropdown, date picker, nested modal) keeps it open — the hook treats Mantine floating layers (role="dialog"/tooltip/listbox/menu, dropdown classes, overlays) as "inside", which is what makes it robust against portaled content.
  • Clicking anywhere else on the page (search bar, filters, chart, empty space) closes it.

Reuse safety

DBSqlRowTableWithSidebar / DBRowSidePanel are reused in several places. The other consumers (PodDetailsSidePanel, PatternSidePanel, K8s node/namespace/pod detail panels) render them inside their own Mantine Drawer (role="dialog"), so the floating-layer exemption neutralizes the behavior there — clicks inside the parent drawer count as "inside". Dashboards render the table directly, where click-outside-to-close is the desired behavior and the default table keep-open zone prevents any close/reopen jank.

Note / possible follow-up

On Search, "outside of the logs table" is taken fairly literally: the keep-open zone is the results panel (table + find-in-page), so clicking the histogram/stats above it also closes the drawer. If we'd rather keep it open across the whole results area, that's a one-line change to which element carries the data-testid.

Testing

  • New unit test useCloseOnClickOutside.test.tsx (4 cases) — passing.
  • Existing SessionSidePanel, DirectTraceSidePanel, DBSearchPage.directTrace, and DBRowSidePanel suites — passing (30 tests across 7 suites).
  • Lint clean on changed files; changeset included.

Made with Cursor

Co-authored-by: Cursor <cursoragent@cursor.com>
@vercel

vercel Bot commented Jul 19, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
hyperdx-oss Ready Ready Preview, Comment Jul 21, 2026 8:55am
hyperdx-storybook Ready Ready Preview, Comment Jul 21, 2026 8:55am

Request Review

@changeset-bot

changeset-bot Bot commented Jul 19, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f1806be

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@hyperdx/app Patch
@hyperdx/api Patch
@hyperdx/otel-collector Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@greptile-apps

greptile-apps Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds a small useCloseOnClickOutside hook and wires it into the row-detail and session-detail drawers so they dismiss on outside clicks, mirroring the existing Esc behaviour. The hook uses the capture phase, exempts all Mantine floating layers (role="dialog", dropdowns, overlays), and accepts a keepOpenSelector to designate "safe" regions (results table / session list) where clicks should not dismiss.

  • useCloseOnClickOutside: depth-less, composable hook; the narrowed Mantine calendar [role="grid"] selector and useStableCallback stabilization address concerns from earlier review rounds.
  • DBRowSidePanelErrorBoundary: gated on sourceStack.length === 0 && navStack.length === 0 so deep in-panel navigation isn't skipped — matches Esc pop-one-level behaviour.
  • SessionSidePanel: gated on !selectedEvent, also matching the existing Esc guard.

Confidence Score: 5/5

Safe to merge; the change is additive and isolated to drawer dismiss behaviour with clean opt-out paths.

The hook is narrow in scope, the depth guards correctly replicate the existing Esc behaviour in both DBRowSidePanelErrorBoundary and SessionSidePanel, and existing consumers that render inside their own role=dialog are naturally neutralised by the floating-layer exemption. The only unaddressed gap is that right-click mousedown events will trigger close, which is a minor UX rough edge rather than a logic defect.

packages/app/src/hooks/useCloseOnClickOutside.ts — the right-click filtering gap is the only item worth a follow-up look.

Important Files Changed

Filename Overview
packages/app/src/hooks/useCloseOnClickOutside.ts New reusable hook for outside-click dismissal; correctly uses capture phase, floating-layer exemption, and useStableCallback. Right-click events are not filtered, so context-menu gestures outside the drawer will also trigger close.
packages/app/src/components/DBRowSidePanel.tsx Correctly adds depth guard (sourceStack.length === 0 && navStack.length === 0) so outside-click only fires at the root panel level, matching Esc behavior.
packages/app/src/components/Search/DirectTraceSidePanel.tsx Hook enabled/disabled based on the opened prop; no internal navigation stack so no depth guard is needed.
packages/app/src/components/DBSqlRowTableWithSidebar.tsx Sensible default keepOpenSelector pointing to the results table; props are threaded through to DBRowSidePanelErrorBoundary cleanly.
packages/app/src/SessionSidePanel.tsx Correctly gates hook on sessionId != null && !selectedEvent, mirroring the existing Esc hotkey guard.
packages/app/src/DBSearchPage.tsx Adds data-testid=search-results-panel keep-open zone and wires it into both DirectTraceSidePanel and DBSqlRowTableWithSidebar.
packages/app/src/SessionsPage.tsx Adds data-testid=session-card-list to the list container and passes it as keepOpenSelector to SessionSidePanel.
packages/app/src/hooks/tests/useCloseOnClickOutside.test.tsx Four unit tests cover close, keep-open selector, floating-layer exemption, and enable/disable/cleanup — good coverage for the hook.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[mousedown captured on document] --> B{event.target instanceof Element?}
    B -- No --> Z[return]
    B -- Yes --> C{target.closest FLOATING_LAYER_SELECTOR?}
    C -- Yes --> Z
    C -- No --> D{keepOpenSelector provided?}
    D -- Yes --> E{target.closest keepOpenSelector?}
    E -- Yes --> Z
    E -- No --> F[stableOnClose]
    D -- No --> F
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[mousedown captured on document] --> B{event.target instanceof Element?}
    B -- No --> Z[return]
    B -- Yes --> C{target.closest FLOATING_LAYER_SELECTOR?}
    C -- Yes --> Z
    C -- No --> D{keepOpenSelector provided?}
    D -- Yes --> E{target.closest keepOpenSelector?}
    E -- Yes --> Z
    E -- No --> F[stableOnClose]
    D -- No --> F
Loading

Reviews (6): Last reviewed commit: "Merge branch 'main' into elizabet/close-..." | Re-trigger Greptile

Comment thread packages/app/src/hooks/useCloseOnClickOutside.ts Outdated
@github-actions

github-actions Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

E2E Test Results

All tests passed • 240 passed • 1 skipped • 1051s

Status Count
✅ Passed 240
❌ Failed 0
⚠️ Flaky 2
⏭️ Skipped 1

Tests ran across 4 shards in parallel.

View full report →

…lickOutside

Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread packages/app/src/components/DBRowSidePanel.tsx
Co-authored-by: Cursor <cursoragent@cursor.com>
@elizabetdev
elizabetdev marked this pull request as ready for review July 20, 2026 10:50
@github-actions github-actions Bot added the review/tier-2 Low risk — AI review + quick human skim label Jul 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🔵 Tier 2 — Low Risk

Small, isolated change with no API route or data model modifications.

Why this tier:

  • Standard feature/fix — introduces new logic or modifies core functionality

Review process: AI review + quick human skim (target: 5–15 min). Reviewer validates AI assessment and checks for domain-specific concerns.
SLA: Resolve within 4 business hours.

Stats
  • Production files changed: 7
  • Production lines changed: 155 (+ 91 in test files, excluded from tier calculation)
  • Branch: elizabet/close-drawer-on-outside-click
  • Author: elizabetdev

To override this classification, remove the review/tier-2 label and apply a different review/tier-* label. Manual overrides are preserved on subsequent pushes.

@github-actions

Copy link
Copy Markdown
Contributor

Deep Review

Scope: New useCloseOnClickOutside hook + wiring into Search/Sessions detail drawers (9 files, ~230 lines).
Intent: Dismiss overlay-less Mantine drawers on outside click; on by default with keepOpenSelector keep-open zones and portaled-floating-layer exemptions.
Mode: report-only (read-only).

No data-loss, auth, injection, or guaranteed-crash issues were found. The feature is purely client-side UI dismissal built on onClose handlers that already existed (Esc / X). Findings are behavior-quality, maintainability, and test-coverage concerns.

✅ No critical issues found.

🟡 P2 -- recommended

  • packages/app/src/hooks/useCloseOnClickOutside.ts:45 -- The handler fires on every mousedown regardless of button, so a right-click on empty page area or a native scrollbar drag that starts outside the keep-open zone dismisses the open drawer.
    • Fix: Ignore non-primary buttons by returning early unless event.button === 0.
    • testing
  • packages/app/src/hooks/useCloseOnClickOutside.ts:10 -- Portaled layers not covered by FLOATING_LAYER_SELECTOR (e.g. Mantine notification toasts, or any non-Mantine/roleless popover spawned while a drawer is open) are treated as "outside", so clicking one closes the drawer.
    • Fix: Broaden the exemption to a stable portal marker or a positive keep-open container, and add a regression test asserting an unmatched portaled element still closes so the allowlist contract is explicit.
    • testing, maintainability
  • packages/app/src/components/DBSqlRowTableWithSidebar.tsx:61 -- Defaulting closeOnClickOutside to true enables outside-click dismissal for every consumer of this component, including dashboard tiles (DBDashboardPage, KubernetesDashboardPage, ServiceDashboardSlowestEventsTile) that do not opt out and have no coverage for the new behavior.
    • Fix: Confirm outside-click dismissal is intended on dashboard tiles and add coverage, or pass closeOnClickOutside={false} where it is not wanted.
    • maintainability, kieran-typescript
  • packages/app/src/components/DBSqlRowTableWithSidebar.tsx:57 -- The data-testid values (search-results-table, search-results-panel, session-card-list) are now load-bearing for runtime dismissal but are referenced by no test, so renaming or removing a test-looking attribute silently breaks drawer dismissal with no failing test.
    • Fix: Use a purpose-named attribute (e.g. data-keep-panel-open) or a shared ref for keep-open zones, or add regression coverage pinning each data-testid.
    • maintainability
  • packages/app/src/hooks/useCloseOnClickOutside.ts:11 -- FLOATING_LAYER_SELECTOR matches Mantine-internal class substrings ([class*="mantine-"][class*="dropdown"], [class*="mantine-Overlay"], calendar role="grid"), which are not a public API, so a Mantine upgrade renaming those classes silently regresses the "clicks inside a nested popup keep the drawer open" guarantee.
    • Fix: Anchor on stable roles/attributes where possible, add a version note tying the constant to the pinned Mantine version, and add a regression test rendering real Mantine Select/DatePicker/Modal rather than synthetic divs.
    • kieran-typescript, maintainability
  • packages/app/src/hooks/__tests__/useCloseOnClickOutside.test.tsx:1 -- The four unit tests exercise the isolated hook only; the wiring into all six consumers and the gating branches (sidePanelStack root-only in DBRowSidePanel, !selectedEvent in SessionSidePanel) are entirely untested, and these gates are the most regression-prone part of the change.
    • Fix: Add integration tests asserting an outside click is ignored when a panel has navigated deeper and fires only at the root, plus that clicking a different row switches rather than closes-and-reopens.
    • testing, project-standards, kieran-typescript, maintainability
🔵 P3 nitpicks (4)
  • packages/app/src/components/DBRowSidePanel.tsx:1075 -- DBRowSidePanel defaults closeOnClickOutside on but leaves keepOpenSelector undefined, so direct callers (PodDetailsSidePanel, PatternSidePanel) rely implicitly on being nested inside a parent role="dialog" drawer for the behavior to be neutralized (verified safe for current consumers).
    • Fix: Default DBRowSidePanel's closeOnClickOutside to false, or require keepOpenSelector when it is enabled, so the contract is explicit.
  • packages/app/src/DBSearchPage.tsx:209 -- Search passes keepOpenSelector="search-results-panel" into DBSqlRowTableWithSideBar, overriding its DEFAULT_KEEP_OPEN_SELECTOR="search-results-table"; since the panel wraps the table the table-level default is dead in the Search path.
    • Fix: Drop the redundant override or document why the wider panel zone is intended on Search.
  • packages/app/src/DBSearchPage.tsx:209 -- The keep-open selector is duplicated as three separate constants across DBSearchPage, SessionsPage, and DBSqlRowTableWithSidebar, each paired with a distant JSX attribute that must be renamed in lockstep.
    • Fix: Co-locate each constant with its element or export shared selector constants referenced at both the query and attribute sites.
  • packages/app/src/hooks/__tests__/useCloseOnClickOutside.test.tsx:58 -- The floating-layer test dispatches two mousedowns then makes one onClose assertion, so a broken dialog-vs-dropdown matcher can still pass; calendar-grid, overlay, tooltip, listbox, and menu branches have no coverage.
    • Fix: Split into per-selector cases and add assertions for the remaining FLOATING_LAYER_SELECTOR branches.

Reviewers (7): testing, maintainability, project-standards, kieran-typescript, previous-comments, agent-native, learnings. Correctness, adversarial, and frontend-races were still running at synthesis time; their primary angles (open/close race, portaled-content dismissal, mouse-button handling) were verified directly against the code and folded into the findings above.

Testing gaps:

  • Hook-to-consumer wiring and gating branches are covered by zero integration tests.
  • FLOATING_LAYER_SELECTOR is validated only against synthetic divs, so a real Mantine class-name change would not fail a test.
  • No test covers non-primary mouse buttons, scrollbar clicks, or unmatched portaled popups closing the drawer.
  • Prior review feedback (calendar role="grid" scoping, root-level depth guard) is confirmed addressed.

@kodiakhq
kodiakhq Bot merged commit e6e0907 into main Jul 21, 2026
27 checks passed
@kodiakhq
kodiakhq Bot deleted the elizabet/close-drawer-on-outside-click branch July 21, 2026 09:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

automerge review/tier-2 Low risk — AI review + quick human skim

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants