fix(ui): ⌘⌫ erases a tree's speed search query (#3226)

While a tree's speed search has a query, the keymap dispatcher ran ⌫ shortcuts before the tree saw the key: in the Database view ⌘⌫ opened Drop or Remove Data Source. The shared Tree now claims ⌫ with any modifier while a query is active and edits the query like a text field: ⌘⌫ erases it, ⌥⌫ / Ctrl+⌫ erase the last word. Without a query the view's shortcuts run as before.

Co-authored-by: Mr0Bread <55754540+Mr0Bread@users.noreply.github.com>
This commit is contained in:
Mr0Bread
2026-11-14 02:15:07 +08:00
committed by GitHub
parent b368f21eb6
commit fcc8583f36
2 changed files with 39 additions and 3 deletions
+26
View File
@@ -3,6 +3,8 @@ import { createRef } from "react";
import { act, fireEvent, render, screen, within } from "@testing-library/react";
import { Tree, type TreeHandle } from "./Tree";
import { getContext } from "@/platform/context";
import { registerAction } from "@/platform/actions";
import { installKeyboardDispatcher, useKeymap } from "@/platform/keymap";
interface N {
id: string;
@@ -251,6 +253,30 @@ describe("Tree speed search", () => {
expect(labels(tree)).toContain("App.tsx");
});
it("⌘⌫ erases the query and ⌥⌫ its last word instead of running the view's ⌘⌫ action", () => {
const drop = vi.fn();
useKeymap.setState({ preset: "intellij-macos", overrides: { "test.drop": ["Meta+Backspace", "Alt+Backspace"] } });
const dispose = [registerAction({ id: "test.drop", title: "Drop", run: drop }), installKeyboardDispatcher()];
try {
const { tree, lead, key } = setup({ defaultExpanded: ["src", "main"] });
key("ArrowDown");
for (const c of "index.t") key(c);
expect(lead()).toBe("index.ts");
key("Backspace", { altKey: true });
expect(screen.getByRole("status")).toHaveTextContent(/^index\.$/);
key("Backspace", { metaKey: true });
expect(screen.queryByRole("status")).toBeNull();
expect(drop).not.toHaveBeenCalled();
// Without a query the shortcut reaches the view again.
key("Backspace", { metaKey: true });
expect(drop).toHaveBeenCalledOnce();
expect(tree).toHaveFocus();
} finally {
dispose.forEach((d) => d());
useKeymap.setState({ overrides: {} });
}
});
it("filter mode hides non-matching rows but keeps ancestors", () => {
const { tree, key } = setup({ defaultExpanded: ["src", "main"], speedSearchMode: "filter" });
key("ArrowDown");
+13 -3
View File
@@ -31,6 +31,7 @@ import { acquirePopup } from "./popupState";
import styles from "./Tree.module.css";
import { T } from "./T";
import { setContext } from "@/platform/context";
import { registerKeyClaim } from "@/platform/keymap";
import { useGivenTx } from "@/platform/i18n";
import { useTreeReorder, type TreeReorder } from "./treeReorder";
import { startPointerDrag, type DragData } from "./pointerDrag";
@@ -367,10 +368,18 @@ export function Tree<T>(props: TreeProps<T>) {
pendingScrollToLead.current = null;
}, [lead, indexOfKey, virtualizer]);
// Speed search owns Escape while active.
// Speed search owns Escape while active, and ⌫ with any modifier: the query is edited like a
// text field instead of the view's shortcuts running (⌘⌫ is Drop… in the Database view).
useEffect(() => {
if (!query) return;
return acquirePopup();
const release = acquirePopup();
const unclaim = registerKeyClaim(
(e) => e.key === "Backspace" && e.target instanceof Node && !!scrollRef.current?.contains(e.target),
);
return () => {
release();
unclaim();
};
}, [query]);
const setExpanded = (row: Row<T>, open: boolean) => {
@@ -524,7 +533,8 @@ export function Tree<T>(props: TreeProps<T>) {
break;
case "Backspace":
if (!query) return;
setQuery(query.slice(0, -1));
// ⌘⌫ erases the query, ⌥⌫ / Ctrl+⌫ its last word, as in a text field.
setQuery(e.metaKey ? "" : e.altKey || e.ctrlKey ? query.replace(/\w*\W*$/, "") : query.slice(0, -1));
break;
case "ArrowDown":
case "ArrowUp": {