diff --git a/ui/src/components/MarkdownEditor.test.tsx b/ui/src/components/MarkdownEditor.test.tsx index 5787060965..ebe0de0de2 100644 --- a/ui/src/components/MarkdownEditor.test.tsx +++ b/ui/src/components/MarkdownEditor.test.tsx @@ -15,6 +15,7 @@ import { placeCaretAfterMentionAnchor, shouldAcceptAutocompleteKey, } from "./MarkdownEditor"; +import { Dialog, DialogContent, DialogTitle } from "./ui/dialog"; const mdxEditorMockState = vi.hoisted(() => ({ emitMountEmptyReset: false, @@ -1029,6 +1030,64 @@ describe("MarkdownEditor", () => { }); }); + it("lets wheel and touch scrolling reach the autocomplete menu inside a modal", async () => { + const root = createRoot(container); + const mentions = Array.from({ length: 12 }, (_, index) => ({ + id: `project:project-${index}`, + kind: "project" as const, + name: `Paperclip App ${index}`, + projectId: `project-${index}`, + projectColor: "#336699", + })); + + await act(async () => { + root.render( + + + Create task + {}} mentions={mentions} /> + + , + ); + }); + await flush(); + + const editable = document.body.querySelector('[data-testid="mdx-editor"]'); + const textNode = editable?.firstChild; + expect(textNode?.nodeType).toBe(Node.TEXT_NODE); + + const selection = window.getSelection(); + const range = document.createRange(); + range.setStart(textNode!, "@Pap".length); + range.collapse(true); + selection?.removeAllRanges(); + selection?.addRange(range); + + act(() => { + document.dispatchEvent(new Event("selectionchange")); + }); + await flush(); + + const menu = document.body.querySelector('[data-testid="mention-autocomplete-menu"]'); + expect(menu).toBeTruthy(); + + const wheel = new WheelEvent("wheel", { bubbles: true, cancelable: true, deltaY: 80 }); + act(() => { + menu?.dispatchEvent(wheel); + }); + expect(wheel.defaultPrevented).toBe(false); + + const touchMove = createTouchEvent("touchmove", [{ clientX: 100, clientY: 90 }]); + act(() => { + menu?.firstElementChild?.dispatchEvent(touchMove); + }); + expect(touchMove.defaultPrevented).toBe(false); + + await act(async () => { + root.unmount(); + }); + }); + it("caps rendered mention matches while keeping the menu scrollable", async () => { const handleChange = vi.fn(); const mentions = Array.from({ length: 60 }, (_, index) => ({ diff --git a/ui/src/components/MarkdownEditor.tsx b/ui/src/components/MarkdownEditor.tsx index 9d7dc7415b..897097e9fb 100644 --- a/ui/src/components/MarkdownEditor.tsx +++ b/ui/src/components/MarkdownEditor.tsx @@ -1392,13 +1392,23 @@ export const MarkdownEditor = forwardRef
{ + // Modal scroll locks treat this body-level portal as outside the + // dialog. Keep wheel input on the menu so the lock cannot cancel it. + event.stopPropagation(); + }} + onTouchMove={(event) => { + // Let the touched option observe movement first, then keep the + // native event from reaching a modal's document-level scroll lock. + event.stopPropagation(); + }} > {filteredMentions.map((option, i) => (