fix(grid): add result search navigation controls

This commit is contained in:
t8y2 committed 2026-07-21 12:09:04 +08:00
1 parent f468eb8f47
commit 8d7acf074d
5 files changed
+146 -10

No files matched your search

@@ -1,7 +1,18 @@
import { nextTick, ref } from "vue";
import { describe, expect, it, vi } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import { useDataGridSearch } from "@/composables/useDataGridSearch";
afterEach(() => {
vi.useRealTimers();
});
async function flushSearchDebounce() {
await nextTick();
vi.runOnlyPendingTimers();
await nextTick();
await nextTick();
}
describe("useDataGridSearch", () => {
it("debounces matching across columns and cells", async () => {
vi.useFakeTimers();
@@ -15,7 +26,6 @@ describe("useDataGridSearch", () => {
expect(search.matches.value).toEqual([{ kind: "cell", displayRow: 0, col: 1 }]);
// matchSet 用数值 key:(displayRow+1)*65536+col
expect(search.matchSet.value.has((0 + 1) * 65536 + 1)).toBe(true);
vi.useRealTimers();
});
it("keys column-name matches with displayRow -1", async () => {
@@ -27,7 +37,6 @@ describe("useDataGridSearch", () => {
await nextTick();
expect(search.matches.value).toEqual([{ kind: "column", displayRow: -1, col: 1 }]);
expect(search.matchSet.value.has((-1 + 1) * 65536 + 1)).toBe(true);
vi.useRealTimers();
});
it("suggests columns and replaces only the active token", async () => {
@@ -39,4 +48,63 @@ describe("useDataGridSearch", () => {
expect(search.acceptSuggestion()).toBe(true);
expect(search.searchText.value).toBe("status = customer_id");
});
it("navigates forward and backward with first/last wrapping", async () => {
vi.useFakeTimers();
const onNavigate = vi.fn();
const search = useDataGridSearch({
columns: ["left", "right"],
rows: [
["hit", "hit"],
["none", "hit"],
],
getCellSearchText: (row, column) => row[column],
onNavigate,
});
search.searchText.value = "hit";
await flushSearchDebounce();
expect(search.currentMatchIndex.value).toBe(0);
search.navigateMatch(1);
expect(search.currentMatchIndex.value).toBe(1);
expect(onNavigate).toHaveBeenLastCalledWith({ kind: "cell", displayRow: 0, col: 1 });
search.navigateMatch(-1);
expect(search.currentMatchIndex.value).toBe(0);
search.navigateMatch(-1);
expect(search.currentMatchIndex.value).toBe(2);
expect(onNavigate).toHaveBeenLastCalledWith({ kind: "cell", displayRow: 1, col: 1 });
search.navigateMatch(1);
expect(search.currentMatchIndex.value).toBe(0);
});
it("resets navigation when results change or the query is cleared", async () => {
vi.useFakeTimers();
const rows = ref([["hit"], ["hit"]]);
const onNavigate = vi.fn();
const search = useDataGridSearch({ columns: ["value"], rows, getCellSearchText: (row, column) => row[column], onNavigate });
search.searchText.value = "hit";
await flushSearchDebounce();
search.navigateMatch(1);
expect(search.currentMatchIndex.value).toBe(1);
rows.value = [["hit"]];
await nextTick();
await nextTick();
expect(search.currentMatchIndex.value).toBe(0);
expect(search.currentMatch.value).toEqual({ kind: "cell", displayRow: 0, col: 0 });
rows.value = [];
await nextTick();
expect(search.currentMatchIndex.value).toBe(-1);
expect(search.currentMatch.value).toBeNull();
rows.value = [["hit"]];
await nextTick();
search.searchText.value = "";
await nextTick();
expect(search.matches.value).toEqual([]);
expect(search.currentMatchIndex.value).toBe(-1);
});
});
@@ -91,8 +91,11 @@ export function useDataGridSearch<Row>(options: UseDataGridSearchOptions<Row>) {
}
function navigateMatch(delta: number) {
if (!matches.value.length) return;
currentMatchIndex.value = (currentMatchIndex.value + delta + matches.value.length) % matches.value.length;
const matchCount = matches.value.length;
if (!matchCount) return;
// Results may change between input and navigation; recover from a stale index in the requested direction.
const currentIndex = currentMatchIndex.value >= 0 && currentMatchIndex.value < matchCount ? currentMatchIndex.value : delta < 0 ? 0 : -1;
currentMatchIndex.value = (((currentIndex + delta) % matchCount) + matchCount) % matchCount;
const match = currentMatch.value;
if (match) options.onNavigate?.(match);
}