Skip to content

fix(gui): portal select dropdowns to avoid clipping - #393

Merged
lidge-jun merged 4 commits into
lidge-jun:devfrom
Wibias:codex/fix-issue-340-claude-sidecar-dropdown
Jul 24, 2026
Merged

lidge-jun merged 4 commits into
lidge-jun:devfrom
Wibias:codex/fix-issue-340-claude-sidecar-dropdown

Conversation

@Wibias

@Wibias Wibias commented Jul 24, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #340

Summary

  • Portal shared GUI Select dropdown menus to document.body so they can escape clipped parents
  • Add viewport-aware positioning with upward flip near the bottom edge
  • Add unit tests for dropdown positioning logic

Testing

  • bun test ./tests/select-position.test.ts
  • bun run lint:gui
  • bun run typecheck

Summary by CodeRabbit

  • New Features

    • Added portal rendering for selected dropdowns, allowing menus to appear outside clipped containers.
    • Improved dropdown positioning, including viewport-aware placement, automatic upward flipping, and boundary handling.
    • Added support for correctly layering portal-based dropdowns above other interface elements.
  • Bug Fixes

    • Prevented dropdown menus from being clipped or incorrectly positioned within settings panels.
  • Tests

    • Added coverage for portal rendering and responsive dropdown positioning.

@github-actions github-actions Bot added the bug Something isn't working label Jul 24, 2026
@coderabbitai

coderabbitai Bot commented Jul 24, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ef8e275c-371b-4dc5-8f05-1e3a72ca7d93

📥 Commits

Reviewing files that changed from the base of the PR and between b758556 and 33aaa5b.

📒 Files selected for processing (6)
  • gui/src/pages/ClaudeCode.tsx
  • gui/src/select-position.ts
  • gui/src/styles.css
  • gui/src/ui.tsx
  • gui/tests/select-portal.test.tsx
  • gui/tests/select-position.test.ts

📝 Walkthrough

Walkthrough

Claude settings dropdowns now support portal rendering outside clipped containers. The Select component calculates fixed viewport-aware placement, flips or clamps menus as needed, applies portal styling, and preserves inline rendering by default. Tests cover both rendering modes and placement behavior.

Changes

Select menu portal flow

Layer / File(s) Summary
Viewport-aware menu positioning
gui/src/select-position.ts, gui/tests/select-position.test.ts
Defines menu geometry options and computes fixed placement with vertical flipping, height limits, alignment, and viewport clamping; tests cover these cases.
Portal rendering and lifecycle
gui/src/ui.tsx, gui/src/styles.css, gui/tests/select-portal.test.tsx
Adds the optional portal prop, body-mounted dropdown rendering, repositioning handlers, portal stacking styles, and portal/non-portal rendering tests.
Claude settings integration
gui/src/pages/ClaudeCode.tsx
Enables portal rendering for authentication, auto-compaction, and sidecar backend selectors.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Select
  participant Positioning
  participant DocumentBody
  User->>Select: Open dropdown
  Select->>Positioning: Compute trigger-based menu style
  Positioning-->>Select: Return fixed placement
  Select->>DocumentBody: Render dropdown through portal
  DocumentBody-->>User: Display positioned listbox
Loading

Suggested reviewers: lidge-j

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@Ingwannu Ingwannu left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

The clipping bug in #340 is real and using a portal is the right general fix, but portaling every Select to document.body breaks the existing language-menu contract. The menu is no longer a descendant of .lang-toggle/.sidebar, so the contextual glass fallback and, more importantly, the mobile rule that moves .select-dropdown-beside above the trigger no longer apply. The shared right-placement calculation can also keep a 120px menu at top=700 in an 800px viewport and enforce maxHeight >=120, leaving it outside the viewport (the new test currently expects this overflow). Please carry the required variant/style explicitly into the portal, clamp or flip right-placement menus to the viewport, and add mounted desktop/mobile portal regressions rather than only testing the pure coordinate helper. Re-request after exact-head CI is green.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6e8190b000

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread gui/src/ui.tsx Outdated
))}
</div>
)}
{dropdown && createPortal(dropdown, document.body)}

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 Badge Render modal dropdowns above the modal overlay

When a Select is used inside existing modals (for example the Dashboard update-channel select and the Models custom-context select), this portals the menu to document.body while .select-dropdown remains at --z-popover below .modal-overlay/native showModal() dialogs. Opening those selects now places the list behind the full-screen modal layer (and outside the native dialog top layer), so the options are not visible or clickable; portal into the owning modal/dialog or give the portal a modal-aware host/z-index.

Useful? React with 👍 / 👎.

