1
1
<template >
2
2
<div >
3
- <div
4
- class =" flex items-center justify-between text-ink-gray-7 [& >div]:w-full"
5
- >
6
- <Popover v-model:show =" showOptions" >
7
- <template #target =" { togglePopover } " >
8
- <TextInput
3
+ <!-- Combobox Input -->
4
+ <div class =" flex items-center w-full text-ink-gray-8 [& >div]:w-full" >
5
+ <ComboboxRoot
6
+ :model-value =" tempSelection"
7
+ :open =" showOptions"
8
+ @update:open =" (o) => (showOptions = o)"
9
+ @update:modelValue =" onSelect"
10
+ :ignore-filter =" true"
11
+ >
12
+ <ComboboxAnchor
13
+ class =" flex w-full text-base items-center gap-1 rounded border border-outline-gray-2 bg-surface-white hover:border-outline-gray-3 focus:border-outline-gray-4 focus:ring-0 focus-visible:ring-2 focus-visible:ring-outline-gray-3 px-2 py-1"
14
+ :class =" [size === 'sm' ? 'h-7' : 'h-8 ', inputClass]"
15
+ @click =" showOptions = true"
16
+ >
17
+ <ComboboxInput
9
18
ref =" search"
10
- type =" text"
11
- :size =" size"
12
- class =" w-full"
13
- variant =" outline"
14
- v-model =" query"
15
- :debounce =" 300"
19
+ autocomplete =" off"
20
+ class =" bg-transparent p-0 outline-none border-0 text-base text-ink-gray-8 h-full placeholder:text-ink-gray-4 w-full focus:outline-none focus:ring-0 focus:border-0"
16
21
:placeholder =" placeholder"
17
- @click =" togglePopover"
18
- @keydown =" onKeydown"
22
+ :value =" query"
23
+ @input =" onInput"
24
+ @keydown.enter.prevent =" handleEnter"
25
+ @keydown.escape.stop =" showOptions = false"
26
+ />
27
+ <FeatherIcon
28
+ name =" chevron-down"
29
+ class =" h-4 text-ink-gray-5 cursor-pointer"
30
+ @click.stop =" showOptions = !showOptions"
31
+ />
32
+ </ComboboxAnchor >
33
+ <ComboboxPortal >
34
+ <ComboboxContent
35
+ class =" z-10 mt-1 min-w-48 w-full max-w-md bg-surface-modal overflow-hidden rounded-lg shadow-2xl ring-1 ring-black ring-opacity-5"
36
+ position =" popper"
37
+ :align =" 'start'"
38
+ @openAutoFocus.prevent
39
+ @closeAutoFocus.prevent
19
40
>
20
- <template #suffix >
21
- <FeatherIcon
22
- name =" chevron-down"
23
- class =" h-4 text-ink-gray-5"
24
- @click.stop =" togglePopover()"
25
- />
26
- </template >
27
- </TextInput >
28
- </template >
29
- <template #body =" { isOpen } " >
30
- <div v-show =" isOpen" >
31
- <div
32
- class =" mt-1 rounded-lg bg-surface-modal shadow-2xl ring-1 ring-black ring-opacity-5 focus:outline-none"
33
- >
34
- <ul
35
- v-if =" options.length"
36
- role =" listbox"
37
- class =" p-1.5 max-h-[12rem] overflow-y-auto"
38
- >
39
- <li
40
- v-for =" (option, idx) in options"
41
- :key =" option.value"
42
- role =" option"
43
- :aria-selected =" idx === highlightIndex"
44
- @click =" selectOption(option)"
45
- @mouseenter =" highlightIndex = idx"
46
- class =" flex cursor-pointer items-center rounded px-2 py-1 text-base"
47
- :class =" { 'bg-surface-gray-3': idx === highlightIndex }"
48
- >
49
- <UserAvatar class =" mr-2" :user =" option.value" size =" lg" />
50
- <div class =" flex flex-col gap-1 p-1 text-ink-gray-8" >
51
- <div class =" text-base font-medium" >
52
- {{ option.label }}
53
- </div >
54
- <div class =" text-sm text-ink-gray-5" >
55
- {{ option.value }}
56
- </div >
57
- </div >
58
- </li >
59
- </ul >
60
- <div
61
- v-else
41
+ <ComboboxViewport class =" max-h-60 overflow-auto p-1.5" >
42
+ <ComboboxEmpty
62
43
class =" flex gap-2 rounded px-2 py-1 text-base text-ink-gray-5"
63
44
>
64
45
<FeatherIcon v-if =" fetchContacts" name =" search" class =" h-4" />
65
- {{
66
- fetchContacts
67
- ? __('No results found')
68
- : __('Type an email address to add attendee')
69
- }}
70
- </div >
71
- </div >
72
- </div >
73
- </template >
74
- </Popover >
46
+ {{ emptyStateText }}
47
+ </ComboboxEmpty >
48
+ <ComboboxItem
49
+ v-for =" option in options"
50
+ :key =" option.value"
51
+ :value =" option.value"
52
+ class =" text-base leading-none text-ink-gray-7 rounded flex items-center px-2 py-1 relative select-none data-[highlighted]:outline-none data-[highlighted]:bg-surface-gray-3 cursor-pointer"
53
+ @mousedown.prevent =" onSelect(option.value, option)"
54
+ >
55
+ <UserAvatar class =" mr-2" :user =" option.value" size =" lg" />
56
+ <div class =" flex flex-col gap-1 p-1 text-ink-gray-8" >
57
+ <div class =" text-base font-medium" >{{ option.label }}</div >
58
+ <div class =" text-sm text-ink-gray-5" >{{ option.value }}</div >
59
+ </div >
60
+ </ComboboxItem >
61
+ </ComboboxViewport >
62
+ </ComboboxContent >
63
+ </ComboboxPortal >
64
+ </ComboboxRoot >
75
65
</div >
66
+
67
+ <!-- Selected Attendees -->
76
68
<div
77
69
v-if =" values.length"
78
70
class =" flex flex-col gap-2 mt-2 max-h-[165px] overflow-y-auto"
105
97
106
98
<script setup>
107
99
import UserAvatar from ' @/components/UserAvatar.vue'
108
- import { createResource , TextInput , Popover } from ' frappe-ui'
109
- import { ref , computed , nextTick , watch } from ' vue'
100
+ import { createResource } from ' frappe-ui'
101
+ import {
102
+ ComboboxRoot ,
103
+ ComboboxAnchor ,
104
+ ComboboxInput ,
105
+ ComboboxPortal ,
106
+ ComboboxContent ,
107
+ ComboboxViewport ,
108
+ ComboboxItem ,
109
+ ComboboxEmpty ,
110
+ } from ' reka-ui'
111
+ import { ref , computed , nextTick } from ' vue'
110
112
import { watchDebounced } from ' @vueuse/core'
111
113
112
114
const props = defineProps ({
@@ -154,7 +156,7 @@ const query = ref('')
154
156
const text = ref (' ' )
155
157
const showOptions = ref (false )
156
158
const optionsRef = ref (null )
157
- const highlightIndex = ref (- 1 )
159
+ const tempSelection = ref (null )
158
160
159
161
const metaByEmail = computed (() => {
160
162
const out = {}
@@ -225,6 +227,12 @@ const options = computed(() => {
225
227
return searchedContacts || []
226
228
})
227
229
230
+ const emptyStateText = computed (() =>
231
+ props .fetchContacts
232
+ ? __ (' No results found' )
233
+ : __ (' Type an email address to add attendee' ),
234
+ )
235
+
228
236
function reload (val ) {
229
237
if (! props .fetchContacts ) return
230
238
@@ -234,34 +242,38 @@ function reload(val) {
234
242
filterOptions .reload ()
235
243
}
236
244
237
- watch (
238
- () => options .value ,
239
- () => {
240
- highlightIndex .value = options .value .length ? 0 : - 1
241
- },
242
- )
243
-
244
- function selectOption (option ) {
245
- if (! option) return
246
- addValue (option)
247
- ! error .value && (query .value = ' ' )
248
- showOptions .value = false
249
- }
250
-
251
- function onKeydown (e ) {
252
- if (e .key === ' Enter' ) {
253
- if (highlightIndex .value >= 0 && options .value [highlightIndex .value ]) {
254
- selectOption (options .value [highlightIndex .value ])
255
- } else if (query .value ) {
256
- // Add entered email directly
257
- selectOption ({ name: ' new' , label: query .value , value: query .value })
245
+ function onSelect (val , fullOption = null ) {
246
+ if (! val) return
247
+ const optionObj = fullOption ||
248
+ options .value .find ((o ) => o .value === val) || {
249
+ name: ' new' ,
250
+ label: val,
251
+ value: val,
258
252
}
259
- e .preventDefault ()
260
- } else if (e .key === ' Escape' ) {
253
+ addValue (optionObj)
254
+ if (! error .value ) {
255
+ query .value = ' '
256
+ tempSelection .value = null
261
257
showOptions .value = false
258
+ nextTick (() => setFocus ())
259
+ }
260
+ }
261
+
262
+ function handleEnter () {
263
+ if (query .value ) {
264
+ onSelect (query .value , {
265
+ name: ' new' ,
266
+ label: query .value ,
267
+ value: query .value ,
268
+ })
262
269
}
263
270
}
264
271
272
+ function onInput (e ) {
273
+ query .value = e .target .value
274
+ showOptions .value = true
275
+ }
276
+
265
277
const addValue = (option ) => {
266
278
// Safeguard for falsy option
267
279
if (! option || ! option .value ) return
@@ -315,7 +327,7 @@ const removeValue = (email) => {
315
327
}
316
328
317
329
function setFocus () {
318
- search .value . $el . focus ()
330
+ search .value ? . focus ? . ()
319
331
}
320
332
321
333
defineExpose ({ setFocus })
0 commit comments