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>