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:
5
.changeset/fn-6122-quick-entry-focus.md
Normal file
5
.changeset/fn-6122-quick-entry-focus.md
Normal 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.
|
||||||
@@ -1507,6 +1507,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`btn btn-sm refine-button ${isRefining ? "refine-button--loading" : ""}`}
|
className={`btn btn-sm refine-button ${isRefining ? "refine-button--loading" : ""}`}
|
||||||
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsRefineMenuOpen((prev) => {
|
setIsRefineMenuOpen((prev) => {
|
||||||
const next = !prev;
|
const next = !prev;
|
||||||
@@ -1578,6 +1579,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
|||||||
<button
|
<button
|
||||||
ref={depTriggerRef}
|
ref={depTriggerRef}
|
||||||
type="button"
|
type="button"
|
||||||
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
className="btn btn-sm dep-trigger"
|
className="btn btn-sm dep-trigger"
|
||||||
data-testid="quick-entry-deps"
|
data-testid="quick-entry-deps"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -1667,6 +1669,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
className="btn btn-sm"
|
className="btn btn-sm"
|
||||||
data-testid="quick-entry-attach"
|
data-testid="quick-entry-attach"
|
||||||
onClick={() => fileInputRef.current?.click()}
|
onClick={() => fileInputRef.current?.click()}
|
||||||
@@ -1678,6 +1681,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
|||||||
<button
|
<button
|
||||||
ref={modelTriggerRef}
|
ref={modelTriggerRef}
|
||||||
type="button"
|
type="button"
|
||||||
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
className="btn btn-sm"
|
className="btn btn-sm"
|
||||||
data-testid="quick-entry-models"
|
data-testid="quick-entry-models"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -1700,6 +1704,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
|||||||
<div className="node-trigger-wrap" ref={nodePickerRef}>
|
<div className="node-trigger-wrap" ref={nodePickerRef}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
className="btn btn-sm dep-trigger"
|
className="btn btn-sm dep-trigger"
|
||||||
data-testid="quick-entry-node-button"
|
data-testid="quick-entry-node-button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -1788,6 +1793,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
|||||||
<div className="agent-trigger-wrap" ref={agentPickerRef}>
|
<div className="agent-trigger-wrap" ref={agentPickerRef}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
className="btn btn-sm dep-trigger"
|
className="btn btn-sm dep-trigger"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (showAgentPicker) {
|
if (showAgentPicker) {
|
||||||
@@ -1862,6 +1868,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
|||||||
<div className="priority-trigger-wrap" ref={priorityPickerRef}>
|
<div className="priority-trigger-wrap" ref={priorityPickerRef}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
className="btn btn-sm dep-trigger"
|
className="btn btn-sm dep-trigger"
|
||||||
data-testid="quick-entry-priority-button"
|
data-testid="quick-entry-priority-button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -323,6 +323,93 @@ describe("QuickEntryBox", () => {
|
|||||||
innerWidthSpy.mockRestore();
|
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)", () => {
|
it("textarea spans full container width (FN-1608)", () => {
|
||||||
mockDesktopViewport();
|
mockDesktopViewport();
|
||||||
renderQuickEntryBox({});
|
renderQuickEntryBox({});
|
||||||
|
|||||||
Reference in New Issue
Block a user