From b5f62751f94588a27a72ddc9be2ca98f5f449ee6 Mon Sep 17 00:00:00 2001 From: Ali Madad Date: Sun, 26 Jul 2026 15:46:48 +0000 Subject: [PATCH] fix: keep mobile submit ahead of keyboard dismissal --- .../promptbox/PromptBoxInternal.test.tsx | 28 +++++++++++++++++++ .../promptbox/PromptBoxInternal.tsx | 23 +++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/apps/app/src/components/promptbox/PromptBoxInternal.test.tsx b/apps/app/src/components/promptbox/PromptBoxInternal.test.tsx index f59ddfe36..918659e25 100644 --- a/apps/app/src/components/promptbox/PromptBoxInternal.test.tsx +++ b/apps/app/src/components/promptbox/PromptBoxInternal.test.tsx @@ -1601,6 +1601,34 @@ describe("PromptBoxInternal compact layout", () => { ).toBe(true); }); + it("keeps the editor focused through a pointer submit", async () => { + const onSubmit = vi.fn(); + render( + , + ); + + await waitForPromptFocus(); + const editor = getPromptEditorElement(); + const submit = screen.getByRole("button", { name: "Submit (Enter)" }); + + expect( + fireEvent.pointerDown(submit, { button: 0, pointerType: "touch" }), + ).toBe(false); + expect(document.activeElement).toBe(editor); + + fireEvent.click(submit); + expect(onSubmit).toHaveBeenCalledOnce(); + }); + it("keeps all Markdown and mention text in the navigable preview", async () => { const mentionToken = "@apps/app/src/components/promptbox/PromptBoxInternal.tsx"; diff --git a/apps/app/src/components/promptbox/PromptBoxInternal.tsx b/apps/app/src/components/promptbox/PromptBoxInternal.tsx index 300c52836..7bc21c333 100644 --- a/apps/app/src/components/promptbox/PromptBoxInternal.tsx +++ b/apps/app/src/components/promptbox/PromptBoxInternal.tsx @@ -19,6 +19,7 @@ import { type ChangeEvent, type FormEvent, type MouseEvent as ReactMouseEvent, + type PointerEvent as ReactPointerEvent, type ReactNode, type Ref, } from "react"; @@ -2319,6 +2320,27 @@ export function PromptBoxInternal({ resetZenModeAfterSubmit(); }, [canSubmit, onSubmit, resetZenModeAfterSubmit]); + const handleSubmitPointerDown = useCallback( + (event: ReactPointerEvent) => { + if (event.button !== 0) return; + const currentEditor = editorRef.current; + if ( + !currentEditor || + currentEditor.isDestroyed || + !currentEditor.isFocused + ) { + return; + } + + // Focus transfer happens before click. On iOS, moving focus from the + // editor to this button begins keyboard dismissal and resizes the app + // shell before the form can submit. Keep the editor focused; the click + // still owns the commit, while genuine outside focus dismisses normally. + event.preventDefault(); + }, + [], + ); + // A no-argument built-in command (currently only `/compact`) is a complete // action the moment it is selected, so applying it with Enter should also // submit instead of leaving the pill parked for a second Enter. The submit is @@ -3028,6 +3050,7 @@ export function PromptBoxInternal({ variant="default" aria-label={effectiveSubmitTitle} disabled={!canSubmit} + onPointerDown={handleSubmitPointerDown} className={cn( showCompactLayout ? COMPACT_PROMPT_ACTION_BUTTON_CLASS