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>

Установка

npx shadcn-vue@latest add https://revueui.rootapi.dev/r/c-combobox-10.json

Зависимости реестра

Источник: порт из ReUI (Keenthemes, MIT)