feat(data-grid): improve filter completion hover behavior

This commit is contained in:
zipg 2026-08-02 08:59:17 +08:00 committed by GitHub
parent cd299fa5fa
commit 64011dff4c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 54 additions and 7 deletions

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, nextTick, onMounted, onUnmounted, ref, useId, watch, type CSSProperties } from "vue";
import { ChevronDown, X } from "@lucide/vue";
import { completeDataGridConditionQuote, useDataGridConditionEditor, type DataGridConditionColumnOption, type DataGridConditionSuggestionProvider } from "@/composables/useDataGridConditionEditor";
import { completeDataGridConditionQuote, useDataGridConditionEditor, type DataGridConditionColumnOption, type DataGridConditionSuggestion, type DataGridConditionSuggestionProvider } from "@/composables/useDataGridConditionEditor";
import { getDataGridConditionSuggestionPosition, getDataGridConditionSuggestionPreferredWidth } from "@/lib/dataGrid/dataGridConditionSuggestionPosition";
import type { DataGridConditionHistoryKind, DataGridConditionHistoryScope } from "@/lib/dataGrid/dataGridConditionHistory";
@ -52,6 +52,7 @@ const expandedRect = ref({ left: 0, top: 0, width: 0, controlsTop: 0, inputTop:
const expandedHeight = ref(56);
const suggestionPosition = ref({ left: 0, top: 0, width: 180 });
const historyPreview = ref<{ value: string; left: number; top: number; maxWidth: number; arrowTop: number; side: "left" | "right" } | null>(null);
const pointerMovedSuggestionIndex = ref(-1);
let collapseTimer: ReturnType<typeof setTimeout> | undefined;
let resizeObserver: ResizeObserver | undefined;
let expandAfterComposition = false;
@ -393,6 +394,17 @@ function acceptSuggestion(index: number) {
focusAfterAccept();
}
function onSuggestionPointerMove(index: number, suggestion: DataGridConditionSuggestion, event: MouseEvent) {
pointerMovedSuggestionIndex.value = index;
editor.highlightedIndex.value = index;
if (suggestion.kind === "history") showHistoryPreview(suggestion.value, event);
}
function onSuggestionPointerLeave() {
pointerMovedSuggestionIndex.value = -1;
hideHistoryPreview();
}
function eventInside(event: Event, element?: HTMLElement) {
if (!element) return false;
const path = typeof event.composedPath === "function" ? event.composedPath() : [];
@ -438,9 +450,16 @@ watch(modelValue, () => resizeEditor());
watch(suggestionPreferredWidth, () => {
if (editor.dropdownOpen.value) updateSuggestionPosition();
});
watch(
() => editor.suggestions.value.map((suggestion) => `${suggestion.kind}:${suggestion.value}`).join("\n"),
() => {
pointerMovedSuggestionIndex.value = -1;
},
);
watch(
() => editor.dropdownOpen.value,
(open) => {
pointerMovedSuggestionIndex.value = -1;
if (open) updateSuggestionPosition();
else hideHistoryPreview();
},
@ -564,13 +583,10 @@ defineExpose({ focus, dismiss: editor.dismiss, rememberHistory: editor.rememberH
role="option"
:aria-selected="index === editor.highlightedIndex.value"
class="flex cursor-pointer items-center px-3 py-1.5 text-xs"
:class="index === editor.highlightedIndex.value ? 'bg-accent text-accent-foreground' : 'hover:bg-gray-200 dark:hover:bg-gray-800'"
:class="index === editor.highlightedIndex.value ? 'bg-accent text-accent-foreground' : pointerMovedSuggestionIndex === index ? 'bg-gray-200 dark:bg-gray-800' : ''"
@mousedown.prevent="acceptSuggestion(index)"
@mouseenter="
editor.highlightedIndex.value = index;
suggestion.kind === 'history' && showHistoryPreview(suggestion.value, $event);
"
@mouseleave="hideHistoryPreview"
@mousemove="onSuggestionPointerMove(index, suggestion, $event)"
@mouseleave="onSuggestionPointerLeave"
>
<span data-condition-history-text class="data-grid-condition-suggestion-field min-w-0 truncate" :class="suggestion.comment ? 'max-w-[75%] shrink-0' : 'flex-1'" :title="suggestion.value">
{{ suggestion.value }}

View File

@ -143,6 +143,37 @@ describe("DataGridConditionEditor quote completion", () => {
expect(value.value).toBe("name");
});
it("does not select a suggestion just because the dropdown appears under the mouse", async () => {
const { value, input } = mountEditor("orderBy", "", { columns: ["name", "namespace"] });
input.focus();
input.value = "na";
input.setSelectionRange(2, 2);
input.dispatchEvent(new Event("input", { bubbles: true }));
await vi.waitFor(() => expect(document.querySelectorAll('[role="option"]')).toHaveLength(2));
const firstOption = document.querySelector('[role="option"]') as HTMLElement;
firstOption.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true, cancelable: true }));
await nextTick();
expect(document.querySelector('[role="option"][aria-selected="true"]')).toBeNull();
expect(firstOption.className).not.toContain("bg-gray-200");
expect(firstOption.className).not.toContain("bg-accent");
input.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true, cancelable: true }));
await nextTick();
expect(value.value).toBe("na");
const secondEditor = mountEditor("orderBy", "", { columns: ["name", "namespace"] });
secondEditor.input.focus();
secondEditor.input.value = "na";
secondEditor.input.setSelectionRange(2, 2);
secondEditor.input.dispatchEvent(new Event("input", { bubbles: true }));
await vi.waitFor(() => expect(document.querySelectorAll('[role="option"]')).toHaveLength(2));
const nextFirstOption = document.querySelector('[role="option"]') as HTMLElement;
nextFirstOption.dispatchEvent(new MouseEvent("mousemove", { bubbles: true, cancelable: true }));
await nextTick();
expect(document.querySelector('[role="option"][aria-selected="true"]')?.textContent).toContain("name");
});
it("keeps expanded input first-line indent and wraps long tokens", () => {
const source = readFileSync(resolve(process.cwd(), "apps/desktop/src/components/grid/DataGridConditionEditor.vue"), "utf8");
const expandedInputCss = source.match(/\.data-grid-topbar-condition-input--expanded\s*\{(?<body>[\s\S]*?)\n\}/)?.groups?.body;