FN-6122: prevent quick-entry buttons from refocusing textarea

Keep quick-entry textarea focus stable while action buttons are clicked.

- prevent mousedown on refine, deps, attach, models, node, agent, and priority controls from stealing textarea focus
- add quick-entry focus regression coverage for individual controls and the full action row
- add a patch changeset for the quick-entry focus behavior fix

Files changed:
 .changeset/fn-6122-quick-entry-focus.md            |  5 ++
 .../dashboard/app/components/QuickEntryBox.tsx     |  7 ++
 .../components/__tests__/QuickEntryBox.test.tsx    | 87 ++++++++++++++++++++++
 3 files changed, 99 insertions(+)

Fusion-Task-Id: FN-6122

Fusion-Task-Lineage: 2a8f876d-f387-4b04-bd1e-0bf79938707e
This commit is contained in:
gsxdsm
2026-06-09 13:40:24 -07:00
parent 1315bc0fc3
commit 7c4e44d6a1
3 changed files with 99 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Prevent QuickEntry quick-action buttons from stealing or restoring textarea focus on mouse down, preserving existing click behavior while avoiding unwanted mobile keyboard refocus.

View File

@@ -1507,6 +1507,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
<button
type="button"
className={`btn btn-sm refine-button ${isRefining ? "refine-button--loading" : ""}`}
onMouseDown={(e) => e.preventDefault()}
onClick={() => {
setIsRefineMenuOpen((prev) => {
const next = !prev;
@@ -1578,6 +1579,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
<button
ref={depTriggerRef}
type="button"
onMouseDown={(e) => e.preventDefault()}
className="btn btn-sm dep-trigger"
data-testid="quick-entry-deps"
onClick={() => {
@@ -1667,6 +1669,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
<button
type="button"
onMouseDown={(e) => e.preventDefault()}
className="btn btn-sm"
data-testid="quick-entry-attach"
onClick={() => fileInputRef.current?.click()}
@@ -1678,6 +1681,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
<button
ref={modelTriggerRef}
type="button"
onMouseDown={(e) => e.preventDefault()}
className="btn btn-sm"
data-testid="quick-entry-models"
onClick={() => {
@@ -1700,6 +1704,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
<div className="node-trigger-wrap" ref={nodePickerRef}>
<button
type="button"
onMouseDown={(e) => e.preventDefault()}
className="btn btn-sm dep-trigger"
data-testid="quick-entry-node-button"
onClick={() => {
@@ -1788,6 +1793,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
<div className="agent-trigger-wrap" ref={agentPickerRef}>
<button
type="button"
onMouseDown={(e) => e.preventDefault()}
className="btn btn-sm dep-trigger"
onClick={() => {
if (showAgentPicker) {
@@ -1862,6 +1868,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
<div className="priority-trigger-wrap" ref={priorityPickerRef}>
<button
type="button"
onMouseDown={(e) => e.preventDefault()}
className="btn btn-sm dep-trigger"
data-testid="quick-entry-priority-button"
onClick={() => {

View File

@@ -323,6 +323,93 @@ describe("QuickEntryBox", () => {
innerWidthSpy.mockRestore();
});
describe("button focus preservation (FN-6122)", () => {
const newlyCoveredActionButtons = [
["Deps", "quick-entry-deps"],
["Attach", "quick-entry-attach"],
["Models", "quick-entry-models"],
["Node", "quick-entry-node-button"],
["Agent", "quick-entry-agent-button"],
["Priority", "quick-entry-priority-button"],
] as const;
const allActionButtons = [
["Plan", "plan-button"],
["Subtask", "subtask-button"],
["Refine", "refine-button"],
...newlyCoveredActionButtons,
["Fast", "quick-entry-fast-toggle"],
["GitHub", "quick-entry-github-toggle"],
["Save", "quick-entry-save"],
] as const;
function focusTextareaWithValue(value: string) {
const textarea = screen.getByTestId("quick-entry-input") as HTMLTextAreaElement;
textarea.focus();
fireEvent.focus(textarea);
fireEvent.change(textarea, { target: { value } });
textarea.focus();
expect(document.activeElement).toBe(textarea);
return textarea;
}
async function clickActionButtonWithoutStealingFocus(
button: HTMLElement,
textarea: HTMLElement,
{ allowsPortalAutoFocus = false }: { allowsPortalAutoFocus?: boolean } = {},
) {
expect(fireEvent.mouseDown(button)).toBe(false);
expect(document.activeElement).toBe(textarea);
await act(async () => {
fireEvent.click(button);
});
if (allowsPortalAutoFocus) {
expect(document.activeElement).not.toBe(button);
return;
}
expect(document.activeElement).toBe(textarea);
}
it.each(newlyCoveredActionButtons)("keeps textarea focused when clicking %s", async (_label, testId) => {
mockDesktopViewport();
renderQuickEntryBox({});
expandQuickEntry();
const textarea = focusTextareaWithValue("Focus-preserving quick-entry action");
await clickActionButtonWithoutStealingFocus(screen.getByTestId(testId), textarea, {
allowsPortalAutoFocus: testId === "quick-entry-deps",
});
});
it("keeps textarea focused for every quick-entry action button", async () => {
mockDesktopViewport();
vi.mocked(fetchSettings).mockResolvedValueOnce({
githubTrackingEnabledByDefault: true,
} as any);
renderQuickEntryBox({});
expandQuickEntry();
await waitFor(() => {
expect(screen.getByTestId("quick-entry-github-toggle")).not.toBeDisabled();
});
const actionsContainer = screen.getByTestId("quick-entry-actions");
for (const [_label, testId] of allActionButtons) {
const textarea = focusTextareaWithValue(`Focus preserved for ${testId}`);
const button = screen.getByTestId(testId);
expect(actionsContainer.contains(button)).toBe(true);
await clickActionButtonWithoutStealingFocus(button, textarea, {
allowsPortalAutoFocus: testId === "quick-entry-deps",
});
if (testId === "quick-entry-save") {
await waitFor(() => {
expect(document.activeElement).toBe(textarea);
});
}
}
});
});
it("textarea spans full container width (FN-1608)", () => {
mockDesktopViewport();
renderQuickEntryBox({});