@saasmakers/ui 2.0.83 → 2.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/components/fields/FieldSelect.vue +330 -218
- package/app/types/fields.d.ts +3 -3
- package/package.json +2 -1
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
2
|
import type { FieldInput } from '#components'
|
|
3
|
+
import { autoUpdate, flip, size as floatingSize, offset, shift, useFloating } from '@floating-ui/vue'
|
|
3
4
|
import { vOnClickOutside } from '@vueuse/components'
|
|
4
|
-
import
|
|
5
|
+
import { onKeyStroke } from '@vueuse/core'
|
|
6
|
+
import type { FieldSelect, FieldSelectMaxHeight, FieldSelectOption, FieldSize } from '../../types/fields'
|
|
5
7
|
|
|
6
8
|
const props = withDefaults(defineProps<Omit<FieldSelect, 'modelValue'>>(), {
|
|
7
9
|
border: 'full',
|
|
8
10
|
caret: true,
|
|
9
11
|
columns: () => [],
|
|
10
12
|
description: '',
|
|
11
|
-
direction: '
|
|
13
|
+
direction: 'auto',
|
|
12
14
|
disabled: false,
|
|
13
|
-
hideCaret: false,
|
|
14
15
|
hideError: false,
|
|
15
|
-
|
|
16
|
+
iconOnly: false,
|
|
16
17
|
label: '',
|
|
17
18
|
labelIcon: undefined,
|
|
18
19
|
maxHeight: 'xs',
|
|
@@ -25,64 +26,81 @@ const props = withDefaults(defineProps<Omit<FieldSelect, 'modelValue'>>(), {
|
|
|
25
26
|
searchPlaceholder: '',
|
|
26
27
|
size: 'base',
|
|
27
28
|
square: false,
|
|
29
|
+
uppercase: true,
|
|
28
30
|
validation: undefined,
|
|
29
31
|
})
|
|
30
32
|
|
|
31
33
|
const emit = defineEmits<{
|
|
32
|
-
change: [event:
|
|
33
|
-
click: [event: MouseEvent, value: string]
|
|
34
|
-
optionClick: [event: MouseEvent, value: string]
|
|
34
|
+
change: [event: Event, value: string]
|
|
35
35
|
}>()
|
|
36
36
|
|
|
37
|
+
const menuLayout = {
|
|
38
|
+
gap: 4,
|
|
39
|
+
maxHeight: {
|
|
40
|
+
lg: 512,
|
|
41
|
+
md: 448,
|
|
42
|
+
sm: 384,
|
|
43
|
+
xs: 320,
|
|
44
|
+
} satisfies Record<FieldSelectMaxHeight, number>,
|
|
45
|
+
viewportPadding: 8,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const textSizeByFieldSize = {
|
|
49
|
+
base: 'sm',
|
|
50
|
+
lg: 'base',
|
|
51
|
+
sm: 'xs',
|
|
52
|
+
} satisfies Record<FieldSize, BaseSize>
|
|
53
|
+
|
|
37
54
|
const { t } = useI18n()
|
|
38
55
|
const id = useId()
|
|
56
|
+
const labelId = `${id}-label`
|
|
57
|
+
const menuId = `${id}-options`
|
|
39
58
|
const modelValue = defineModel<FieldSelect['modelValue']>({ default: '' })
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
const
|
|
59
|
+
const isOpen = ref(false)
|
|
60
|
+
const searchQuery = ref('')
|
|
61
|
+
const searchInputRef = ref<InstanceType<typeof FieldInput>>()
|
|
62
|
+
const labelRef = ref<HTMLElement>()
|
|
63
|
+
const triggerRef = ref<HTMLElement>()
|
|
64
|
+
const menuRef = ref<HTMLElement>()
|
|
43
65
|
|
|
44
|
-
|
|
66
|
+
let hoverCloseTimeout: ReturnType<typeof setTimeout> | undefined
|
|
67
|
+
let isClosing = false
|
|
68
|
+
|
|
69
|
+
const resolvedColumns = computed(() => {
|
|
45
70
|
if (props.columns.length > 0) {
|
|
46
71
|
return props.columns
|
|
47
72
|
}
|
|
48
73
|
|
|
49
|
-
return [{ options:
|
|
74
|
+
return [{ options: props.options }]
|
|
50
75
|
})
|
|
51
76
|
|
|
52
|
-
const
|
|
53
|
-
const options: FieldSelectOption[] = []
|
|
54
|
-
|
|
77
|
+
const allOptions = computed(() => {
|
|
55
78
|
if (props.columns.length > 0) {
|
|
56
|
-
|
|
57
|
-
for (const columnOption of column.options) {
|
|
58
|
-
options.push(columnOption)
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
else {
|
|
63
|
-
for (const columnOption of props.options) {
|
|
64
|
-
options.push(columnOption)
|
|
65
|
-
}
|
|
79
|
+
return props.columns.flatMap(column => column.options)
|
|
66
80
|
}
|
|
67
81
|
|
|
68
|
-
return options
|
|
82
|
+
return props.options
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
const isMultiColumn = computed(() => {
|
|
86
|
+
return resolvedColumns.value.length >= 2
|
|
69
87
|
})
|
|
70
88
|
|
|
71
|
-
const
|
|
72
|
-
return normalizeText(
|
|
89
|
+
const normalizedSearchQuery = computed(() => {
|
|
90
|
+
return normalizeText(searchQuery.value.trim())
|
|
73
91
|
})
|
|
74
92
|
|
|
75
93
|
const filteredColumns = computed(() => {
|
|
76
|
-
if (!props.searchable || !
|
|
77
|
-
return
|
|
94
|
+
if (!props.searchable || !normalizedSearchQuery.value) {
|
|
95
|
+
return resolvedColumns.value
|
|
78
96
|
}
|
|
79
97
|
|
|
80
|
-
return
|
|
98
|
+
return resolvedColumns.value
|
|
81
99
|
.map(column => ({
|
|
82
100
|
...column,
|
|
83
101
|
options: column.options.filter((option) => {
|
|
84
|
-
return normalizeText(option.text).includes(
|
|
85
|
-
|| normalizeText(option.value).includes(
|
|
102
|
+
return normalizeText(option.text).includes(normalizedSearchQuery.value)
|
|
103
|
+
|| normalizeText(option.value).includes(normalizedSearchQuery.value)
|
|
86
104
|
}),
|
|
87
105
|
}))
|
|
88
106
|
.filter(column => column.options.length > 0)
|
|
@@ -95,252 +113,346 @@ const firstFilteredOption = computed(() => {
|
|
|
95
113
|
})
|
|
96
114
|
|
|
97
115
|
const selectedOption = computed(() => {
|
|
98
|
-
return
|
|
116
|
+
return allOptions.value.find((option) => {
|
|
99
117
|
return option.value === modelValue.value
|
|
100
118
|
})
|
|
101
119
|
})
|
|
102
120
|
|
|
103
|
-
const textSize = computed
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
121
|
+
const textSize = computed(() => {
|
|
122
|
+
return textSizeByFieldSize[props.size]
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
const placement = computed(() => {
|
|
126
|
+
return props.direction === 'top' ? 'top-start' : 'bottom-start'
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
const middleware = computed(() => {
|
|
130
|
+
return [
|
|
131
|
+
offset(menuLayout.gap),
|
|
132
|
+
...(props.direction === 'auto' ? [flip()] : []),
|
|
133
|
+
shift({ padding: menuLayout.viewportPadding }),
|
|
134
|
+
floatingSize({
|
|
135
|
+
apply({ availableHeight, availableWidth, elements, rects }) {
|
|
136
|
+
const preferredMaxHeight = menuLayout.maxHeight[props.maxHeight]
|
|
137
|
+
const maxHeight = Math.max(0, Math.min(preferredMaxHeight, availableHeight))
|
|
138
|
+
|
|
139
|
+
elements.floating.style.setProperty('--field-select-menu-max-height', `${maxHeight}px`)
|
|
140
|
+
|
|
141
|
+
Object.assign(elements.floating.style, {
|
|
142
|
+
maxWidth: `${availableWidth}px`,
|
|
143
|
+
minWidth: `${rects.reference.width}px`,
|
|
144
|
+
})
|
|
145
|
+
},
|
|
146
|
+
padding: menuLayout.viewportPadding,
|
|
147
|
+
}),
|
|
148
|
+
]
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
const { floatingStyles, isPositioned } = useFloating(triggerRef, menuRef, {
|
|
152
|
+
middleware,
|
|
153
|
+
placement,
|
|
154
|
+
strategy: 'fixed',
|
|
155
|
+
whileElementsMounted: autoUpdate,
|
|
114
156
|
})
|
|
115
157
|
|
|
116
|
-
|
|
117
|
-
if (
|
|
118
|
-
|
|
158
|
+
onKeyStroke('Escape', (event) => {
|
|
159
|
+
if (!isOpen.value) {
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
event.preventDefault()
|
|
164
|
+
close({ restoreFocus: true })
|
|
165
|
+
}, { target: window })
|
|
119
166
|
|
|
120
|
-
|
|
167
|
+
watch(isOpen, async (opened) => {
|
|
168
|
+
if (!opened || !props.searchable) {
|
|
169
|
+
return
|
|
121
170
|
}
|
|
171
|
+
|
|
172
|
+
await nextTick()
|
|
173
|
+
|
|
174
|
+
searchInputRef.value?.focus()
|
|
122
175
|
})
|
|
123
176
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
177
|
+
watch(() => props.disabled, (isDisabled) => {
|
|
178
|
+
if (isDisabled) {
|
|
179
|
+
close()
|
|
180
|
+
}
|
|
181
|
+
})
|
|
127
182
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
183
|
+
onBeforeUnmount(() => {
|
|
184
|
+
clearHoverCloseTimeout()
|
|
185
|
+
})
|
|
131
186
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
187
|
+
function clearHoverCloseTimeout() {
|
|
188
|
+
if (hoverCloseTimeout !== undefined) {
|
|
189
|
+
clearTimeout(hoverCloseTimeout)
|
|
190
|
+
|
|
191
|
+
hoverCloseTimeout = undefined
|
|
135
192
|
}
|
|
136
193
|
}
|
|
137
194
|
|
|
138
|
-
function
|
|
139
|
-
|
|
140
|
-
|
|
195
|
+
function close(options?: {
|
|
196
|
+
restoreFocus?: boolean
|
|
197
|
+
}) {
|
|
198
|
+
clearHoverCloseTimeout()
|
|
199
|
+
|
|
200
|
+
isClosing = true
|
|
201
|
+
isOpen.value = false
|
|
202
|
+
searchQuery.value = ''
|
|
203
|
+
|
|
204
|
+
if (options?.restoreFocus) {
|
|
205
|
+
triggerRef.value?.focus()
|
|
141
206
|
}
|
|
207
|
+
|
|
208
|
+
isClosing = false
|
|
142
209
|
}
|
|
143
210
|
|
|
144
|
-
function
|
|
145
|
-
|
|
146
|
-
|
|
211
|
+
function isEventTargetInside(event: FocusEvent | MouseEvent) {
|
|
212
|
+
const nextTarget = event.relatedTarget
|
|
213
|
+
|
|
214
|
+
if (!(nextTarget instanceof Node)) {
|
|
215
|
+
return false
|
|
147
216
|
}
|
|
217
|
+
|
|
218
|
+
return menuRef.value?.contains(nextTarget) === true || triggerRef.value?.contains(nextTarget) === true
|
|
148
219
|
}
|
|
149
220
|
|
|
150
|
-
function
|
|
151
|
-
|
|
152
|
-
|
|
221
|
+
function isSelected(option: FieldSelectOption) {
|
|
222
|
+
return option.value === selectedOption.value?.value
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function onHoverEnd(event: FocusEvent | MouseEvent) {
|
|
226
|
+
if (isClosing || !props.openOnHover || props.disabled || isEventTargetInside(event)) {
|
|
227
|
+
return
|
|
153
228
|
}
|
|
229
|
+
|
|
230
|
+
clearHoverCloseTimeout()
|
|
231
|
+
|
|
232
|
+
hoverCloseTimeout = setTimeout(() => {
|
|
233
|
+
isOpen.value = false
|
|
234
|
+
hoverCloseTimeout = undefined
|
|
235
|
+
}, 100)
|
|
154
236
|
}
|
|
155
237
|
|
|
156
|
-
function
|
|
157
|
-
if (props.openOnHover
|
|
158
|
-
|
|
238
|
+
function onHoverStart() {
|
|
239
|
+
if (isClosing || !props.openOnHover || props.disabled) {
|
|
240
|
+
return
|
|
159
241
|
}
|
|
242
|
+
|
|
243
|
+
clearHoverCloseTimeout()
|
|
244
|
+
|
|
245
|
+
isOpen.value = true
|
|
160
246
|
}
|
|
161
247
|
|
|
162
|
-
function
|
|
163
|
-
|
|
164
|
-
if (selectedOption.value?.value === option.value) {
|
|
165
|
-
reset()
|
|
166
|
-
}
|
|
167
|
-
else {
|
|
168
|
-
selectOption(event, option.value)
|
|
169
|
-
}
|
|
248
|
+
function onLabelClick() {
|
|
249
|
+
toggle()
|
|
170
250
|
|
|
171
|
-
|
|
251
|
+
if (isOpen.value) {
|
|
252
|
+
triggerRef.value?.focus()
|
|
172
253
|
}
|
|
173
254
|
}
|
|
174
255
|
|
|
175
|
-
function
|
|
176
|
-
|
|
177
|
-
|
|
256
|
+
function onOptionClick(event: Event, option: FieldSelectOption) {
|
|
257
|
+
const restoreFocus = event instanceof KeyboardEvent
|
|
258
|
+
|
|
259
|
+
if (isSelected(option)) {
|
|
260
|
+
close({ restoreFocus })
|
|
261
|
+
|
|
262
|
+
return
|
|
178
263
|
}
|
|
264
|
+
|
|
265
|
+
selectOption(event, option.value, restoreFocus)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function onOptionKeydown(event: KeyboardEvent, option: FieldSelectOption) {
|
|
269
|
+
onOptionClick(event, option)
|
|
179
270
|
}
|
|
180
271
|
|
|
181
272
|
function onSearchKeydown(event: KeyboardEvent) {
|
|
182
|
-
if (event.key
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
else if (event.key === 'Enter' && !props.disabled && firstFilteredOption.value) {
|
|
186
|
-
event.preventDefault()
|
|
187
|
-
onOptionClick(event as unknown as MouseEvent, firstFilteredOption.value)
|
|
273
|
+
if (event.key !== 'Enter' || !firstFilteredOption.value) {
|
|
274
|
+
return
|
|
188
275
|
}
|
|
276
|
+
|
|
277
|
+
event.preventDefault()
|
|
278
|
+
onOptionClick(event, firstFilteredOption.value)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function onTriggerClick() {
|
|
282
|
+
toggle()
|
|
189
283
|
}
|
|
190
284
|
|
|
191
|
-
function
|
|
192
|
-
|
|
193
|
-
searchRaw.value = ''
|
|
285
|
+
function onTriggerKeydown() {
|
|
286
|
+
toggle()
|
|
194
287
|
}
|
|
195
288
|
|
|
196
|
-
function selectOption(event:
|
|
289
|
+
function selectOption(event: Event, value: string, restoreFocus = false) {
|
|
197
290
|
modelValue.value = value
|
|
198
291
|
|
|
199
292
|
emit('change', event, value)
|
|
200
|
-
|
|
293
|
+
close({ restoreFocus })
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function toggle() {
|
|
297
|
+
if (props.disabled) {
|
|
298
|
+
return
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
isOpen.value = props.openOnHover ? true : !isOpen.value
|
|
201
302
|
}
|
|
202
303
|
</script>
|
|
203
304
|
|
|
204
305
|
<template>
|
|
205
306
|
<div
|
|
206
307
|
class="flex flex-col"
|
|
207
|
-
:class="{ 'w-fit':
|
|
308
|
+
:class="{ 'w-fit': iconOnly || square }"
|
|
208
309
|
>
|
|
209
|
-
<
|
|
310
|
+
<div
|
|
210
311
|
v-if="label"
|
|
211
|
-
:
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
312
|
+
:id="labelId"
|
|
313
|
+
ref="labelRef"
|
|
314
|
+
>
|
|
315
|
+
<FieldLabel
|
|
316
|
+
:disabled="disabled"
|
|
317
|
+
:for-field="id"
|
|
318
|
+
has-margin-bottom
|
|
319
|
+
:icon="labelIcon"
|
|
320
|
+
:label="label"
|
|
321
|
+
:required="required"
|
|
322
|
+
:size="size"
|
|
323
|
+
@click="onLabelClick"
|
|
324
|
+
/>
|
|
325
|
+
</div>
|
|
220
326
|
|
|
221
327
|
<div
|
|
222
|
-
v-on-click-outside="
|
|
328
|
+
v-on-click-outside="[() => close(), { ignore: [menuRef, labelRef] }]"
|
|
223
329
|
class="text-left"
|
|
224
330
|
:class="{
|
|
225
331
|
'field-disabled': disabled,
|
|
226
332
|
'cursor-pointer': !disabled,
|
|
227
|
-
'
|
|
333
|
+
'shadow-sm': border === 'full',
|
|
334
|
+
'w-fit': iconOnly || square,
|
|
335
|
+
'field-uppercase': uppercase,
|
|
228
336
|
}"
|
|
229
337
|
>
|
|
230
338
|
<div
|
|
231
|
-
|
|
339
|
+
:id="id"
|
|
340
|
+
ref="triggerRef"
|
|
341
|
+
class="group flex items-center rounded-lg bg-white outline-none dark:bg-gray-900"
|
|
232
342
|
:class="{
|
|
233
|
-
'
|
|
234
|
-
'
|
|
343
|
+
'base-box': border === 'full',
|
|
344
|
+
'base-border-b': border === 'bottom',
|
|
345
|
+
'border-0': border === 'none',
|
|
346
|
+
'base-border-active': isOpen,
|
|
347
|
+
'base-border-hover': !isOpen && !disabled,
|
|
348
|
+
'base-border-interactive-focus-visible': !disabled,
|
|
349
|
+
'text-xs h-sm': size === 'sm' && !square,
|
|
350
|
+
'text-sm h-base': size === 'base' && !square,
|
|
351
|
+
'text-base h-lg': size === 'lg' && !square,
|
|
352
|
+
'text-xs size-sm': size === 'sm' && square,
|
|
353
|
+
'text-sm size-base': size === 'base' && square,
|
|
354
|
+
'text-base size-lg': size === 'lg' && square,
|
|
355
|
+
'px-sm': padding && !iconOnly && !square && size === 'sm',
|
|
356
|
+
'px-base': padding && !iconOnly && !square && size === 'base',
|
|
357
|
+
'px-lg': padding && !iconOnly && !square && size === 'lg',
|
|
358
|
+
'px-2': padding && iconOnly && !square,
|
|
359
|
+
'inline-flex w-fit gap-1': iconOnly && !square,
|
|
360
|
+
'justify-center': square,
|
|
235
361
|
}"
|
|
362
|
+
role="button"
|
|
363
|
+
:aria-controls="isOpen ? menuId : undefined"
|
|
364
|
+
:aria-disabled="disabled"
|
|
365
|
+
:aria-expanded="isOpen"
|
|
366
|
+
aria-haspopup="listbox"
|
|
367
|
+
:aria-labelledby="label ? labelId : undefined"
|
|
368
|
+
tabindex="0"
|
|
369
|
+
@click="onTriggerClick"
|
|
370
|
+
@keydown.enter.prevent="onTriggerKeydown"
|
|
371
|
+
@keydown.space.prevent="onTriggerKeydown"
|
|
372
|
+
@mouseenter="onHoverStart"
|
|
373
|
+
@mouseleave="onHoverEnd"
|
|
374
|
+
@focusin="onHoverStart"
|
|
375
|
+
@focusout="onHoverEnd"
|
|
236
376
|
>
|
|
237
|
-
<
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
'
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
'
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
'px-2': padding && hideLabel && !square,
|
|
256
|
-
'inline-flex w-fit gap-1': hideLabel && !square,
|
|
257
|
-
'justify-center': square,
|
|
258
|
-
}"
|
|
259
|
-
role="button"
|
|
260
|
-
:aria-disabled="disabled"
|
|
261
|
-
tabindex="0"
|
|
262
|
-
@click="onContainerClick"
|
|
263
|
-
@keypress.prevent="onContainerKeypress"
|
|
264
|
-
@mouseenter="onMouseEnterOrFocus"
|
|
265
|
-
@mouseleave="onMouseLeaveOrBlur"
|
|
266
|
-
@focusin="onMouseEnterOrFocus"
|
|
267
|
-
@focusout="onMouseLeaveOrBlur"
|
|
268
|
-
>
|
|
269
|
-
<template v-if="selectedOption">
|
|
270
|
-
<BaseEmoji
|
|
271
|
-
v-if="selectedOption.emoji"
|
|
272
|
-
class="pointer-events-none shrink-0"
|
|
273
|
-
:class="{ 'mr-2': !hideLabel && !square }"
|
|
274
|
-
:clickable="false"
|
|
275
|
-
:emoji="selectedOption.emoji"
|
|
276
|
-
size="sm"
|
|
277
|
-
/>
|
|
278
|
-
|
|
279
|
-
<BaseIcon
|
|
280
|
-
v-else-if="selectedOption.icon"
|
|
281
|
-
class="pointer-events-none"
|
|
282
|
-
:class="{ 'mr-2': !hideLabel && !square }"
|
|
283
|
-
:color="selectedOption.iconColor"
|
|
284
|
-
:icon="selectedOption.icon"
|
|
285
|
-
:size="textSize"
|
|
286
|
-
/>
|
|
287
|
-
|
|
288
|
-
<span
|
|
289
|
-
v-if="selectedOption.text && !hideLabel && !square"
|
|
290
|
-
class="flex-1 truncate font-medium"
|
|
291
|
-
>
|
|
292
|
-
{{ selectedOption.text }}
|
|
293
|
-
</span>
|
|
294
|
-
</template>
|
|
377
|
+
<template v-if="selectedOption">
|
|
378
|
+
<BaseEmoji
|
|
379
|
+
v-if="selectedOption.emoji"
|
|
380
|
+
class="pointer-events-none shrink-0"
|
|
381
|
+
:class="{ 'mr-2': !iconOnly && !square }"
|
|
382
|
+
:clickable="false"
|
|
383
|
+
:emoji="selectedOption.emoji"
|
|
384
|
+
size="sm"
|
|
385
|
+
/>
|
|
386
|
+
|
|
387
|
+
<BaseIcon
|
|
388
|
+
v-else-if="selectedOption.icon"
|
|
389
|
+
class="pointer-events-none"
|
|
390
|
+
:class="{ 'mr-2': !iconOnly && !square }"
|
|
391
|
+
:color="selectedOption.iconColor"
|
|
392
|
+
:icon="selectedOption.icon"
|
|
393
|
+
:size="textSize"
|
|
394
|
+
/>
|
|
295
395
|
|
|
296
396
|
<span
|
|
297
|
-
v-
|
|
298
|
-
class="flex-1 truncate
|
|
397
|
+
v-if="selectedOption.text && !iconOnly && !square"
|
|
398
|
+
class="flex-1 truncate font-medium"
|
|
299
399
|
>
|
|
300
|
-
{{
|
|
400
|
+
{{ selectedOption.text }}
|
|
301
401
|
</span>
|
|
402
|
+
</template>
|
|
302
403
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
404
|
+
<span
|
|
405
|
+
v-else-if="placeholder"
|
|
406
|
+
class="flex-1 truncate text-gray-600 font-medium dark:text-gray-400 group-hover:text-gray-900 dark:group-hover:text-gray-100"
|
|
407
|
+
>
|
|
408
|
+
{{ placeholder }}
|
|
409
|
+
</span>
|
|
410
|
+
|
|
411
|
+
<BaseIcon
|
|
412
|
+
v-if="caret && !square"
|
|
413
|
+
class="flex-initial"
|
|
414
|
+
:class="{
|
|
415
|
+
'ml-2': !iconOnly,
|
|
416
|
+
'rotate-180': isOpen,
|
|
417
|
+
}"
|
|
418
|
+
color="gray"
|
|
419
|
+
:icon="getLayerIcon('arrowDown')"
|
|
420
|
+
/>
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
314
423
|
|
|
424
|
+
<Teleport to="body">
|
|
425
|
+
<div
|
|
426
|
+
v-if="isOpen"
|
|
427
|
+
ref="menuRef"
|
|
428
|
+
class="z-[70] cursor-default"
|
|
429
|
+
:style="{
|
|
430
|
+
...floatingStyles,
|
|
431
|
+
visibility: isPositioned ? 'visible' : 'hidden',
|
|
432
|
+
}"
|
|
433
|
+
@mouseenter="onHoverStart"
|
|
434
|
+
@mouseleave="onHoverEnd"
|
|
435
|
+
@focusin="onHoverStart"
|
|
436
|
+
@focusout="onHoverEnd"
|
|
437
|
+
>
|
|
315
438
|
<Motion
|
|
316
|
-
v-if="opened"
|
|
317
439
|
:animate="fadeIn.animate"
|
|
318
440
|
as="div"
|
|
319
|
-
class="absolute left-0 right-0 z-10 mt-0.5 cursor-default"
|
|
320
|
-
:class="{
|
|
321
|
-
'top-0': direction === 'bottom',
|
|
322
|
-
'bottom-0': direction === 'top',
|
|
323
|
-
}"
|
|
324
441
|
:initial="fadeIn.initial"
|
|
325
442
|
>
|
|
326
443
|
<div
|
|
327
|
-
|
|
444
|
+
:id="menuId"
|
|
445
|
+
class="options min-w-xs overflow-y-auto border base-border-active rounded-lg bg-white text-gray-600 font-medium dark:bg-gray-900 dark:text-gray-400"
|
|
328
446
|
:class="{
|
|
329
|
-
'text-xs
|
|
330
|
-
'text-sm
|
|
331
|
-
'text-base
|
|
332
|
-
'
|
|
333
|
-
'
|
|
334
|
-
'mt-10': ((size === 'sm' && padding) || (size === 'base' && !padding)) && direction === 'bottom',
|
|
335
|
-
'mt-12': size === 'base' && padding && direction === 'bottom',
|
|
336
|
-
'mb-8': size === 'sm' && !padding && direction === 'top',
|
|
337
|
-
'mb-10': ((size === 'sm' && padding) || (size === 'base' && !padding)) && direction === 'top',
|
|
338
|
-
'mb-12': size === 'base' && padding && direction === 'top',
|
|
339
|
-
'max-h-xs': maxHeight === 'xs',
|
|
340
|
-
'max-h-sm': maxHeight === 'sm',
|
|
341
|
-
'max-h-md': maxHeight === 'md',
|
|
342
|
-
'max-h-lg': maxHeight === 'lg',
|
|
447
|
+
'text-xs': size === 'sm',
|
|
448
|
+
'text-sm': size === 'base',
|
|
449
|
+
'text-base': size === 'lg',
|
|
450
|
+
'field-uppercase': uppercase,
|
|
451
|
+
'md:min-w-xl p-1 pb-0': isMultiColumn,
|
|
343
452
|
}"
|
|
453
|
+
role="listbox"
|
|
454
|
+
:aria-labelledby="label ? labelId : undefined"
|
|
455
|
+
:style="{ maxHeight: 'var(--field-select-menu-max-height)' }"
|
|
344
456
|
>
|
|
345
457
|
<div
|
|
346
458
|
v-if="searchable"
|
|
@@ -348,8 +460,8 @@ function selectOption(event: MouseEvent, value: string) {
|
|
|
348
460
|
@click.stop
|
|
349
461
|
>
|
|
350
462
|
<FieldInput
|
|
351
|
-
ref="
|
|
352
|
-
v-model="
|
|
463
|
+
ref="searchInputRef"
|
|
464
|
+
v-model="searchQuery"
|
|
353
465
|
:autocomplete="false"
|
|
354
466
|
background="white"
|
|
355
467
|
border="none"
|
|
@@ -365,7 +477,7 @@ function selectOption(event: MouseEvent, value: string) {
|
|
|
365
477
|
</div>
|
|
366
478
|
|
|
367
479
|
<div
|
|
368
|
-
v-if="searchable &&
|
|
480
|
+
v-if="searchable && normalizedSearchQuery && !firstFilteredOption"
|
|
369
481
|
class="flex justify-center base-border-b p-3"
|
|
370
482
|
>
|
|
371
483
|
<BaseIcon
|
|
@@ -379,7 +491,7 @@ function selectOption(event: MouseEvent, value: string) {
|
|
|
379
491
|
|
|
380
492
|
<div
|
|
381
493
|
v-for="(column, columnIndex) in filteredColumns"
|
|
382
|
-
:key="columnIndex"
|
|
494
|
+
:key="column.title ?? columnIndex"
|
|
383
495
|
>
|
|
384
496
|
<BaseIcon
|
|
385
497
|
v-if="column.title"
|
|
@@ -392,20 +504,20 @@ function selectOption(event: MouseEvent, value: string) {
|
|
|
392
504
|
:key="option.value"
|
|
393
505
|
class="flex cursor-pointer items-center outline-none"
|
|
394
506
|
:class="{
|
|
395
|
-
'base-border-b base-bg-hover last:border-b-0':
|
|
396
|
-
'h-sm px-sm': size === 'sm' &&
|
|
397
|
-
'h-base px-base': size === 'base' &&
|
|
398
|
-
'h-lg px-lg': size === 'lg' &&
|
|
399
|
-
'base-bg-hover px-2 py-1 last:mb-2':
|
|
400
|
-
'
|
|
401
|
-
'text-gray-900 dark:text-gray-100': option
|
|
402
|
-
'bg-white dark:bg-gray-900': selectedOption && option.value !== selectedOption.value,
|
|
507
|
+
'base-border-b base-bg-hover last:border-b-0': !isMultiColumn,
|
|
508
|
+
'h-sm px-sm': size === 'sm' && !isMultiColumn,
|
|
509
|
+
'h-base px-base': size === 'base' && !isMultiColumn,
|
|
510
|
+
'h-lg px-lg': size === 'lg' && !isMultiColumn,
|
|
511
|
+
'base-bg-hover px-2 py-1 last:mb-2': isMultiColumn,
|
|
512
|
+
'underline': isSelected(option),
|
|
513
|
+
'text-gray-900 dark:text-gray-100': isSelected(option),
|
|
403
514
|
}"
|
|
404
|
-
role="
|
|
515
|
+
role="option"
|
|
516
|
+
:aria-selected="isSelected(option)"
|
|
405
517
|
tabindex="0"
|
|
406
518
|
@click="onOptionClick($event, option)"
|
|
407
|
-
@keydown.enter.prevent="
|
|
408
|
-
@keydown.space.prevent="
|
|
519
|
+
@keydown.enter.prevent="onOptionKeydown($event, option)"
|
|
520
|
+
@keydown.space.prevent="onOptionKeydown($event, option)"
|
|
409
521
|
>
|
|
410
522
|
<template v-if="option.emoji">
|
|
411
523
|
<BaseEmoji
|
|
@@ -433,7 +545,7 @@ function selectOption(event: MouseEvent, value: string) {
|
|
|
433
545
|
</div>
|
|
434
546
|
</Motion>
|
|
435
547
|
</div>
|
|
436
|
-
</
|
|
548
|
+
</Teleport>
|
|
437
549
|
|
|
438
550
|
<FieldMessage
|
|
439
551
|
:description="description"
|
package/app/types/fields.d.ts
CHANGED
|
@@ -164,9 +164,8 @@ export interface FieldSelect {
|
|
|
164
164
|
description?: BaseTextText
|
|
165
165
|
direction?: FieldSelectDirection
|
|
166
166
|
disabled?: boolean
|
|
167
|
-
hideCaret?: boolean
|
|
168
167
|
hideError?: boolean
|
|
169
|
-
|
|
168
|
+
iconOnly?: boolean
|
|
170
169
|
label?: BaseTextText
|
|
171
170
|
labelIcon?: string
|
|
172
171
|
maxHeight?: FieldSelectMaxHeight
|
|
@@ -180,6 +179,7 @@ export interface FieldSelect {
|
|
|
180
179
|
searchPlaceholder?: string
|
|
181
180
|
size?: FieldSize
|
|
182
181
|
square?: boolean
|
|
182
|
+
uppercase?: boolean
|
|
183
183
|
validation?: VuelidateValidation
|
|
184
184
|
}
|
|
185
185
|
|
|
@@ -190,7 +190,7 @@ export interface FieldSelectColumn {
|
|
|
190
190
|
title?: string
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
export type FieldSelectDirection = 'bottom' | 'top'
|
|
193
|
+
export type FieldSelectDirection = 'auto' | 'bottom' | 'top'
|
|
194
194
|
|
|
195
195
|
export type FieldSelectMaxHeight = 'lg' | 'md' | 'sm' | 'xs'
|
|
196
196
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saasmakers/ui",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Reusable Nuxt UI components for SaaS Makers projects",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"@capacitor/core": "8.4.0",
|
|
29
29
|
"@capacitor/preferences": "8.0.1",
|
|
30
30
|
"@capgo/capacitor-social-login": "8.3.22",
|
|
31
|
+
"@floating-ui/vue": "2.0.1",
|
|
31
32
|
"@nuxt/icon": "2.2.3",
|
|
32
33
|
"@nuxt/scripts": "0.13.2",
|
|
33
34
|
"@nuxtjs/color-mode": "3.5.2",
|