feat(ui): show Tab keycap on completion list rows

This commit is contained in:
HARRISON authored and GitHub committed 2026-10-26 18:37:44 +08:00
1 parent 14e9683aae
commit d2077e17e5
11 files changed
+64

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

@@ -3,6 +3,7 @@ import type { CSSProperties } from "vue";
import { computed } from "vue";
import type { DataGridConditionSuggestionPosition } from "@/lib/dataGrid/dataGridConditionSuggestionPosition";
import type { MongoCompletionItem } from "@/lib/mongo/mongoCompletion";
import KeyCap from "@/components/ui/KeyCap.vue";
defineOptions({ name: "DocumentQueryCompletionMenu" });
@@ -75,6 +76,10 @@ function isSelected(index: number): boolean {
<span class="w-3 shrink-0 text-center font-mono text-[10px] text-muted-foreground">{{ glyph(item) }}</span>
<span class="min-w-0 flex-1 truncate font-mono">{{ item.label }}</span>
<span v-if="item.detail" class="max-w-[55%] shrink-0 truncate text-[11px] text-muted-foreground">{{ item.detail }}</span>
<!-- The slot keeps its width on every row so the hint never shifts the text. -->
<span class="flex w-7 shrink-0 justify-end">
<KeyCap v-if="isSelected(index)" label="Tab" />
</span>
</button>
</div>
</Teleport>
@@ -1,6 +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 KeyCap from "@/components/ui/KeyCap.vue";
import { completeDataGridConditionQuote, useDataGridConditionEditor, type DataGridConditionColumnOption, type DataGridConditionSuggestion, type DataGridConditionSuggestionProvider } from "@/composables/useDataGridConditionEditor";
import { tokenizeDataGridCondition, type DataGridConditionTokenType } from "@/lib/dataGrid/dataGridConditionHighlight";
import { getDataGridConditionSuggestionPosition, getDataGridConditionSuggestionPreferredWidth } from "@/lib/dataGrid/dataGridConditionSuggestionPosition";
@@ -129,6 +130,9 @@ function onEditorScroll(event: Event) {
}
const emptyHistoryText = computed(() => (modelValue.value.trim() ? props.historyNoMatchesText : props.historyEmptyText));
const activeSuggestionId = computed(() => (editor.highlightedIndex.value >= 0 ? `${suggestionListId}-${editor.highlightedIndex.value}` : undefined));
// Tab accepts the highlighted suggestion, or the first one when nothing is
// highlighted yet, so the keycap marks that same row.
const acceptTargetIndex = computed(() => Math.max(0, editor.highlightedIndex.value));
const suggestionPreferredWidth = computed(() => getDataGridConditionSuggestionPreferredWidth(editor.suggestions.value));
const suggestionStyle = computed<CSSProperties>(() => ({
left: `${suggestionPosition.value.left}px`,
@@ -763,6 +767,10 @@ defineExpose({ focus, dismiss: editor.dismiss, rememberHistory: editor.rememberH
{{ suggestion.value }}
</span>
<span v-if="suggestion.comment" class="ml-3 min-w-0 flex-1 truncate text-right text-muted-foreground" :title="suggestion.comment">{{ suggestion.comment }}</span>
<!-- The slot keeps its width on every row so the hint never shifts the text. -->
<span class="ml-2 flex w-7 shrink-0 justify-end">
<KeyCap v-if="index === acceptTargetIndex" label="Tab" />
</span>
<button v-if="suggestion.kind === 'history'" type="button" class="ml-2 shrink-0 text-muted-foreground hover:text-foreground" @mousedown.stop.prevent="editor.deleteHistory(suggestion.value)">
<X class="h-3 w-3" />
</button>
@@ -313,6 +313,33 @@ describe("DataGridConditionEditor quote completion", () => {
expect(value.value).toBe("namespace");
});
it("prints the Tab keycap on the suggestion Tab would accept", async () => {
const { value, input } = mountEditor("where", "", { columns: ["status"] });
input.focus();
input.value = "status ";
input.setSelectionRange(7, 7);
input.dispatchEvent(new Event("input", { bubbles: true }));
await vi.waitFor(() => expect(document.querySelectorAll('[role="option"]')).toHaveLength(12));
const options = () => Array.from(document.querySelectorAll<HTMLElement>('[role="option"]'));
// Nothing is highlighted yet, but Tab still accepts the first row.
expect(options()[0].querySelector("kbd")?.getAttribute("data-key")).toBe("Tab");
expect(options()[1].querySelector("kbd")).toBeNull();
input.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true, cancelable: true }));
await nextTick();
expect(options()[0].querySelector("kbd")?.getAttribute("data-key")).toBe("Tab");
input.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true, cancelable: true }));
await nextTick();
expect(options()[0].querySelector("kbd")).toBeNull();
expect(options()[1].querySelector("kbd")?.getAttribute("data-key")).toBe("Tab");
input.dispatchEvent(new KeyboardEvent("keydown", { key: "Tab", bubbles: true, cancelable: true }));
await nextTick();
expect(value.value).toBe("status <> ");
});
it("selects the first WHERE field suggestion with Enter", async () => {
const { value, input } = mountEditor("where", "", { columns: ["customer_id", "customer_name"] });
input.focus();
@@ -16,6 +16,7 @@ import { OptionHelpPanel } from "@/components/ui/option-help-panel";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Switch } from "@/components/ui/switch";
import DateTimePicker from "@/components/ui/date-time-picker/DateTimePicker.vue";
import KeyCap from "@/components/ui/KeyCap.vue";
import CustomContextMenu, { type ContextMenuItem } from "@/components/ui/CustomContextMenu.vue";
import DangerConfirmDialog from "@/components/editor/DangerConfirmDialog.vue";
import { redisDeleteGroupPreview, redisDeleteKeysPreview } from "@/lib/redis/redisCommandPreview";
@@ -3960,6 +3961,10 @@ defineExpose({ focusSearch, insertCommand, executeCommand: executeAiCommand });
<span v-if="item.apply && item.apply !== item.label" class="block truncate text-[11px] text-slate-500">{{ item.apply }}</span>
</span>
<span v-if="item.detail" class="shrink-0 text-slate-400">{{ item.detail }}</span>
<!-- The slot keeps its width on every row so the hint never shifts the text. -->
<span class="flex w-7 shrink-0 justify-end">
<KeyCap v-if="commandCompletionSelectedIndex === index" label="Tab" />
</span>
</button>
</div>
</div>
+19
View File
@@ -0,0 +1,19 @@
<script setup lang="ts">
/**
* A single key printed as a quiet keycap. Colors come from `currentColor`, so
* one component works on light popovers and on the hard-coded dark Redis
* terminal instead of carrying per-surface color props. The label is painted
* from CSS content so the hint stays out of the option's text content.
*/
defineProps<{ label: string }>();
</script>
<template>
<kbd aria-hidden="true" :data-key="label" class="dbx-key-cap shrink-0 rounded-[3px] border border-current/25 bg-current/8 px-1 py-px font-mono text-[10px] leading-none font-medium opacity-70" />
</template>
<style scoped>
.dbx-key-cap::after {
content: attr(data-key);
}
</style>