From a07e6cef7b64c97bf3fdb86fa57c9f7e8dc051c0 Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Mon, 18 May 2026 14:28:49 -0500 Subject: [PATCH] [codex] Fix new issue autocomplete pointer selection (#6311) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies. > - Human operators create and edit issues through modal-heavy board UI workflows. > - The new-issue dialog embeds markdown editors that render autocomplete menus through body-level portals. > - Radix Dialog treats those portal clicks as outside-dialog pointer events and prevents their default behavior. > - That prevention made completion items hard or impossible to select from inside the new-issue dialog. > - This pull request marks the markdown editor floating autocomplete menu as allowed dialog-external UI and extends the dialog outside-pointer handler to preserve those interactions. > - The benefit is that users can click/tap autocomplete completions while keeping the existing modal behavior intact. ## What Changed - Added a stable `data-paperclip-floating-ui` marker and explicit pointer event handling to the markdown editor mention/autocomplete portal. - Updated the new issue dialog outside-pointer guard so editor autocomplete portals are handled like Radix popover portals. - Added regression coverage for markdown editor portal markup and new issue dialog completion selection behavior. ## Verification - `pnpm exec vitest run ui/src/components/MarkdownEditor.test.tsx ui/src/components/NewIssueDialog.test.tsx` passed: 2 files, 38 tests. - Confirmed the branch is rebased onto current `public-gh/master` before opening this PR. - Confirmed the diff does not include `pnpm-lock.yaml` or `.github/workflows` changes. ## Risks - Low risk. The change is scoped to allowing pointer events from known body-level UI portals while keeping other outside-dialog pointer events under Radix Dialog control. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - OpenAI Codex, GPT-5 coding agent with repository tool use and local command execution. Exact hosted context window is not surfaced in this runtime. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] If this change affects the UI, I have included before/after screenshots - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge Screenshot note: this is an interaction/event-handling fix with no visible UI change; verification is covered by the focused regression tests above. Co-authored-by: Paperclip --- ui/src/components/MarkdownEditor.test.tsx | 13 ++++++++ ui/src/components/MarkdownEditor.tsx | 3 +- ui/src/components/NewIssueDialog.test.tsx | 36 +++++++++++++++++++++-- ui/src/components/NewIssueDialog.tsx | 10 +++---- 4 files changed, 54 insertions(+), 8 deletions(-) diff --git a/ui/src/components/MarkdownEditor.test.tsx b/ui/src/components/MarkdownEditor.test.tsx index 0eb67618..0d5f1cad 100644 --- a/ui/src/components/MarkdownEditor.test.tsx +++ b/ui/src/components/MarkdownEditor.test.tsx @@ -753,6 +753,19 @@ describe("MarkdownEditor", () => { }); }); + it("marks the autocomplete portal as floating UI for modal pointer handling", async () => { + const handleChange = vi.fn(); + const { option, root } = await openMentionMenuFor(handleChange); + + const menu = option.closest("[data-paperclip-floating-ui]"); + expect(menu).toBeTruthy(); + expect(menu?.className).toContain("pointer-events-auto"); + + await act(async () => { + root.unmount(); + }); + }); + it("does not preventDefault on touchstart so the mention menu can scroll on mobile", async () => { const handleChange = vi.fn(); const { option, root } = await openMentionMenuFor(handleChange); diff --git a/ui/src/components/MarkdownEditor.tsx b/ui/src/components/MarkdownEditor.tsx index 0a0c5067..78ae521a 100644 --- a/ui/src/components/MarkdownEditor.tsx +++ b/ui/src/components/MarkdownEditor.tsx @@ -1241,7 +1241,8 @@ export const MarkdownEditor = forwardRef {mentionActive && filteredMentions.length > 0 && mentionMenuPosition && createPortal(
({ closeNewIssue: vi.fn(), })); +const dialogContentState = vi.hoisted(() => ({ + onPointerDownOutside: null as null | ((event: { + detail: { originalEvent: { target: EventTarget | null } }; + preventDefault: () => void; + }) => void), +})); + const companyState = vi.hoisted(() => ({ companies: [ { @@ -186,13 +193,16 @@ vi.mock("@/components/ui/dialog", () => ({ children, showCloseButton: _showCloseButton, onEscapeKeyDown: _onEscapeKeyDown, - onPointerDownOutside: _onPointerDownOutside, + onPointerDownOutside, ...props }: ComponentProps<"div"> & { showCloseButton?: boolean; onEscapeKeyDown?: (event: unknown) => void; onPointerDownOutside?: (event: unknown) => void; - }) =>
{children}
, + }) => { + dialogContentState.onPointerDownOutside = onPointerDownOutside as typeof dialogContentState.onPointerDownOutside; + return
{children}
; + }, })); vi.mock("@/components/ui/button", () => ({ @@ -285,6 +295,7 @@ describe("NewIssueDialog", () => { dialogState.newIssueOpen = true; dialogState.newIssueDefaults = {}; dialogState.closeNewIssue.mockReset(); + dialogContentState.onPointerDownOutside = null; toastState.pushToast.mockReset(); mockIssuesApi.create.mockReset(); mockIssuesApi.upsertDocument.mockReset(); @@ -729,6 +740,27 @@ describe("NewIssueDialog", () => { act(() => root.unmount()); }); + it("allows editor autocomplete portal pointer events inside the modal", async () => { + const { root } = renderDialog(container); + await flush(); + + const menu = document.createElement("div"); + menu.setAttribute("data-paperclip-floating-ui", ""); + const option = document.createElement("button"); + menu.appendChild(option); + document.body.appendChild(menu); + const preventDefault = vi.fn(); + + dialogContentState.onPointerDownOutside?.({ + detail: { originalEvent: { target: option } }, + preventDefault, + }); + + expect(preventDefault).toHaveBeenCalledTimes(1); + + act(() => root.unmount()); + }); + it("warns when a sub-issue stops matching the parent workspace", async () => { mockProjectsApi.list.mockResolvedValue([ { diff --git a/ui/src/components/NewIssueDialog.tsx b/ui/src/components/NewIssueDialog.tsx index 1ff5e772..4de20971 100644 --- a/ui/src/components/NewIssueDialog.tsx +++ b/ui/src/components/NewIssueDialog.tsx @@ -1221,12 +1221,12 @@ export function NewIssueDialog() { } // Radix Dialog's modal DismissableLayer calls preventDefault() on // pointerdown events that originate outside the Dialog DOM tree. - // Popover portals render at the body level (outside the Dialog), so - // touch events on popover content get their default prevented — which - // kills scroll gesture recognition on mobile. Telling Radix "this - // event is handled" skips that preventDefault, restoring touch scroll. + // Popover and editor autocomplete portals render at the body level + // (outside the Dialog), so touch/click events on their content get + // their default prevented. Telling Radix "this event is handled" skips + // that preventDefault, restoring popover scroll and autocomplete taps. const target = event.detail.originalEvent.target as HTMLElement | null; - if (target?.closest("[data-radix-popper-content-wrapper]")) { + if (target?.closest("[data-radix-popper-content-wrapper], [data-paperclip-floating-ui]")) { event.preventDefault(); } }}