combobox
A combobox rendered inside a popup
A combobox rendered inside a popup
Загрузка превью…
src/combobox/c-combobox-10.vue
<script setup lang="ts">
/**
* Оригинал использует `<ComboboxTrigger render={<Button variant="outline"
* .../>}>` (Base UI render-композиция: `Button`'s собственные cva-классы
* сливаются с `cn-combobox-trigger` через `cn()` внутри самого `Button`).
* У reka-ui `ComboboxTrigger` нет прямого аналога `render`/`asChild` с
* готовой сшивкой двух компонентов без риска сломать поведение примитива
* (два дочерних узла — слот + иконка — не позволяют чисто использовать
* `as-child` с одним ребёнком). Здесь конечный результат воспроизведён
* напрямую: тот же `<button data-slot="combobox-trigger">`, но с классом,
* вычисленным через `cn(buttonVariants({variant:"outline"}), ...)` — то
* же итоговое множество классов, что получилось бы от слияния Button
* cva-варианта с `cn-combobox-trigger` в оригинале.
*
* `flagcdn.com` — реальный сетевой запрос, недетерминирован в headless-
* стенде; заменён локальным 1x1 data-URI (тот же приём, что и в
* `block-select`/`block-avatar`).
*
* `w-full` добавлен на триггер сверх оригинального `className="justify-
* between font-normal"` (без `w-full`) — задокументированное расхождение
* движков, не улучшение дизайна. Причина: Base UI `Combobox.Root` не
* рендерит собственный DOM-узел, поэтому в оригинале `Button`-триггер —
* прямой flex-ребёнок `Field` и растягивается на всю ширину через
* `*:w-full`/`align-items: stretch` самого `Field` без явного класса. У
* reka-ui `ComboboxRoot` (построен на `ListboxRoot`) рендерит настоящий
* оборачивающий `<div>`, из-за чего триггер оказывается на уровень глубже
* и до него это правило `Field` не дотягивается — без явного `w-full`
* кнопка (inline-flex) сжимается по контенту. Остальные паттерны
* (16/21-24/26-28) не задеты: их триггер уже несёт `w-full` в самом
* оригинале.
*/
import { buttonVariants } from "@/components/ui/button"
import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, ComboboxTrigger, ComboboxValue } from "@/components/ui/combobox"
import { Field } from "@/components/ui/field"
import { cn } from "@/lib/utils"
const PIXEL =
"data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAUwAOw=="
const countries = [
{ code: "af", label: "Afghanistan" },
{ code: "al", label: "Albania" },
{ code: "dz", label: "Algeria" },
{ code: "as", label: "American Samoa" },
{ code: "ad", label: "Andorra" },
{ code: "ao", label: "Angola" },
]
</script>
<template>
<Field class="max-w-xs">
<Combobox :default-value="countries[0]">
<ComboboxTrigger
:class="cn(buttonVariants({ variant: 'outline' }), 'w-full justify-between font-normal')"
>
<ComboboxValue v-slot="{ value }">
<span v-if="value" class="flex items-center gap-2">
<img :src="PIXEL" alt="" width="16" height="16" class="rounded-xs" />
<span>{{ (value as (typeof countries)[number]).label }}</span>
</span>
</ComboboxValue>
</ComboboxTrigger>
<ComboboxContent class="max-w-(--anchor-width) min-w-(--anchor-width)">
<ComboboxInput :show-trigger="false" placeholder="Search" />
<ComboboxEmpty>No items found.</ComboboxEmpty>
<ComboboxList>
<ComboboxItem v-for="item in countries" :key="item.code" :value="item">
<img :src="PIXEL" alt="" width="16" height="12" class="rounded-xs" />
{{ item.label }}
</ComboboxItem>
</ComboboxList>
</ComboboxContent>
</Combobox>
</Field>
</template>
<style scoped>
/* reka-ui's ComboboxRoot renders a real DOM wrapper div (via internal
ListboxRoot Primitive) around its slot content, unlike Base UI's
headless ComboboxPrimitive.Root, which renders nothing. When the only
child is a ComboboxTrigger-as-button (no sibling ComboboxInput), this
unstyled div participates in an inline formatting context and can add
a stray 1-3px of height depending on the theme's font metrics --
invisible but breaks pixel parity with the React reference, which has
no such wrapper. display:contents removes the anonymous box without
touching layout/functionality (the div stays in the DOM, still used
internally by reka-ui as the popper anchor ref). */
:deep(div[dir]) {
display: contents;
}
</style>