|
1 | 1 | <template> |
2 | | - <PopoverRoot v-model:open="isOpen"> |
3 | | - <PopoverTrigger as-child> |
4 | | - <button |
5 | | - ref="triggerRef" |
| 2 | + <ComboboxRoot |
| 3 | + v-model="selectedItems" |
| 4 | + multiple |
| 5 | + by="value" |
| 6 | + :disabled |
| 7 | + ignore-filter |
| 8 | + :reset-search-term-on-select="false" |
| 9 | + > |
| 10 | + <ComboboxAnchor as-child> |
| 11 | + <ComboboxTrigger |
6 | 12 | v-bind="$attrs" |
7 | | - type="button" |
8 | | - :disabled |
9 | 13 | :aria-label="label || t('g.multiSelectDropdown')" |
10 | | - role="combobox" |
11 | | - :aria-expanded="isOpen" |
12 | | - aria-haspopup="listbox" |
13 | | - :tabindex="0" |
14 | 14 | :class=" |
15 | 15 | cn( |
16 | 16 | 'relative inline-flex cursor-pointer items-center select-none', |
|
49 | 49 | > |
50 | 50 | <i class="icon-[lucide--chevron-down] text-muted-foreground" /> |
51 | 51 | </div> |
52 | | - </button> |
53 | | - </PopoverTrigger> |
| 52 | + </ComboboxTrigger> |
| 53 | + </ComboboxAnchor> |
54 | 54 |
|
55 | | - <PopoverPortal> |
56 | | - <PopoverContent |
| 55 | + <ComboboxPortal> |
| 56 | + <ComboboxContent |
| 57 | + position="popper" |
57 | 58 | :side-offset="8" |
58 | 59 | align="start" |
59 | | - :style="{ minWidth: contentMinWidth }" |
60 | 60 | :class=" |
61 | 61 | cn( |
62 | 62 | 'z-3000 overflow-hidden', |
|
70 | 70 | 'data-[side=bottom]:slide-in-from-top-2' |
71 | 71 | ) |
72 | 72 | " |
73 | | - @open-auto-focus.prevent |
74 | 73 | > |
75 | 74 | <div |
76 | 75 | v-if="showSearchBox || showSelectedCount || showClearButton" |
77 | 76 | class="flex flex-col px-2 pt-2 pb-0" |
78 | 77 | > |
79 | | - <SearchInput |
| 78 | + <div |
80 | 79 | v-if="showSearchBox" |
81 | | - v-model="searchQuery" |
82 | | - :class="showSelectedCount || showClearButton ? 'mb-2' : ''" |
83 | | - :placeholder="searchPlaceholder" |
84 | | - size="sm" |
85 | | - /> |
| 80 | + :class=" |
| 81 | + cn( |
| 82 | + 'flex items-center gap-2 rounded-lg border border-solid border-border-default px-3 py-1.5', |
| 83 | + showSelectedCount || showClearButton ? 'mb-2' : '' |
| 84 | + ) |
| 85 | + " |
| 86 | + > |
| 87 | + <i |
| 88 | + class="icon-[lucide--search] shrink-0 text-sm text-muted-foreground" |
| 89 | + /> |
| 90 | + <ComboboxInput |
| 91 | + v-model="searchQuery" |
| 92 | + :placeholder="searchPlaceholder" |
| 93 | + class="w-full border-none bg-transparent text-sm outline-none" |
| 94 | + /> |
| 95 | + </div> |
86 | 96 | <div |
87 | 97 | v-if="showSelectedCount || showClearButton" |
88 | 98 | class="mt-2 flex items-center justify-between" |
|
109 | 119 | <div class="my-4 h-px bg-border-default" /> |
110 | 120 | </div> |
111 | 121 |
|
112 | | - <ListboxRoot |
113 | | - v-model="selectedItems" |
114 | | - multiple |
115 | | - by="value" |
| 122 | + <ComboboxViewport |
116 | 123 | :class=" |
117 | 124 | cn( |
118 | | - 'flex flex-col gap-0 p-0 text-sm outline-none', |
119 | | - 'scrollbar-custom overflow-y-auto' |
| 125 | + 'flex flex-col gap-0 p-0 text-sm', |
| 126 | + 'scrollbar-custom overflow-y-auto', |
| 127 | + 'min-w-(--reka-combobox-trigger-width)' |
120 | 128 | ) |
121 | 129 | " |
122 | 130 | :style="{ maxHeight: `min(${listMaxHeight}, 50vh)` }" |
123 | 131 | > |
124 | | - <ListboxContent> |
125 | | - <ListboxItem |
126 | | - v-for="opt in filteredOptions" |
127 | | - :key="opt.value" |
128 | | - :value="opt" |
129 | | - :class=" |
130 | | - cn( |
131 | | - 'flex h-10 cursor-pointer items-center gap-2 rounded-lg px-2 outline-none', |
132 | | - 'hover:bg-secondary-background-hover', |
133 | | - 'focus:bg-secondary-background-selected focus:hover:bg-secondary-background-selected' |
134 | | - ) |
135 | | - " |
136 | | - :style="popoverStyle" |
| 132 | + <ComboboxItem |
| 133 | + v-for="opt in filteredOptions" |
| 134 | + :key="opt.value" |
| 135 | + :value="opt" |
| 136 | + :class=" |
| 137 | + cn( |
| 138 | + 'group flex h-10 cursor-pointer items-center gap-2 rounded-lg px-2 outline-none', |
| 139 | + 'hover:bg-secondary-background-hover', |
| 140 | + 'data-highlighted:bg-secondary-background-selected data-highlighted:hover:bg-secondary-background-selected' |
| 141 | + ) |
| 142 | + " |
| 143 | + :style="popoverStyle" |
| 144 | + > |
| 145 | + <div |
| 146 | + class="flex size-4 shrink-0 items-center justify-center rounded-sm p-0.5 transition-all duration-200 group-data-[state=checked]:bg-primary-background group-data-[state=unchecked]:bg-secondary-background" |
137 | 147 | > |
138 | | - <div |
139 | | - class="flex size-4 shrink-0 items-center justify-center rounded-sm p-0.5 transition-all duration-200" |
140 | | - :class=" |
141 | | - isSelected(opt) |
142 | | - ? 'bg-primary-background' |
143 | | - : 'bg-secondary-background' |
144 | | - " |
145 | | - > |
| 148 | + <ComboboxItemIndicator> |
146 | 149 | <i |
147 | | - v-if="isSelected(opt)" |
148 | 150 | class="text-bold icon-[lucide--check] text-xs text-base-foreground" |
149 | 151 | /> |
150 | | - </div> |
151 | | - <span>{{ opt.name }}</span> |
152 | | - </ListboxItem> |
153 | | - <div |
154 | | - v-if="filteredOptions.length === 0" |
155 | | - class="px-3 pb-4 text-sm text-muted-foreground" |
156 | | - > |
157 | | - {{ $t('g.noResultsFound') }} |
| 152 | + </ComboboxItemIndicator> |
158 | 153 | </div> |
159 | | - </ListboxContent> |
160 | | - </ListboxRoot> |
161 | | - </PopoverContent> |
162 | | - </PopoverPortal> |
163 | | - </PopoverRoot> |
| 154 | + <span>{{ opt.name }}</span> |
| 155 | + </ComboboxItem> |
| 156 | + <ComboboxEmpty class="px-3 pb-4 text-sm text-muted-foreground"> |
| 157 | + {{ $t('g.noResultsFound') }} |
| 158 | + </ComboboxEmpty> |
| 159 | + </ComboboxViewport> |
| 160 | + </ComboboxContent> |
| 161 | + </ComboboxPortal> |
| 162 | + </ComboboxRoot> |
164 | 163 | </template> |
165 | 164 |
|
166 | 165 | <script setup lang="ts"> |
167 | 166 | import { useFuse } from '@vueuse/integrations/useFuse' |
168 | 167 | import type { UseFuseOptions } from '@vueuse/integrations/useFuse' |
169 | | -import { useElementSize } from '@vueuse/core' |
170 | 168 | import { |
171 | | - ListboxContent, |
172 | | - ListboxItem, |
173 | | - ListboxRoot, |
174 | | - PopoverContent, |
175 | | - PopoverPortal, |
176 | | - PopoverRoot, |
177 | | - PopoverTrigger |
| 169 | + ComboboxAnchor, |
| 170 | + ComboboxContent, |
| 171 | + ComboboxEmpty, |
| 172 | + ComboboxInput, |
| 173 | + ComboboxItem, |
| 174 | + ComboboxItemIndicator, |
| 175 | + ComboboxPortal, |
| 176 | + ComboboxRoot, |
| 177 | + ComboboxTrigger, |
| 178 | + ComboboxViewport |
178 | 179 | } from 'reka-ui' |
179 | | -import type { ComponentPublicInstance } from 'vue' |
180 | | -import { computed, ref } from 'vue' |
| 180 | +import { computed } from 'vue' |
181 | 181 | import { useI18n } from 'vue-i18n' |
182 | 182 |
|
183 | 183 | import Button from '@/components/ui/button/Button.vue' |
184 | | -import SearchInput from '@/components/ui/search-input/SearchInput.vue' |
185 | 184 | import { usePopoverSizing } from '@/composables/usePopoverSizing' |
186 | 185 | import { cn } from '@/utils/tailwindUtil' |
187 | 186 |
|
@@ -234,23 +233,13 @@ const selectedItems = defineModel<SelectOption[]>({ |
234 | 233 | const searchQuery = defineModel<string>('searchQuery', { default: '' }) |
235 | 234 |
|
236 | 235 | const { t } = useI18n() |
237 | | -const isOpen = ref(false) |
238 | | -const triggerRef = ref<ComponentPublicInstance | null>(null) |
239 | | -const { width: triggerWidth } = useElementSize(triggerRef) |
240 | | -const contentMinWidth = computed(() => |
241 | | - triggerWidth.value > 0 ? `${triggerWidth.value}px` : undefined |
242 | | -) |
243 | 236 | const selectedCount = computed(() => selectedItems.value.length) |
244 | 237 |
|
245 | 238 | const popoverStyle = usePopoverSizing({ |
246 | 239 | minWidth: popoverMinWidth, |
247 | 240 | maxWidth: popoverMaxWidth |
248 | 241 | }) |
249 | 242 |
|
250 | | -function isSelected(opt: SelectOption): boolean { |
251 | | - return selectedItems.value.some((item) => item.value === opt.value) |
252 | | -} |
253 | | -
|
254 | 243 | const fuseOptions: UseFuseOptions<SelectOption> = { |
255 | 244 | fuseOptions: { |
256 | 245 | keys: ['name', 'value'], |
|
0 commit comments