Comment thread gui/src/select-position.ts Outdated
if (openAbove) {
return {
position: "fixed",
left: trigger.right + 6,

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 Badge Clamp beside dropdowns to the viewport

For placement="right" this positions the menu at trigger.right + 6 without any horizontal fit check. The language selector in App.tsx uses this placement inside the mobile drawer (width: min(280px, 84vw)), and because the dropdown is now portaled the old .sidebar .lang-toggle .select-dropdown-beside mobile override no longer applies; on narrow phones the menu starts near the drawer's right edge and extends off-screen, making some locale options hard or impossible to reach. Add a left/right fallback or clamp for beside placement.

Useful? React with 👍 / 👎.

if (flipUp) {
const style: CSSProperties = {
position: "fixed",
bottom: vh - trigger.top + FLIP_GAP_PX,

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 Badge Clear the default top offset when flipping upward

When the viewport logic chooses the flip-up path, the returned style only sets bottom, but the same element still has .select-dropdown { top: calc(100% + 4px) } (or .select-dropdown-beside { top: 0 }) from CSS because the portal class only changes position. With both top and bottom applied, dropdowns near the bottom will be laid out at the stale top offset or stretched instead of appearing above the trigger; set top: "auto" in the flipped style (and similarly clear conflicting offsets) so the upward placement actually takes effect.

Useful? React with 👍 / 👎.

Comment thread gui/src/select-position.ts Outdated
if (placement === "right") {
const spaceBelow = vh - trigger.bottom - VIEWPORT_PAD_PX;
const overflowBelow = trigger.top + measuredHeight - (vh - VIEWPORT_PAD_PX);
const openAbove = overflowBelow > 0 && overflowBelow > spaceBelow;

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 Badge Flip beside menus whenever they would overflow vertically

For placement="right", this condition can leave a menu opening downward even when its fixed top plus the enforced 120px minimum height exceeds the viewport. The added test case with top: 700, bottom: 736, and menuHeight: 120 demonstrates it: openAbove is false, so the returned style uses top: 700 and maxHeight: 120, putting the bottom past an 800px viewport. Base the decision on whether the menu fits below (or clamp to the available space) rather than comparing overflow amount to the remaining space below.

Useful? React with 👍 / 👎.

@Wibias
Wibias marked this pull request as draft July 24, 2026 07:23
Wibias and others added 3 commits July 24, 2026 12:04
…wn (#340)

Address review feedback on #393: add mounted React regressions (happy-dom)
proving (1) a portal Select renders its dropdown under document.body,
outside the clipping card, with the select-dropdown-portal marker, and
(2) a non-portal Select keeps the language-menu contract — dropdown stays
a descendant of .custom-select and retains .select-dropdown-beside so the
glass fallback and mobile upward-placement CSS still apply. The pure
coordinate helper (select-position.test.ts) already covers viewport
flip/clamp.
@lidge-jun

Copy link
Copy Markdown
Owner

Re-reviewed against @Ingwannu's CHANGES_REQUESTED — the two behavioral concerns are already resolved on the current head, and I added the missing mounted regressions (pushed to this branch as maintainer edit, 33aaa5bb):

(A) language-menu contract — portaling is opt-in (portal defaults to false in gui/src/ui.tsx; only the Claude settings selects pass portal). The language Select doesn't portal, so its dropdown stays under .custom-select/.sidebar and keeps .select-dropdown-beside, so the glass fallback and the mobile upward-placement rule still apply.

(B) right-placement overflow — computeSelectMenuStyle now compares space above/below and flips upward near the bottom (gui/src/select-position.ts); the 800px-viewport / 120px-menu case returns bottom, not top=700. The tests assert the flip rather than encoding the overflow.

(C) mounted regressions — added gui/tests/select-portal.test.tsx: (1) a portal Select renders its dropdown under document.body, outside the clipping card, with select-dropdown-portal; (2) a non-portal Select keeps its dropdown inside .custom-select with .select-dropdown-beside. The pure-coordinate viewport flip/clamp stays covered by select-position.test.ts (happy-dom has no real layout height to drive a mounted flip).

lint:gui + build:gui pass; 9 gui select tests green. Will merge once the branch CI is green.

@lidge-jun
lidge-jun marked this pull request as ready for review July 24, 2026 12:39
@lidge-jun
lidge-jun merged commit eb01dd7 into lidge-jun:dev Jul 24, 2026
9 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 33aaa5bb4b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

position: "fixed",
bottom: vh - trigger.top + FLIP_GAP_PX,
minWidth: width,
maxHeight: Math.max(MIN_MENU_HEIGHT_PX, spaceAbove - MENU_GAP_PX),

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 Badge Preserve the menu-height cap when flipping upward

When a portaled select with many options opens near the bottom, this branch can return nearly the entire space above the trigger (for example, 688px), overriding the existing 280px CSS cap and the MAX_MENU_HEIGHT_PX limit used by every other branch. The Claude auto-summary select already has 12 options, so its menu expands well beyond 280px when flipped instead of remaining consistently sized and scrollable; cap this calculation with MAX_MENU_HEIGHT_PX as the below and beside branches do.

Useful? React with 👍 / 👎.

Comment thread gui/src/ui.tsx
))}
</div>
)}
{portal ? (dropdown && createPortal(dropdown, document.body)) : dropdown}

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 Badge Move focus into portaled options when opening

When a keyboard user activates one of the new portaled Claude selects, focus remains on the trigger, while this portal places the option buttons at the end of document.body after the application root. Because Select implements neither arrow-key navigation nor programmatic option focus, pressing Tab visits every subsequent control in the page before reaching the open menu, regressing the adjacent tab order that the non-portaled dropdown had. Focus the selected or first option on open and restore the trigger on close, or implement an equivalent active-descendant keyboard interaction.

Useful? React with 👍 / 👎.

@Wibias
Wibias deleted the codex/fix-issue-340-claude-sidecar-dropdown branch July 25, 2026 07:00
agentHits pushed a commit to agentHits/opencodex that referenced this pull request Sep 17, 2026
* fix(gui): portal select dropdowns to avoid clipping

* fix(gui): scope select portals to clipped sidecar panels

* fix(gui): clamp and flip right-placed select menus

* test(gui): mounted portal/language-menu regressions for select dropdown (lidge-jun#340)

Address review feedback on lidge-jun#393: add mounted React regressions (happy-dom)
proving (1) a portal Select renders its dropdown under document.body,
outside the clipping card, with the select-dropdown-portal marker, and
(2) a non-portal Select keeps the language-menu contract — dropdown stays
a descendant of .custom-select and retains .select-dropdown-beside so the
glass fallback and mobile upward-placement CSS still apply. The pure
coordinate helper (select-position.test.ts) already covers viewport
flip/clamp.

---------

Co-authored-by: bitkyc08-arch <bitkyc08@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants