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:
@@ -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={() => {
|
||||
|
||||
@@ -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({});
|
||||
|
||||
Reference in New Issue
Block a user