Skip to content

Fix combobox inside the health-check editor sheet#1110

Closed
RhysSullivan wants to merge 5 commits into
claude/health-checks-providersfrom
claude/health-checks-ui-fixes
Closed

Fix combobox inside the health-check editor sheet#1110
RhysSullivan wants to merge 5 commits into
claude/health-checks-providersfrom
claude/health-checks-ui-fixes

Conversation

@RhysSullivan

@RhysSullivan RhysSullivan commented Jun 23, 2026

Copy link
Copy Markdown
Owner

The operation/identity comboboxes portal their popup to <body>, outside the sheet's dialog subtree, so a modal Radix sheet froze them: body pointer-events were disabled (couldn't click/select) and react-remove-scroll locked wheel scroll.

  • The editor sheet is non-modal, which drops both locks while keeping the dim overlay.
  • The popup gets pointer-events: auto (defensive for any modal-dialog combobox).
  • The sheet stays open when an interaction originates inside a combobox popup, so clicking an option no longer dismisses it.

Adds e2e guards that click and wheel-scroll the popup inside the sheet.

Stacked on #1109.

Stack

  1. Connection health checks (liveness) with OpenAPI backing #1108
  2. Connection account info: derive identity from the health-check probe #1111
  3. MCP connection liveness health checks #1112
  4. Health checks for Microsoft Graph and Google #1109
  5. Fix combobox inside the health-check editor sheet #1110 👈 current

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 23, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
executor-marketing 816cfdd Commit Preview URL

Branch Preview URL
Jun 25 2026, 08:51 PM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 23, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
executor-cloud 816cfdd Jun 25 2026, 08:52 PM

@github-actions

github-actions Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Cloudflare preview

Console https://executor-preview-pr-1110.executor-e2e.workers.dev
MCP https://executor-preview-pr-1110.executor-e2e.workers.dev/mcp
Deployed commit 816cfdd

Sign-in is Cloudflare Access (one-time PIN to an allowed email). The preview has its own database and encryption key; it is destroyed when this PR closes.

@greptile-apps

greptile-apps Bot commented Jun 23, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes portaled combobox popups (operation and identity pickers) becoming unclickable and unscrollable inside the health-check editor sheet by applying three complementary changes.

  • The editor sheet is switched to modal={false}, dropping the react-remove-scroll body lock and Radix's pointer-events: none that froze all elements outside the dialog subtree, including the portaled popup.
  • pointer-events-auto is added to the ComboboxPrimitive.Positioner so the popup stays interactive if it is ever rendered inside any modal dialog.
  • SheetContent gains an onInteractOutside guard that checks whether a click originates inside [data-slot='combobox-content'] or [data-slot='select-content'] and calls event.preventDefault() to prevent the sheet from closing before an option selection can land.
  • Two new e2e scenarios cover the exact failure modes: scenario 6 verifies that clicking a combobox option selects it without dismissing the sheet, and scenario 7 verifies that wheel-scrolling the overflowing list actually moves it.

Confidence Score: 5/5

The three-part fix (non-modal sheet, pointer-events-auto on the positioner, onInteractOutside guard) is internally consistent and each change is independently correct. The e2e tests exercise both the click and scroll failure modes directly.

All changes are narrowly scoped to the health-check editor sheet and the shared combobox component. The onInteractOutside guard uses a precise data-slot selector and guards only against the known portal pattern. The pointer-events-auto addition is a safe override that is harmless when no modal is involved. No data-path changes, no new APIs exposed.

No files require special attention beyond the open threads already on this PR.

Important Files Changed

