From ea5e12e8ebd9258f704b71a00434d78d7187bfce Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Thu, 25 Jun 2026 22:28:21 -0700 Subject: [PATCH] FN-7042: stop refocusing quick task input after submit Prevent successful quick task creation from stealing focus back to the quick-entry textarea. - Remove the post-submit focus restoration request/effect from QuickEntryBox.\n- Update desktop, mobile, duplicate-confirm, and image-upload tests to assert no automatic refocus.\n- Add a patch changeset documenting the quick-entry focus fix.\n\nFiles changed:\n .changeset/fn-7042-quick-entry-no-refocus.md | 7 +++\n .../dashboard/app/components/QuickEntryBox.tsx | 46 +------------------\n .../components/__tests__/QuickEntryBox.test.tsx | 53 ++++++++++++----------\n 3 files changed, 39 insertions(+), 67 deletions(-) Fusion-Task-Id: FN-7042 Fusion-Task-Lineage: 7c8e26a6-3a6b-463f-bfb7-8310682584f8 --- .changeset/fn-7042-quick-entry-no-refocus.md | 7 +++ .../app/components/QuickEntryBox.tsx | 46 +--------------- .../__tests__/QuickEntryBox.test.tsx | 53 +++++++++++-------- 3 files changed, 39 insertions(+), 67 deletions(-) create mode 100644 .changeset/fn-7042-quick-entry-no-refocus.md diff --git a/.changeset/fn-7042-quick-entry-no-refocus.md b/.changeset/fn-7042-quick-entry-no-refocus.md new file mode 100644 index 0000000000..7832ffd107 --- /dev/null +++ b/.changeset/fn-7042-quick-entry-no-refocus.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Quick task input no longer refocuses itself after you add a task. +category: fix +dev: Removed QuickEntryBox post-submit focus restoration (FNXC:QuickEntryFocus); supersedes FN-6217/FN-6219. diff --git a/packages/dashboard/app/components/QuickEntryBox.tsx b/packages/dashboard/app/components/QuickEntryBox.tsx index 4d53ae05c8..a7a988b838 100644 --- a/packages/dashboard/app/components/QuickEntryBox.tsx +++ b/packages/dashboard/app/components/QuickEntryBox.tsx @@ -18,7 +18,6 @@ import { ProviderIcon } from "./ProviderIcon"; import { WorkflowOptionalStepsDropdown } from "./WorkflowOptionalStepsDropdown"; const STORAGE_KEY = "kb-quick-entry-text"; -const MOBILE_BREAKPOINT_PX = 768; const ALLOWED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"]; interface PendingImage { @@ -111,7 +110,6 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels, return ""; }); const [isSubmitting, setIsSubmitting] = useState(false); - const [postSubmitFocusRequest, setPostSubmitFocusRequest] = useState(0); // isExpanded controls textarea height styling (auto-resize) // FNXC:QuickEntry 2026-06-22-19:25: singleLine (List view) starts collapsed so the textarea is one line, not the tall 80px variant. const [isExpanded, setIsExpanded] = useState(!singleLine); @@ -122,8 +120,6 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels, const fileInputRef = useRef(null); const touchButtonRef = useRef(null); const justResetRef = useRef(false); - const postSubmitFocusTimeoutRef = useRef | null>(null); - const handledPostSubmitFocusRequestRef = useRef(0); const previousProjectIdRef = useRef(projectId); const [pendingImages, setPendingImages] = useState([]); const pendingImagesRef = useRef([]); @@ -386,46 +382,10 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels, } }, [description, isExpanded, autoResize, singleLine]); - const requestFocusAfterSuccessfulSubmit = useCallback(() => { - setPostSubmitFocusRequest((request) => request + 1); - }, []); - /* - FNXC:QuickEntryFocus 2026-06-19-16:50: - Desktop users should keep typing after Enter, Save, or duplicate-confirmed task creation, while mobile users must not receive an automatic focus that opens the soft keyboard. - Drive the post-submit focus from a resolved-submit state request instead of a ref-gated effect so React state ordering cannot skip the restoration when the form clears under broad jsdom load. + FNXC:QuickEntryFocus 2026-06-25-00:00: + After a successful task creation, the quick-entry textarea must not re-focus itself on any surface, desktop or mobile. The user explicitly does not want focus to return to the input after adding a task, superseding FN-6217/FN-6219; keep clearing the form on submit. */ - useEffect(() => { - if ( - postSubmitFocusRequest === 0 || - handledPostSubmitFocusRequestRef.current === postSubmitFocusRequest || - isSubmitting || - description !== "" || - !textareaRef.current - ) { - return; - } - - handledPostSubmitFocusRequestRef.current = postSubmitFocusRequest; - - if (postSubmitFocusTimeoutRef.current) { - clearTimeout(postSubmitFocusTimeoutRef.current); - } - - postSubmitFocusTimeoutRef.current = setTimeout(() => { - postSubmitFocusTimeoutRef.current = null; - if (typeof window !== "undefined" && window.innerWidth > MOBILE_BREAKPOINT_PX) { - textareaRef.current?.focus(); - } - }, 0); - - return () => { - if (postSubmitFocusTimeoutRef.current) { - clearTimeout(postSubmitFocusTimeoutRef.current); - postSubmitFocusTimeoutRef.current = null; - } - }; - }, [description, isSubmitting, postSubmitFocusRequest]); // Clear dep search when dropdown closes useEffect(() => { @@ -634,7 +594,6 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels, } } resetForm(); - requestFocusAfterSuccessfulSubmit(); } catch (err) { setDescription(originalDescription); addToast(getErrorMessage(err) || t("tasks.createFailed", "Failed to create task"), "error"); @@ -667,7 +626,6 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels, projectId, addToast, resetForm, - requestFocusAfterSuccessfulSubmit, ]); const handleSubmit = useCallback(async () => { diff --git a/packages/dashboard/app/components/__tests__/QuickEntryBox.test.tsx b/packages/dashboard/app/components/__tests__/QuickEntryBox.test.tsx index 479ea86702..c51ce5cb39 100644 --- a/packages/dashboard/app/components/__tests__/QuickEntryBox.test.tsx +++ b/packages/dashboard/app/components/__tests__/QuickEntryBox.test.tsx @@ -560,7 +560,7 @@ describe("QuickEntryBox", () => { }); }); - describe("post-submission focus restoration (FN-6217)", () => { + describe("post-submission focus behavior (FN-7042: no refocus)", () => { it("does not auto-focus the quick-entry textarea on empty desktop mount", async () => { mockDesktopViewport(); renderQuickEntryBox({}); @@ -602,7 +602,7 @@ describe("QuickEntryBox", () => { expect(document.activeElement).not.toBe(textarea); }); - it("focuses the quick-entry textarea after a successful Enter submission on desktop", async () => { + it("does not refocus the quick-entry textarea after a successful Enter submission on desktop", async () => { mockDesktopViewport(); const onCreate = vi.fn().mockResolvedValue(CREATED_TASK); renderQuickEntryBox({ onCreate }); @@ -616,11 +616,12 @@ describe("QuickEntryBox", () => { await waitForSubmitSuccessToClear(textarea); await flushPendingTimers(); - expect(focusSpy).toHaveBeenCalledTimes(1); - expect(document.activeElement).toBe(textarea); + // FN-7042 reverses FN-6217/FN-6219: successful creation clears the draft but never steals focus back. + expect(focusSpy).not.toHaveBeenCalled(); + expect(document.activeElement).not.toBe(textarea); }); - it("focuses the quick-entry textarea after a successful Save-button submission on desktop", async () => { + it("does not refocus the quick-entry textarea after a successful Save-button submission on desktop", async () => { mockDesktopViewport(); const onCreate = vi.fn().mockResolvedValue(CREATED_TASK); renderQuickEntryBox({ onCreate }); @@ -634,11 +635,11 @@ describe("QuickEntryBox", () => { await waitForSubmitSuccessToClear(textarea); await flushPendingTimers(); - expect(focusSpy).toHaveBeenCalledTimes(1); - expect(document.activeElement).toBe(textarea); + expect(focusSpy).not.toHaveBeenCalled(); + expect(document.activeElement).not.toBe(textarea); }); - it("focuses the quick-entry textarea only after duplicate-confirmed creation completes on desktop", async () => { + it("does not refocus the quick-entry textarea after duplicate-confirmed creation completes on desktop", async () => { mockDesktopViewport(); const onCreate = vi.fn().mockResolvedValue(CREATED_TASK); vi.mocked(checkDuplicateTasks).mockResolvedValueOnce([ @@ -660,8 +661,8 @@ describe("QuickEntryBox", () => { await waitForSubmitSuccessToClear(textarea); await flushPendingTimers(); - expect(focusSpy).toHaveBeenCalledTimes(1); - expect(document.activeElement).toBe(textarea); + expect(focusSpy).not.toHaveBeenCalled(); + expect(document.activeElement).not.toBe(textarea); }); it("never auto-focuses the quick-entry textarea on mobile, including after a successful submission", async () => { @@ -1557,25 +1558,25 @@ describe("QuickEntryBox", () => { }); }); - it("maintains focus after successful creation", async () => { + it("does not refocus after successful creation", async () => { const { props } = renderQuickEntryBox({}); - const textarea = screen.getByTestId("quick-entry-input"); + const textarea = screen.getByTestId("quick-entry-input") as HTMLTextAreaElement; + const focusSpy = vi.spyOn(textarea, "focus"); - fireEvent.focus(textarea); fireEvent.change(textarea, { target: { value: "Task to create" } }); fireEvent.keyDown(textarea, { key: "Enter" }); await waitFor(() => { expect(props.onCreate).toHaveBeenCalled(); }); + await waitFor(() => expect(textarea.value).toBe("")); + await flushPendingTimers(); - // Focus restoration happens after submit state clears - await waitFor(() => { - expect(document.activeElement).toBe(textarea); - }); + expect(focusSpy).not.toHaveBeenCalled(); + expect(document.activeElement).not.toBe(textarea); }); - it("does not restore focus after successful creation at mobile width", async () => { + it("does not refocus after successful creation at mobile width", async () => { const innerWidthSpy = vi.spyOn(window, "innerWidth", "get").mockReturnValue(375); const { props } = renderQuickEntryBox({}); const textarea = screen.getByTestId("quick-entry-input"); @@ -2939,12 +2940,13 @@ describe("QuickEntryBox", () => { expect(screen.queryByAltText("remove.png")).toBeNull(); }); - it("uploads each pending image after task creation", async () => { + it("uploads each pending image after task creation without refocusing", async () => { const onCreate = vi.fn().mockResolvedValue(CREATED_TASK); renderQuickEntryBox({ onCreate }); expandQuickEntry(); - const textarea = screen.getByTestId("quick-entry-input"); + const textarea = screen.getByTestId("quick-entry-input") as HTMLTextAreaElement; + const focusSpy = vi.spyOn(textarea, "focus"); const fileInput = screen.getByTestId("quick-entry-file-input") as HTMLInputElement; const fileA = new File(["a"], "a.png", { type: "image/png" }); const fileB = new File(["b"], "b.png", { type: "image/png" }); @@ -2960,6 +2962,8 @@ describe("QuickEntryBox", () => { expect(uploadAttachment).toHaveBeenCalledWith(CREATED_TASK.id, fileA, TEST_PROJECT_ID); expect(uploadAttachment).toHaveBeenCalledWith(CREATED_TASK.id, fileB, TEST_PROJECT_ID); + expect(focusSpy).not.toHaveBeenCalled(); + expect(document.activeElement).not.toBe(textarea); }); it("does not upload attachments when no pending images exist", async () => { @@ -3043,9 +3047,10 @@ describe("QuickEntryBox", () => { expect(controls?.hasAttribute("hidden")).toBe(true); }); - it("after task creation with autoExpand, focus restore preserves visible controls", async () => { + it("after task creation with autoExpand, no refocus still preserves visible controls", async () => { const { props } = renderQuickEntryBox(); - const textarea = screen.getByTestId("quick-entry-input"); + const textarea = screen.getByTestId("quick-entry-input") as HTMLTextAreaElement; + const focusSpy = vi.spyOn(textarea, "focus"); const controls = document.getElementById("quick-entry-controls"); // Type and submit without collapsing disclosure. @@ -3056,10 +3061,12 @@ describe("QuickEntryBox", () => { expect(props.onCreate).toHaveBeenCalled(); }); - // After creation, focus is restored asynchronously and visible controls remain visible. + // FN-7042: creation no longer restores focus; visible controls should remain visible anyway. await waitFor(() => { expect(textarea.classList.contains("quick-entry-input--expanded")).toBe(true); }); + expect(focusSpy).not.toHaveBeenCalled(); + expect(document.activeElement).not.toBe(textarea); expect(controls?.hasAttribute("hidden")).toBe(false); });