- {open && (
-
- {options.map(o => (
-
- ))}
-
- )}
+ {portal ? (dropdown && createPortal(dropdown, document.body)) : dropdown}
);
}
diff --git a/gui/tests/select-portal.test.tsx b/gui/tests/select-portal.test.tsx
new file mode 100644
index 00000000000..2bf9db98442
--- /dev/null
+++ b/gui/tests/select-portal.test.tsx
@@ -0,0 +1,83 @@
+import { afterEach, beforeEach, expect, test } from "bun:test";
+import { Window } from "happy-dom";
+import { act } from "react";
+import type { Root } from "react-dom/client";
+import { Select } from "../src/ui";
+
+// Mounted regressions for #340 / PR #393: the portal fix must (1) render an opt-in portaled
+// dropdown under document.body (outside the clipping card) with fixed positioning, and (2) leave a
+// NON-portal Select (the language menu contract) as a descendant of .custom-select keeping the
+// .select-dropdown-beside class so its glass fallback + mobile upward-placement CSS still apply.
+
+const globals = ["document", "window", "navigator", "IS_REACT_ACT_ENVIRONMENT"] as const;
+let previousGlobals: Record<(typeof globals)[number], unknown>;
+let testWindow: Window;
+
+beforeEach(() => {
+ previousGlobals = Object.fromEntries(globals.map((key) => [key, Reflect.get(globalThis, key)])) as typeof previousGlobals;
+ testWindow = new Window({ url: "http://localhost/" });
+ Object.defineProperties(globalThis, {
+ document: { configurable: true, value: testWindow.document },
+ window: { configurable: true, value: testWindow },
+ navigator: { configurable: true, value: testWindow.navigator },
+ });
+ (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
+});
+
+afterEach(() => {
+ testWindow.close();
+ for (const key of globals) {
+ Object.defineProperty(globalThis, key, { configurable: true, value: previousGlobals[key] });
+ }
+});
+
+const OPTIONS = [
+ { value: "a", label: "Alpha" },
+ { value: "b", label: "Beta" },
+];
+
+async function mountAndOpen(node: React.ReactElement): Promise<{ container: HTMLElement; root: Root }> {
+ const { createRoot } = await import("react-dom/client");
+ // A bounded, clipping card the Claude settings dropdowns live inside.
+ const card = document.createElement("div");
+ card.className = "settings-card";
+ card.style.overflow = "hidden";
+ document.body.append(card);
+ let root!: Root;
+ await act(async () => {
+ root = createRoot(card);
+ root.render(node);
+ });
+ // Open the dropdown by clicking the trigger.
+ const trigger = card.querySelector