Filename Overview
packages/react/src/components/sheet.tsx Adds onInteractOutside guard to prevent sheet dismissal when a click originates inside a portaled combobox/select popup; extracts and forwards the consumer's onInteractOutside prop correctly.
packages/react/src/components/combobox.tsx Adds pointer-events-auto to ComboboxPrimitive.Positioner so the portaled popup remains interactive when Radix disables pointer events on the body for any modal dialog.
packages/react/src/components/health-check-editor.tsx Sets modal={false} on the editor Sheet to drop the react-remove-scroll body lock and pointer-events:none that were freezing the portaled combobox popups.
e2e/scenarios/health-checks-ui.test.ts Adds scenarios 6 (mouse-click option selects without closing sheet) and 7 (wheel-scroll works in non-modal sheet) using Effect.ensuring cleanup, deterministic polling, and value-based assertions aligned with e2e/AGENTS.md quality bar.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User
    participant ComboboxTrigger
    participant ComboboxPopup as ComboboxPopup (portaled to body)
    participant Sheet as Sheet (modal=false)
    participant Radix as Radix Dialog

    User->>ComboboxTrigger: click
    ComboboxTrigger->>ComboboxPopup: open popup (portaled outside sheet subtree)
    User->>ComboboxPopup: click option
    ComboboxPopup->>Radix: pointerdown fires outside sheet content
    Radix->>Sheet: onInteractOutside(event)
    Sheet->>Sheet: target.closest(PORTALED_POPUP_SELECTOR)?
    alt target is inside combobox popup
        Sheet->>Radix: event.preventDefault() -- sheet stays open
        ComboboxPopup->>ComboboxTrigger: selection applied
    else target is truly outside
        Radix->>Sheet: dismiss sheet
    end
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"}}}%%
sequenceDiagram
    participant User
    participant ComboboxTrigger
    participant ComboboxPopup as ComboboxPopup (portaled to body)
    participant Sheet as Sheet (modal=false)
    participant Radix as Radix Dialog

    User->>ComboboxTrigger: click
    ComboboxTrigger->>ComboboxPopup: open popup (portaled outside sheet subtree)
    User->>ComboboxPopup: click option
    ComboboxPopup->>Radix: pointerdown fires outside sheet content
    Radix->>Sheet: onInteractOutside(event)
    Sheet->>Sheet: target.closest(PORTALED_POPUP_SELECTOR)?
    alt target is inside combobox popup
        Sheet->>Radix: event.preventDefault() -- sheet stays open
        ComboboxPopup->>ComboboxTrigger: selection applied
    else target is truly outside
        Radix->>Sheet: dismiss sheet
    end
Loading

Reviews (4): Last reviewed commit: "fix: combobox works inside the health-ch..." | Re-trigger Greptile

// (portaled to <body>, outside the dialog subtree) - it can't be scrolled or
// clicked. The overlay still renders and the dismissal guard still keeps the
// sheet open while interacting with the popup.
<Sheet open={open} onOpenChange={onOpenChange} modal={false}>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 modal={false} disables focus trapping

Setting modal={false} drops react-remove-scroll and the body pointer-events lock (the stated goal), but it also disables Radix's focus trap. Keyboard and screen-reader users can Tab past the sheet's close button and land on focusable elements under the overlay, even though the overlay visually blocks the page. For a right-side editor panel this is likely to surprise users who expect Tab to cycle within the sheet. Consider whether focus should be manually managed (e.g. initialFocus / finalFocus props) or whether a FocusScope wrapper inside SheetContent should be added to restore the trapping behaviour without re-enabling the scroll lock.

