feat(ui): show Tab keycap on completion list rows
This commit is contained in:
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>
|
||||
|
||||
@@ -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>
|
||||
Reference in new issue
Block a user