diff --git a/packages/views/projects/components/project-picker.open-state.test.tsx b/packages/views/projects/components/project-picker.open-state.test.tsx
new file mode 100644
index 000000000..331b7ded5
--- /dev/null
+++ b/packages/views/projects/components/project-picker.open-state.test.tsx
@@ -0,0 +1,95 @@
+// Regression test: selecting a project in the create-issue
+// dialog left the dropdown stuck open. The dialog wires the picker with
+// `open={cond ? true : undefined}`; Base UI's Menu latches a controlled
+// `open={true}` and does NOT treat a later `undefined` as "close", so the
+// picker must normalize to an always-boolean controlled value. This test
+// uses the REAL dropdown-menu (Base UI) — do not mock it here.
+import { useState } from "react";
+import { describe, expect, it, vi } from "vitest";
+import { render, screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { I18nProvider } from "@multica/core/i18n/react";
+import enProjects from "../../locales/en/projects.json";
+import { ProjectPicker } from "./project-picker";
+import { PillButton } from "../../common/pill-button";
+
+vi.mock("@tanstack/react-query", () => ({
+ useQuery: () => ({
+ data: [
+ { id: "project-1", title: "Launch Command Center", icon: null },
+ { id: "project-2", title: "Mobile Web", icon: null },
+ ],
+ }),
+}));
+
+vi.mock("@multica/core/hooks", () => ({
+ useWorkspaceId: () => "workspace-1",
+}));
+
+vi.mock("@multica/core/projects/queries", () => ({
+ projectListOptions: () => ({ queryKey: ["projects"] }),
+}));
+
+vi.mock("./project-icon", () => ({
+ ProjectIcon: () => ,
+}));
+
+/** Mirrors the create-issue dialog wiring from packages/views/modals/create-issue.tsx. */
+function CreateDialogHarness({ onUpdate }: { onUpdate: (u: object) => void }) {
+ const [fieldPickerOpen, setFieldPickerOpen] = useState<"project" | null>(null);
+ const [projectId, setProjectId] = useState(null);
+ return (
+
+ {
+ onUpdate(u);
+ setProjectId(u.project_id ?? null);
+ }}
+ triggerRender={}
+ align="start"
+ open={fieldPickerOpen === "project" ? true : undefined}
+ onOpenChange={(open) => setFieldPickerOpen(open ? "project" : null)}
+ />
+
+ );
+}
+
+describe("ProjectPicker open state under create-dialog wiring", () => {
+ it("closes the dropdown after selecting a project", async () => {
+ const user = userEvent.setup();
+ const onUpdate = vi.fn();
+
+ render();
+
+ // Open the picker via its trigger.
+ await user.click(screen.getByRole("button", { name: /no project/i }));
+ const item = await screen.findByRole("menuitem", { name: /mobile web/i });
+
+ // Select a project — the selection must register AND the popup must close.
+ await user.click(item);
+ expect(onUpdate).toHaveBeenCalledWith({ project_id: "project-2" });
+ await waitFor(() => {
+ expect(screen.queryByRole("menuitem", { name: /mobile web/i })).not.toBeInTheDocument();
+ });
+ });
+
+ it("can be reopened and closed again after a selection", async () => {
+ const user = userEvent.setup();
+
+ render();
+
+ await user.click(screen.getByRole("button", { name: /no project/i }));
+ await user.click(await screen.findByRole("menuitem", { name: /launch command center/i }));
+ await waitFor(() => {
+ expect(screen.queryByRole("menuitem", { name: /launch command center/i })).not.toBeInTheDocument();
+ });
+
+ // Reopen from the (now selected) trigger and close by selecting again.
+ await user.click(screen.getByRole("button", { name: /launch command center/i }));
+ await user.click(await screen.findByRole("menuitem", { name: /mobile web/i }));
+ await waitFor(() => {
+ expect(screen.queryByRole("menuitem", { name: /mobile web/i })).not.toBeInTheDocument();
+ });
+ });
+});
diff --git a/packages/views/projects/components/project-picker.tsx b/packages/views/projects/components/project-picker.tsx
index 9020f9d8c..5096229a0 100644
--- a/packages/views/projects/components/project-picker.tsx
+++ b/packages/views/projects/components/project-picker.tsx
@@ -1,5 +1,6 @@
"use client";
+import { useState } from "react";
import { Check, FolderKanban, X } from "lucide-react";
import { useQuery } from "@tanstack/react-query";
import { projectListOptions } from "@multica/core/projects/queries";
@@ -21,7 +22,7 @@ export function ProjectPicker({
triggerRender,
align = "start",
defaultOpen = false,
- open,
+ open: controlledOpen,
onOpenChange,
}: {
projectId: string | null;
@@ -38,9 +39,17 @@ export function ProjectPicker({
const wsId = useWorkspaceId();
const { data: projects = [] } = useQuery(projectListOptions(wsId));
const current = projects.find((p) => p.id === projectId);
+ // Normalize to an always-boolean controlled `open`, matching the other
+ // pickers (status/priority/assignee/labels). Base UI's Menu latches a
+ // controlled `open={true}` — a later `undefined` does NOT close it — so
+ // callers wiring `open={cond ? true : undefined}` (create-issue dialog)
+ // would leave the popup stuck open after selecting a project.
+ const [internalOpen, setInternalOpen] = useState(defaultOpen);
+ const open = controlledOpen ?? internalOpen;
+ const setOpen = onOpenChange ?? setInternalOpen;
return (
-
+