Comment on lines +63 to +68
onInteractOutside={(event) => {
const target = event.detail.originalEvent.target;
if (target instanceof Element && target.closest(PORTALED_POPUP_SELECTOR)) {
event.preventDefault();
}
onInteractOutside?.(event);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 onInteractOutside consumer receives an already-prevented event

The custom handler calls event.preventDefault() before forwarding to onInteractOutside?.(event). Any consumer that inspects event.defaultPrevented to decide whether to suppress its own side-effects will see the event as already prevented for every combobox/select click. The forward call is also unconditional: even when the guard fires, the consumer still gets invoked. If future callers expect the callback to fire only when a real outside interaction happened, they will see spurious calls. Guarding the forward call with an early return when the event is suppressed, or documenting this behaviour explicitly, would make the contract clearer.

@pkg-pr-new

pkg-pr-new Bot commented Jun 23, 2026

Copy link
Copy Markdown

Open in StackBlitz

@executor-js/cli

npm i https://pkg.pr.new/@executor-js/cli@1110

@executor-js/config

npm i https://pkg.pr.new/@executor-js/config@1110

@executor-js/execution

npm i https://pkg.pr.new/@executor-js/execution@1110

@executor-js/sdk

npm i https://pkg.pr.new/@executor-js/sdk@1110

@executor-js/codemode-core

npm i https://pkg.pr.new/@executor-js/codemode-core@1110

@executor-js/runtime-quickjs

npm i https://pkg.pr.new/@executor-js/runtime-quickjs@1110

@executor-js/plugin-file-secrets

npm i https://pkg.pr.new/@executor-js/plugin-file-secrets@1110

@executor-js/plugin-graphql

npm i https://pkg.pr.new/@executor-js/plugin-graphql@1110

@executor-js/plugin-keychain

npm i https://pkg.pr.new/@executor-js/plugin-keychain@1110

@executor-js/plugin-mcp

npm i https://pkg.pr.new/@executor-js/plugin-mcp@1110

@executor-js/plugin-onepassword

npm i https://pkg.pr.new/@executor-js/plugin-onepassword@1110

@executor-js/plugin-openapi

npm i https://pkg.pr.new/@executor-js/plugin-openapi@1110

executor

npm i https://pkg.pr.new/executor@1110

commit: 816cfdd

A connection can declare ONE authenticated operation that answers "is this
credential still alive?". The probe runs the operation, maps the HTTP status to
a health state (2xx healthy, 401/403 expired, other degraded, unset unknown),
and reports it. Built for short-lived OAuth tokens (Google's 7-day dev-token
revocation): a credential that authenticated yesterday and now 401s reads as
expired.

Core owns the shared vocabulary (HealthStatus, HealthCheckSpec, HealthCheckResult,
HealthCheckCandidate, classifyHttpStatus, candidate ranking); plugins own which
operation runs, stored in their opaque integration config and picked the same way
auth methods are. Dispatch adds integrations.healthCheck.{get,candidates,set} and
connections.{checkHealth,validate}.

OpenAPI backing: list ranked candidates (non-destructive GET first), persist the
chosen spec (merging over the raw config so provider supersets keep their keys),
and run the probe against a resolved credential.

React surfaces:
- a per-connection status dot + "Check now",
- a self-hiding operation editor (a searchable freeform combobox over the whole
  spec),
- and "Check the key works" in the Add Connection modal: probe the pasted key
  before saving. When no health check is configured yet, the user picks a
  read-only operation inline; a healthy probe saves it as the integration's
  health check.

Covered by e2e: a saved connection flips healthy -> expired when its key stops
working; an unconfigured integration reports unknown; the operation picker
filters a hundreds-long candidate list on both the edit sheet and the add screen;
and Add Connection checks the key against an inline-picked operation and persists
it.
Layer account identity on top of the liveness probe. A health check can now
name a response field whose value identifies the connected account, so the same
probe that answers "is this alive?" also answers "whose account is this?".

Core: HealthCheckSpec gains an optional identityField (a dot-path into the
response body); HealthCheckResult carries the extracted identity plus a bounded
responseSample of the actual returned fields; HealthCheckCandidate carries the
operation's projected responseFields. New pure helpers: extractIdentity (resolve
a dot-path, numeric segments index arrays), projectResponseFields (enumerate a
response schema's scalar leaves breadth-first, merging every allOf/oneOf/anyOf
branch so discriminated-union fields aren't dropped), and extractResponseFields
(walk a real body for the live preview).

OpenAPI backing extracts the identity on a healthy probe and projects each
candidate's response fields. React: the editor gains a typed identity-field
picker fed by those fields and a live preview (probe a pasted test key, see the
real response plus what the identity resolves to); the account row labels itself
with the probed identity; and the Add Connection "check the key works" flow gains
the identity field (in the inline picker) and auto-fills the connection name from
the probed identity.

Covered by e2e: validating a key derives the identity; a saved connection's probe
surfaces the account then drops it once expired; the identity picker surfaces a
shallow scalar and a second-union-branch-only field across a discriminated union;
the editor live preview and the Add Connection name-derivation drive the identity
flow in the browser.
Give MCP connections a liveness probe. `checkHealth` dials the server and lists
its tools (the same connect path tool discovery uses): a credential that
authenticates and gets a tool list reads healthy; a 401/403 (or an OAuth
re-authorization signal) reads expired; any other connection/discovery failure
reads degraded. The connect-modal "Validate key" path runs the same probe on an
unsaved credential.

MCP gets liveness ONLY: there is no usable identity source (no id_token, no
userinfo, no whoami convention across servers), so no identity is derived and no
operation/identity editor is shown - the connection's name stays the user's
label. The plugin implements only `checkHealth`, so the editor self-hides while
the generic status dot + "Check now" still render.

Factors the HTTP-status extraction out of invoke into a shared http-status helper
and surfaces the upstream status in connect errors so the probe can classify a
401/403 as expired rather than a generic degraded.

Covered by e2e: a saved MCP connection reads healthy, then expired once the
upstream revokes the token; validate reports healthy for a live key and expired
for a rejected one; no identity is ever derived.
Wire the OpenAPI health-check backing into the Microsoft Graph and Google
provider plugins (both compile their spec through the OpenAPI machinery and store
config in the same shape), so their connections report alive/expired + identity
instead of always "unknown".

Both plugins now implement the four health-check hooks by delegating to the
shared OpenAPI backing, and persist a `healthCheck` field on their own config
schema (so it survives the plugin's own decode). Each auto-configures a sensible
default at add time:

- Microsoft Graph: `GET /me` with identity `userPrincipalName` (always present on
  /me, unlike `mail`), when the default profile preset is selected.
- Google: People API `people.get` with the required args pinned
  (`resourceName=people/me`, `personFields=names,emailAddresses`) and identity
  `emailAddresses.0.value`, when the bundle includes the People API.

The user can switch the operation/identity from the now-available editor.

Covered by e2e: adding a Google People bundle (against a local discovery doc)
auto-writes the default identity check and projects the email field as a typed
candidate. Graph's spec source is a fixed upstream URL, so its hooks + default
are covered through the shared backing path and verified against a live tenant.
The operation/identity pickers are base-ui comboboxes whose popup portals to
document.body, which collides with the Radix sheet the editor opens in:

- The sheet runs modal, so react-dismissable-layer sets the body's
  pointer-events to none and react-remove-scroll locks the wheel to the sheet
  subtree. The portaled popup, living outside that subtree, could neither be
  clicked nor scrolled. Fix: pointer-events:auto on the popup, and open the
  editor sheet non-modal.
- Clicking a portaled option is a pointer-down outside the sheet and dismissed
  it before the selection landed. Fix: the sheet's onInteractOutside ignores
  interactions originating in a combobox/select popup.

Covered by e2e: opening the editor over a large spec, the operation popup
scrolls and an option is clickable without the sheet closing.
@RhysSullivan RhysSullivan force-pushed the claude/health-checks-providers branch from 69d2ed8 to cc3fa8e Compare June 25, 2026 20:46
@RhysSullivan RhysSullivan force-pushed the claude/health-checks-ui-fixes branch from 674550b to 816cfdd Compare June 25, 2026 20:46
@RhysSullivan

Copy link
Copy Markdown
Owner Author

Folded into the base PR (#1108). The combobox-in-modal fixes (popup pointer-events, dialog + sheet outside-interaction guards, non-modal editor sheet) are prerequisites for the base PR's operation picker in both the editor sheet and the Add Connection modal, so they belong in #1108 rather than as a trailing PR. The scroll/click e2e moved to #1108 (sheet) and the identity-combobox click into #1111.

@RhysSullivan RhysSullivan force-pushed the claude/health-checks-providers branch from cc3fa8e to 2cbcd9c Compare June 26, 2026 02:26
@RhysSullivan RhysSullivan deleted the claude/health-checks-ui-fixes branch June 26, 2026 02:26
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