feat(data-grid): improve filter completion hover behavior
This commit is contained in:
parent
cd299fa5fa
commit
64011dff4c
|
|
@ -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 }}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue