combobox

A lead selection combobox

A lead selection combobox

Загрузка превью…

src/combobox/c-combobox-23.vue

<!--
  Unsplash-URL заменены локальным 1x1 data-URI (см. `c-combobox-10.vue`).
  `UserGlyph`/`LeadTriggerLabel`/`LeadListRow` — фабрики `VNodeChild` через
  `<component :is="() => fn()" />`, как в `c-combobox-21.vue`.
-->
<script setup lang="ts">
import { h, ref, type VNodeChild } from "vue"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { buttonVariants } from "@/components/ui/button"
import { Combobox, ComboboxCollection, ComboboxContent, ComboboxEmpty, ComboboxGroup, ComboboxInput, ComboboxItem, ComboboxLabel, ComboboxList, ComboboxTrigger, ComboboxValue } from "@/components/ui/combobox"
import { Field } from "@/components/ui/field"
import { Item, ItemContent, ItemMedia, ItemTitle } from "@/components/ui/item"
import { cn } from "@/lib/utils"

const PIXEL =
  "data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAUwAOw=="

type NoLeadOption = { type: "none"; value: "none"; label: "No lead" }
type TeamMember = {
  type: "member"
  id: string
  value: string
  label: string
  avatar: string
  initials: string
  isCurrentUser?: boolean
}
type LeadOption = NoLeadOption | TeamMember

const noLeadOption: NoLeadOption = { type: "none", value: "none", label: "No lead" }

const teamMembers: TeamMember[] = [
  { type: "member", id: "member-1", value: "shuhrat-saipov", label: "Shuhrat Saipov", avatar: PIXEL, initials: "SS", isCurrentUser: true },
  { type: "member", id: "member-2", value: "nadia-karimova", label: "Nadia Karimova", avatar: PIXEL, initials: "NK" },
  { type: "member", id: "member-3", value: "bekzod-rakhimov", label: "Bekzod Rakhimov", avatar: PIXEL, initials: "BR" },
  { type: "member", id: "member-4", value: "lina-bauer", label: "Lina Bauer", avatar: PIXEL, initials: "LB" },
  { type: "member", id: "member-5", value: "omar-haddad", label: "Omar Haddad", avatar: PIXEL, initials: "OH" },
  { type: "member", id: "member-6", value: "priya-nand", label: "Priya Nand", avatar: PIXEL, initials: "PN" },
  { type: "member", id: "member-7", value: "kenji-watan", label: "Kenji Watan", avatar: PIXEL, initials: "KW" },
  { type: "member", id: "member-8", value: "ava-sinclair", label: "Ava Sinclair", avatar: PIXEL, initials: "AS" },
  { type: "member", id: "member-9", value: "nia-okafor", label: "Nia Okafor", avatar: PIXEL, initials: "NO" },
  { type: "member", id: "member-10", value: "matteo-sosa", label: "Matteo Sosa", avatar: PIXEL, initials: "MS" },
  { type: "member", id: "member-11", value: "salma-rahman", label: "Salma Rahman", avatar: PIXEL, initials: "SR" },
  { type: "member", id: "member-12", value: "jonas-meyer", label: "Jonas Meyer", avatar: PIXEL, initials: "JM" },
]

function userGlyph(): VNodeChild {
  return h(
    "svg",
    {
      xmlns: "http://www.w3.org/2000/svg",
      width: "24",
      height: "24",
      viewBox: "0 0 24 24",
      fill: "none",
      stroke: "currentColor",
      "stroke-width": "2",
      "stroke-linecap": "round",
      "stroke-linejoin": "round",
      class: "size-4 shrink-0 text-muted-foreground",
    },
    [
      h("path", { d: "M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" }),
      h("circle", { cx: "12", cy: "7", r: "4" }),
    ]
  )
}

function leadTriggerLabel(option: LeadOption): VNodeChild {
  if (option.type === "none") {
    return h("span", { class: "flex min-w-0 items-center gap-2" }, [
      userGlyph(),
      h("span", { class: "truncate" }, option.label),
    ])
  }
  return h("span", { class: "flex min-w-0 items-center gap-2" }, [
    h(Avatar, { class: "size-5" }, () => [
      h(AvatarImage, { src: option.avatar, alt: option.label }),
      h(AvatarFallback, { class: "text-[9px]" }, () => option.initials),
    ]),
    h("span", { class: "truncate" }, option.label),
  ])
}

function leadListRow(option: LeadOption): VNodeChild {
  if (option.type === "none") {
    return h("span", { class: "flex min-w-0 items-center gap-2" }, [
      userGlyph(),
      h("span", { class: "truncate" }, option.label),
    ])
  }
  return h(Item, { size: "xs", class: "p-0" }, () => [
    h(ItemMedia, {}, () => [
      h(Avatar, { class: "size-5" }, () => [
        h(AvatarImage, { src: option.avatar, alt: option.label }),
        h(AvatarFallback, { class: "text-[9px]" }, () => option.initials),
      ]),
    ]),
    h(ItemContent, {}, () => [
      h(ItemTitle, { class: "gap-1 whitespace-nowrap" }, () => [
        h("span", {}, option.label),
        option.isCurrentUser ? h("span", { class: "text-muted-foreground font-normal" }, "(You)") : null,
      ]),
    ]),
  ])
}

const lead = ref<LeadOption | null>(noLeadOption)
</script>

<template>
  <Field class="max-w-xs">
    <Combobox
      :model-value="lead"
      @update:model-value="(v) => (lead = v as LeadOption | null)"
    >
      <ComboboxTrigger
        :class="cn(buttonVariants({ variant: 'outline' }), 'w-full justify-between font-normal')"
      >
        <ComboboxValue v-slot="{ value }">
          <component :is="() => ((value as LeadOption | null) ? leadTriggerLabel(value as LeadOption) : h('span', { class: 'text-muted-foreground' }, 'No lead'))" />
        </ComboboxValue>
      </ComboboxTrigger>

      <ComboboxContent class="max-w-(--anchor-width) min-w-(--anchor-width)">
        <ComboboxInput :show-trigger="false" placeholder="Select lead" class="mb-1" />
        <ComboboxEmpty>No team members found.</ComboboxEmpty>
        <ComboboxList>
          <ComboboxItem :value="noLeadOption">
            <component :is="() => leadListRow(noLeadOption)" />
          </ComboboxItem>

          <ComboboxGroup>
            <ComboboxLabel>Team members</ComboboxLabel>
            <ComboboxCollection>
              <ComboboxItem v-for="member in teamMembers" :key="member.id" :value="member">
                <component :is="() => leadListRow(member)" />
              </ComboboxItem>
            </ComboboxCollection>
          </ComboboxGroup>
        </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-23.json

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

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