|
| 1 | +<script setup lang="ts"> |
| 2 | +import { CheckIcon, ChevronDown } from 'lucide-vue-next' |
| 3 | +import { ListboxContent, ListboxFilter, ListboxItem, ListboxItemIndicator, ListboxRoot, useFilter } from 'reka-ui' |
| 4 | +import { ref } from 'vue' |
| 5 | +import { Button } from '@/registry/new-york-v4/ui/button' |
| 6 | +import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from '@/registry/new-york-v4/ui/popover' |
| 7 | +import { TagsInput, TagsInputInput, TagsInputItem, TagsInputItemDelete, TagsInputItemText } from '@/registry/new-york-v4/ui/tags-input' |
| 8 | +
|
| 9 | +const frameworks = [ |
| 10 | + { value: 'next.js', label: 'Next.js' }, |
| 11 | + { value: 'sveltekit', label: 'SvelteKit' }, |
| 12 | + { value: 'nuxt', label: 'Nuxt' }, |
| 13 | + { value: 'remix', label: 'Remix' }, |
| 14 | + { value: 'astro', label: 'Astro' }, |
| 15 | +] |
| 16 | +
|
| 17 | +const searchTerm = ref('') |
| 18 | +const frameworksRef = ref(['Nuxt', 'Remix']) |
| 19 | +const open = ref(false) |
| 20 | +
|
| 21 | +const { contains } = useFilter({ sensitivity: 'base' }) |
| 22 | +
|
| 23 | +const filteredFrameworks = computed(() => |
| 24 | + searchTerm.value === '' |
| 25 | + ? frameworks |
| 26 | + : frameworks.filter(option => contains(option.label, searchTerm.value)), |
| 27 | +) |
| 28 | +
|
| 29 | +watch(searchTerm, (f) => { |
| 30 | + if (f) { |
| 31 | + open.value = true |
| 32 | + } |
| 33 | +}) |
| 34 | +</script> |
| 35 | + |
| 36 | +<template> |
| 37 | + <Popover v-model:open="open"> |
| 38 | + <ListboxRoot |
| 39 | + v-model="frameworksRef" |
| 40 | + highlight-on-hover |
| 41 | + multiple |
| 42 | + > |
| 43 | + <PopoverAnchor class="inline-flex w-[300px]"> |
| 44 | + <TagsInput v-slot="{ modelValue: tags }" v-model="frameworksRef" class="w-full"> |
| 45 | + <TagsInputItem v-for="item in tags" :key="item.toString()" :value="item.toString()"> |
| 46 | + <TagsInputItemText /> |
| 47 | + <TagsInputItemDelete /> |
| 48 | + </TagsInputItem> |
| 49 | + |
| 50 | + <ListboxFilter v-model="searchTerm" as-child> |
| 51 | + <TagsInputInput placeholder="Frameworks..." @keydown.enter.prevent @keydown.down="open = true" /> |
| 52 | + </ListboxFilter> |
| 53 | + |
| 54 | + <PopoverTrigger as-child> |
| 55 | + <Button size="icon-sm" variant="ghost" class="order-last self-start ml-auto"> |
| 56 | + <ChevronDown class="size-3.5" /> |
| 57 | + </Button> |
| 58 | + </PopoverTrigger> |
| 59 | + </TagsInput> |
| 60 | + </PopoverAnchor> |
| 61 | + |
| 62 | + <PopoverContent |
| 63 | + class="p-1" |
| 64 | + @open-auto-focus.prevent |
| 65 | + > |
| 66 | + <ListboxContent class="max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto empty:after:content-['No_options'] empty:p-1 empty:after:block" tabindex="0"> |
| 67 | + <!-- <CommandEmpty>No results found.</CommandEmpty> --> |
| 68 | + <ListboxItem |
| 69 | + v-for="item in filteredFrameworks" :key="item.value" class="data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground [&_svg:not([class*=\'text-\'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4" :value="item.label" @select="() => { |
| 70 | + searchTerm = '' |
| 71 | + }" |
| 72 | + > |
| 73 | + <span>{{ item.label }}</span> |
| 74 | + |
| 75 | + <ListboxItemIndicator |
| 76 | + class="ml-auto inline-flex items-center justify-center" |
| 77 | + > |
| 78 | + <CheckIcon /> |
| 79 | + </ListboxItemIndicator> |
| 80 | + </ListboxItem> |
| 81 | + </ListboxContent> |
| 82 | + </PopoverContent> |
| 83 | + </ListboxRoot> |
| 84 | + </Popover> |
| 85 | +</template> |
0 commit comments