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