fix(ui): keep slash autocomplete scrollable in dialogs (#11222)
<!-- Write all pull request text in Simplified Technical English (ASD-STE100): short sentences, one instruction per sentence, simple approved vocabulary, and the active voice. --> ## Thinking Path > - Paperclip helps operators manage AI-agent companies. > - Operators create tasks and comments through shared rich-text editors. > - These editors show slash-command and mention matches in a floating menu. > - Modal dialogs treat that body-level menu as outside content and cancel its wheel and touch movement. > - This pull request keeps scroll events inside the floating menu and preserves native scrolling. > - The benefit is that operators can reach every match with a mouse wheel, a trackpad, or a touch screen. ## Linked Issues or Issue Description No public GitHub issue exists for this bug. **What happened?** Slash-command and mention menus could contain more matches than their visible height. When an editor was inside a modal dialog, the modal scroll lock canceled wheel and touch movement on the body-level menu portal. Operators could not scroll to later matches. **Expected behavior** The autocomplete menu must scroll with a mouse wheel, a two-finger trackpad gesture, and a vertical touch gesture. Keyboard selection and normal editor behavior must stay unchanged. **Steps to reproduce** 1. Open a task or comment editor inside a modal dialog. 2. Enter a slash command or mention query that has more matches than the menu can show. 3. Try to scroll the menu with a wheel, trackpad, or touch gesture. **Paperclip version or commit** `7ea2068ef8` on `master`. **Deployment mode** Local development UI built from source. ## What Changed - Keep wheel and touch movement inside the shared autocomplete menu portal. - Add vertical overscroll containment while preserving native momentum scrolling. - Add a regression test that mounts the real dialog and verifies that wheel and touch movement stay uncanceled. ## Verification - `pnpm --dir ui exec vitest run src/components/MarkdownEditor.test.tsx` - `pnpm check:token-gates` - `pnpm -r typecheck` - `env -u AWS_ACCESS_KEY_ID -u AWS_SECRET_ACCESS_KEY pnpm test:run` - `pnpm build` The two AWS variables are omitted from the full test command because this agent runtime injects static AWS credentials. One unrelated CLI doctor test correctly warns when those credentials are present. The CI environment does not inject them. ## Risks - Low risk. Event propagation stops only on the open autocomplete menu portal. - Ancestor listeners no longer receive wheel or touch movement from that menu. Native menu scrolling and option-level touch handling still receive the events. > 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 with model ID `gpt-5`. The deployment suffix and context-window size are not exposed to the agent. The model used agentic reasoning, repository tools, GitHub tools, and local code execution. ## 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 searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
3e1ea39ff3
commit
7734f4b32d
|
|
@ -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(
|
||||
<Dialog open>
|
||||
<DialogContent>
|
||||
<DialogTitle>Create task</DialogTitle>
|
||||
<MarkdownEditor value="@Pap" onChange={() => {}} mentions={mentions} />
|
||||
</DialogContent>
|
||||
</Dialog>,
|
||||
);
|
||||
});
|
||||
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) => ({
|
||||
|
|
|
|||
|
|
@ -1392,13 +1392,23 @@ export const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>
|
|||
<div
|
||||
data-paperclip-floating-ui=""
|
||||
data-testid="mention-autocomplete-menu"
|
||||
className="pointer-events-auto fixed z-(--z-9999) min-w-(--sz-180px) max-w-(--sz-calc-15) max-h-(--sz-208px) overflow-y-auto rounded-md border border-border bg-popover shadow-md"
|
||||
className="pointer-events-auto fixed z-(--z-9999) min-w-(--sz-180px) max-w-(--sz-calc-15) max-h-(--sz-208px) overflow-y-auto overscroll-contain rounded-md border border-border bg-popover shadow-md"
|
||||
style={{
|
||||
top: mentionMenuPosition.top,
|
||||
left: mentionMenuPosition.left,
|
||||
touchAction: "pan-y",
|
||||
WebkitOverflowScrolling: "touch",
|
||||
}}
|
||||
onWheelCapture={(event) => {
|
||||
// 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) => (
|
||||
<button
|
||||
|
|
|
|||
Loading…
Reference in New Issue