fix(ui): truncate long searchable select labels

This commit is contained in:
t8y2 2026-07-15 13:45:28 +08:00
parent fb2a04660b
commit ba12720e7e
5 changed files with 55 additions and 27 deletions

View File

@ -913,9 +913,9 @@ watch(
content-class="w-[var(--reka-popover-trigger-width)]"
>
<template #option-label="{ option, label }">
<div class="flex items-center gap-2">
<DatabaseIcon :db-type="connectionIconType(option)" class="w-3.5 h-3.5" />
{{ label }}
<div class="flex min-w-0 items-center gap-2">
<DatabaseIcon :db-type="connectionIconType(option)" class="h-3.5 w-3.5 shrink-0" />
<span class="min-w-0 flex-1 truncate">{{ label }}</span>
</div>
</template>
</SearchableSelect>
@ -999,9 +999,9 @@ watch(
content-class="w-[var(--reka-popover-trigger-width)]"
>
<template #option-label="{ option, label }">
<div class="flex items-center gap-2">
<DatabaseIcon :db-type="connectionIconType(option)" class="w-3.5 h-3.5" />
{{ label }}
<div class="flex min-w-0 items-center gap-2">
<DatabaseIcon :db-type="connectionIconType(option)" class="h-3.5 w-3.5 shrink-0" />
<span class="min-w-0 flex-1 truncate">{{ label }}</span>
</div>
</template>
</SearchableSelect>

View File

@ -267,9 +267,9 @@ async function fetchDbVersion(connectionId: string, database: string, schema: st
content-class="w-[var(--reka-popover-trigger-width)]"
>
<template #option-label="{ option, label }">
<div class="flex items-center gap-2">
<DatabaseIcon :db-type="sqlConnections.find((c) => c.id === option)?.driver_profile || sqlConnections.find((c) => c.id === option)?.db_type || 'mysql'" class="w-3.5 h-3.5" />
{{ label }}
<div class="flex min-w-0 items-center gap-2">
<DatabaseIcon :db-type="sqlConnections.find((c) => c.id === option)?.driver_profile || sqlConnections.find((c) => c.id === option)?.db_type || 'mysql'" class="h-3.5 w-3.5 shrink-0" />
<span class="min-w-0 flex-1 truncate">{{ label }}</span>
</div>
</template>
</SearchableSelect>
@ -351,9 +351,9 @@ async function fetchDbVersion(connectionId: string, database: string, schema: st
content-class="w-[var(--reka-popover-trigger-width)]"
>
<template #option-label="{ option, label }">
<div class="flex items-center gap-2">
<DatabaseIcon :db-type="sqlConnections.find((c) => c.id === option)?.driver_profile || sqlConnections.find((c) => c.id === option)?.db_type || 'mysql'" class="w-3.5 h-3.5" />
{{ label }}
<div class="flex min-w-0 items-center gap-2">
<DatabaseIcon :db-type="sqlConnections.find((c) => c.id === option)?.driver_profile || sqlConnections.find((c) => c.id === option)?.db_type || 'mysql'" class="h-3.5 w-3.5 shrink-0" />
<span class="min-w-0 flex-1 truncate">{{ label }}</span>
</div>
</template>
</SearchableSelect>

View File

@ -380,9 +380,9 @@ function getConnectionName(id: string) {
content-class="w-[var(--reka-popover-trigger-width)]"
>
<template #option-label="{ option, label }">
<div class="flex items-center gap-1.5">
<DatabaseIcon :db-type="sqlConnections.find((c) => c.id === option)?.db_type ?? 'mysql'" class="w-3.5 h-3.5" />
{{ label }}
<div class="flex min-w-0 items-center gap-1.5">
<DatabaseIcon :db-type="sqlConnections.find((c) => c.id === option)?.db_type ?? 'mysql'" class="h-3.5 w-3.5 shrink-0" />
<span class="min-w-0 flex-1 truncate">{{ label }}</span>
</div>
</template>
</SearchableSelect>
@ -443,9 +443,9 @@ function getConnectionName(id: string) {
content-class="w-[var(--reka-popover-trigger-width)]"
>
<template #option-label="{ option, label }">
<div class="flex items-center gap-1.5">
<DatabaseIcon :db-type="sqlConnections.find((c) => c.id === option)?.db_type ?? 'mysql'" class="w-3.5 h-3.5" />
{{ label }}
<div class="flex min-w-0 items-center gap-1.5">
<DatabaseIcon :db-type="sqlConnections.find((c) => c.id === option)?.db_type ?? 'mysql'" class="h-3.5 w-3.5 shrink-0" />
<span class="min-w-0 flex-1 truncate">{{ label }}</span>
</div>
</template>
</SearchableSelect>

View File

@ -273,9 +273,12 @@ function handleKeydown(event: KeyboardEvent) {
<Check :class="cn('absolute inset-0 h-3.5 w-3.5', option !== modelValue ? 'opacity-0' : clearSelectedOption ? 'opacity-100 group-hover:opacity-0 group-focus-visible:opacity-0' : 'opacity-100')" />
<X v-if="clearSelectedOption && option === modelValue" class="absolute inset-0 h-3.5 w-3.5 opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100" />
</span>
<slot name="option-label" :option="option" :label="displayName?.(option)">
<span class="truncate">{{ displayName?.(option) }}</span>
</slot>
<!-- Keep custom labels inside the flex row so long content cannot overlap adjacent UI. -->
<div class="dbx-searchable-select-option-label min-w-0 flex-1 overflow-hidden">
<slot name="option-label" :option="option" :label="displayName?.(option)">
<span class="block truncate">{{ displayName?.(option) }}</span>
</slot>
</div>
</button>
<button
v-if="canSelectCustom"
@ -292,9 +295,11 @@ function handleKeydown(event: KeyboardEvent) {
@click="selectCustomOption"
>
<Check class="h-3.5 w-3.5 shrink-0 opacity-0" />
<slot name="custom-option-label" :value="customOptionValue">
<span class="truncate">{{ customOptionValue }}</span>
</slot>
<div class="dbx-searchable-select-option-label min-w-0 flex-1 overflow-hidden">
<slot name="custom-option-label" :value="customOptionValue">
<span class="block truncate">{{ customOptionValue }}</span>
</slot>
</div>
</button>
</template>
<button
@ -312,9 +317,11 @@ function handleKeydown(event: KeyboardEvent) {
@click="selectCustomOption"
>
<Check class="h-3.5 w-3.5 shrink-0 opacity-0" />
<slot name="custom-option-label" :value="customOptionValue">
<span class="truncate">{{ customOptionValue }}</span>
</slot>
<div class="dbx-searchable-select-option-label min-w-0 flex-1 overflow-hidden">
<slot name="custom-option-label" :value="customOptionValue">
<span class="block truncate">{{ customOptionValue }}</span>
</slot>
</div>
</button>
<div v-else class="px-2 py-2 text-sm text-muted-foreground">
{{ emptyText }}

View File

@ -0,0 +1,21 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const searchableSelectSource = readFileSync(new URL("../../../components/ui/searchable-select/SearchableSelect.vue", import.meta.url), "utf8");
const dataTransferDialogSource = readFileSync(new URL("../../../components/transfer/DataTransferDialog.vue", import.meta.url), "utf8");
describe("SearchableSelect layout", () => {
it("keeps slotted option labels inside a shrinkable overflow boundary", () => {
const labelBoundaries = searchableSelectSource.match(/dbx-searchable-select-option-label min-w-0 flex-1 overflow-hidden/g) ?? [];
expect(labelBoundaries).toHaveLength(3);
});
it("truncates long data transfer connection labels without shrinking their icons", () => {
const truncatedConnectionLabels = dataTransferDialogSource.match(/<span class="min-w-0 flex-1 truncate">\{\{ label \}\}<\/span>/g) ?? [];
const fixedConnectionIcons = dataTransferDialogSource.match(/class="h-3\.5 w-3\.5 shrink-0"/g) ?? [];
expect(truncatedConnectionLabels).toHaveLength(2);
expect(fixedConnectionIcons).toHaveLength(2);
});
});