fix(grid): stop stealing focus after cell edit commit on blur

This commit is contained in:
zipg authored and GitHub committed 2026-09-21 11:37:38 +08:00
1 parent e364e4a70f
commit 566bfc3039
7 files changed
+74 -4

No files matched your search

@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { resolveGridFocusRestoreTarget } from "@/lib/dataGrid/dataGridFocusRestore";
import { resolveGridFocusRestoreTarget, shouldRestoreDataGridFocusAfterEditCommit } from "@/lib/dataGrid/dataGridFocusRestore";
function element(overrides: { connected?: boolean; children?: HTMLElement[]; closest?: Record<string, HTMLElement | null>; ownerDocument?: { body: HTMLElement; documentElement: HTMLElement }; activeGrid?: boolean } = {}) {
const children = overrides.children ?? [];
@@ -66,3 +66,48 @@ describe("resolveGridFocusRestoreTarget", () => {
expect(resolveGridFocusRestoreTarget(root, searchInput, activeGridInput)).toBeNull();
});
});
describe("shouldRestoreDataGridFocusAfterEditCommit", () => {
it("restores focus when the grid root is not mounted yet", () => {
expect(shouldRestoreDataGridFocusAfterEditCommit(null, null)).toBe(true);
expect(shouldRestoreDataGridFocusAfterEditCommit(undefined, null)).toBe(true);
});
it("keeps focus in the grid after a keyboard commit", () => {
const input = element();
const root = element({ children: [input] });
expect(shouldRestoreDataGridFocusAfterEditCommit(root, input)).toBe(true);
});
it("restores focus when the blur landed on body or the document element", () => {
const body = element();
const documentElement = element();
const root = element({ ownerDocument: { body, documentElement } });
expect(shouldRestoreDataGridFocusAfterEditCommit(root, body)).toBe(true);
expect(shouldRestoreDataGridFocusAfterEditCommit(root, documentElement)).toBe(true);
});
it("restores focus from a stale removed element", () => {
const root = element();
expect(shouldRestoreDataGridFocusAfterEditCommit(root, element({ connected: false }))).toBe(true);
});
it("restores focus when the tab strip took it", () => {
const root = element();
const tabButton = element({ closest: { ".app-tab-bar, [role='tab'], [role='tablist']": element() } });
expect(shouldRestoreDataGridFocusAfterEditCommit(root, tabButton)).toBe(true);
});
it("does not steal focus back from the SQL editor after an outside click", () => {
const root = element();
const sqlEditor = element();
expect(shouldRestoreDataGridFocusAfterEditCommit(root, sqlEditor)).toBe(false);
});
it("does not steal focus back from another active grid", () => {
const root = element();
const grid = element({ activeGrid: true });
const otherGridInput = element({ closest: { "[data-grid-root]": grid } });
expect(shouldRestoreDataGridFocusAfterEditCommit(root, otherGridInput)).toBe(false);
});
});
@@ -32,3 +32,19 @@ function gridFocusCanTransferFrom(root: HTMLElement, activeElement: Element | nu
if (activeGrid && activeGrid !== root) return activeGrid.dataset.gridActive !== "true";
return !!closest(".app-tab-bar, [role='tab'], [role='tablist']");
}
/**
* Decide whether a committed cell edit should hand focus back to the grid.
*
* Keyboard commits (Enter, inline bulk edit) leave focus inside the grid, so
* the grid must take it back for arrow-key navigation to keep working. A commit
* that ended because an outside pointer interaction moved focus elsewhere — the
* SQL editor, the WHERE condition bar, a toolbar button — must not, otherwise
* the element the user just clicked never keeps the caret, and the next
* keystroke or paste is claimed by the grid instead (#9383).
*/
export function shouldRestoreDataGridFocusAfterEditCommit(root: HTMLElement | null | undefined, activeElement: Element | null): boolean {
if (!root) return true;
if (root.contains(activeElement)) return true;
return gridFocusCanTransferFrom(root, activeElement);
}