@tachui/forms 0.7.1-alpha → 0.8.1-alpha
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/README.md +87 -272
- package/dist/DatePicker-D5nRFTUm.js +475 -0
- package/dist/DatePicker-D5nRFTUm.js.map +1 -0
- package/dist/Select-yZyKooXk.js +945 -0
- package/dist/Select-yZyKooXk.js.map +1 -0
- package/dist/Slider-0-oal5YR.js +644 -0
- package/dist/Slider-0-oal5YR.js.map +1 -0
- package/dist/TextField-hX15dY3U.js +509 -0
- package/dist/TextField-hX15dY3U.js.map +1 -0
- package/dist/components/advanced/Slider.d.ts +190 -0
- package/dist/components/advanced/Slider.d.ts.map +1 -0
- package/dist/components/advanced/Stepper.d.ts +161 -0
- package/dist/components/advanced/Stepper.d.ts.map +1 -0
- package/dist/components/advanced/index.d.ts +15 -0
- package/dist/components/advanced/index.d.ts.map +1 -0
- package/dist/components/advanced/index.js +6 -0
- package/dist/components/advanced/index.js.map +1 -0
- package/dist/components/date-picker/DatePicker.d.ts +126 -0
- package/dist/components/date-picker/DatePicker.d.ts.map +1 -0
- package/dist/components/date-picker/index.d.ts +14 -0
- package/dist/components/date-picker/index.d.ts.map +1 -0
- package/dist/components/date-picker/index.js +5 -0
- package/dist/components/date-picker/index.js.map +1 -0
- package/dist/components/form-container/index.d.ts +58 -0
- package/dist/components/form-container/index.d.ts.map +1 -0
- package/dist/components/selection/Checkbox.d.ts.map +1 -0
- package/dist/components/selection/Radio.d.ts.map +1 -0
- package/dist/components/selection/Select.d.ts.map +1 -0
- package/dist/components/selection/index.d.ts +68 -0
- package/dist/components/selection/index.d.ts.map +1 -0
- package/dist/components/selection/index.js +12 -0
- package/dist/components/selection/index.js.map +1 -0
- package/dist/components/text-input/TextField.d.ts.map +1 -0
- package/dist/components/text-input/index.d.ts +8 -0
- package/dist/components/text-input/index.d.ts.map +1 -0
- package/dist/components/text-input/index.js +18 -0
- package/dist/components/text-input/index.js.map +1 -0
- package/dist/{state/index.js → index-D3WfkqVv.js} +15 -8
- package/dist/index-D3WfkqVv.js.map +1 -0
- package/dist/index.d.ts +10 -15
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +198 -376
- package/dist/index.js.map +1 -0
- package/dist/state/index.d.ts.map +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/index.d.ts +19 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/validation/component-validation.d.ts +11 -2
- package/dist/validation/component-validation.d.ts.map +1 -1
- package/dist/validation/index.d.ts.map +1 -1
- package/dist/validation/index.js +282 -191
- package/dist/validation/index.js.map +1 -0
- package/package.json +53 -39
- package/src/components/advanced/Slider.ts +722 -0
- package/src/components/advanced/Stepper.ts +715 -0
- package/src/components/advanced/index.ts +20 -0
- package/src/components/date-picker/DatePicker.ts +925 -0
- package/src/components/date-picker/index.ts +20 -0
- package/src/components/form-container/index.ts +266 -0
- package/src/components/selection/Checkbox.ts +478 -0
- package/src/components/selection/Radio.ts +470 -0
- package/src/components/selection/Select.ts +620 -0
- package/src/components/selection/index.ts +81 -0
- package/src/components/text-input/TextField.ts +728 -0
- package/src/components/text-input/index.ts +35 -0
- package/src/index.ts +48 -0
- package/src/state/index.ts +544 -0
- package/src/types/index.ts +579 -0
- package/src/utils/formatters.ts +184 -0
- package/src/utils/index.ts +57 -0
- package/src/validation/component-validation.ts +429 -0
- package/src/validation/index.ts +641 -0
- package/dist/TextField-CGBM3x7K.js +0 -1799
- package/dist/components/Form.d.ts +0 -76
- package/dist/components/Form.d.ts.map +0 -1
- package/dist/components/index.d.ts +0 -9
- package/dist/components/index.d.ts.map +0 -1
- package/dist/components/index.js +0 -28
- package/dist/components/input/Checkbox.d.ts.map +0 -1
- package/dist/components/input/Radio.d.ts.map +0 -1
- package/dist/components/input/Select.d.ts.map +0 -1
- package/dist/components/input/TextField.d.ts.map +0 -1
- package/dist/components/input/index.d.ts +0 -11
- package/dist/components/input/index.d.ts.map +0 -1
- package/dist/utils/validators.d.ts +0 -101
- package/dist/utils/validators.d.ts.map +0 -1
- /package/dist/components/{input → selection}/Checkbox.d.ts +0 -0
- /package/dist/components/{input → selection}/Radio.d.ts +0 -0
- /package/dist/components/{input → selection}/Select.d.ts +0 -0
- /package/dist/components/{input → text-input}/TextField.d.ts +0 -0
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Select/Picker Component
|
|
3
|
+
*
|
|
4
|
+
* SwiftUI-inspired select component with search, multiple selection,
|
|
5
|
+
* async options loading, and comprehensive accessibility support.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Component, ComponentInstance } from '@tachui/core'
|
|
9
|
+
import {
|
|
10
|
+
createEffect,
|
|
11
|
+
createSignal,
|
|
12
|
+
h,
|
|
13
|
+
text,
|
|
14
|
+
useLifecycle,
|
|
15
|
+
setupOutsideClickDetection,
|
|
16
|
+
} from '@tachui/core'
|
|
17
|
+
import { createField } from '../../state'
|
|
18
|
+
import type { SelectOption, SelectProps } from '../../types'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Select component implementation
|
|
22
|
+
*/
|
|
23
|
+
export const Select: Component<SelectProps> = props => {
|
|
24
|
+
const {
|
|
25
|
+
name,
|
|
26
|
+
label,
|
|
27
|
+
options,
|
|
28
|
+
multiple = false,
|
|
29
|
+
searchable = false,
|
|
30
|
+
clearable = false,
|
|
31
|
+
placeholder = multiple ? 'Select options...' : 'Select an option...',
|
|
32
|
+
noOptionsMessage = 'No options available',
|
|
33
|
+
loadingMessage = 'Loading...',
|
|
34
|
+
maxMenuHeight = 200,
|
|
35
|
+
disabled = false,
|
|
36
|
+
required = false,
|
|
37
|
+
value: controlledValue,
|
|
38
|
+
defaultValue,
|
|
39
|
+
validation,
|
|
40
|
+
onChange,
|
|
41
|
+
onBlur,
|
|
42
|
+
onFocus,
|
|
43
|
+
error: externalError,
|
|
44
|
+
helperText,
|
|
45
|
+
...restProps
|
|
46
|
+
} = props
|
|
47
|
+
|
|
48
|
+
// Get form context if available
|
|
49
|
+
const formContext = (props as any)._formContext
|
|
50
|
+
|
|
51
|
+
// Create field state
|
|
52
|
+
const field = createField(name, controlledValue ?? defaultValue, validation)
|
|
53
|
+
|
|
54
|
+
// Register field with form if form context exists
|
|
55
|
+
if (formContext) {
|
|
56
|
+
formContext.register(name, validation)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const [isOpen, setIsOpen] = createSignal(false)
|
|
60
|
+
const [focused, setFocused] = createSignal(false)
|
|
61
|
+
const [searchQuery, setSearchQuery] = createSignal('')
|
|
62
|
+
const [highlightedIndex, setHighlightedIndex] = createSignal(-1)
|
|
63
|
+
const [loading] = createSignal(false)
|
|
64
|
+
|
|
65
|
+
// Sync with controlled value
|
|
66
|
+
if (controlledValue !== undefined) {
|
|
67
|
+
createEffect(() => {
|
|
68
|
+
if (field.value() !== controlledValue) {
|
|
69
|
+
field.setValue(controlledValue)
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Filter options based on search query
|
|
75
|
+
const filteredOptions = () => {
|
|
76
|
+
if (!searchable || !searchQuery()) {
|
|
77
|
+
return options
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const query = searchQuery().toLowerCase()
|
|
81
|
+
return options.filter(
|
|
82
|
+
option =>
|
|
83
|
+
option.label.toLowerCase().includes(query) ||
|
|
84
|
+
String(option.value).toLowerCase().includes(query)
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Get display value for selected option(s)
|
|
89
|
+
const getDisplayValue = () => {
|
|
90
|
+
const value = field.value()
|
|
91
|
+
|
|
92
|
+
if (multiple) {
|
|
93
|
+
if (!value || !Array.isArray(value) || value.length === 0) {
|
|
94
|
+
return placeholder
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const selectedOptions = options.filter(opt => value.includes(opt.value))
|
|
98
|
+
return selectedOptions.map(opt => opt.label).join(', ')
|
|
99
|
+
} else {
|
|
100
|
+
if (value === null || value === undefined || value === '') {
|
|
101
|
+
return placeholder
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const selectedOption = options.find(opt => opt.value === value)
|
|
105
|
+
return selectedOption ? selectedOption.label : String(value)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Handle option selection
|
|
110
|
+
const handleOptionSelect = (option: SelectOption) => {
|
|
111
|
+
if (option.disabled) return
|
|
112
|
+
|
|
113
|
+
let newValue: any
|
|
114
|
+
|
|
115
|
+
if (multiple) {
|
|
116
|
+
const currentValue = field.value() || []
|
|
117
|
+
if (currentValue.includes(option.value)) {
|
|
118
|
+
newValue = currentValue.filter((v: any) => v !== option.value)
|
|
119
|
+
} else {
|
|
120
|
+
newValue = [...currentValue, option.value]
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
newValue = option.value
|
|
124
|
+
setIsOpen(false)
|
|
125
|
+
setSearchQuery('')
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
field.setValue(newValue)
|
|
129
|
+
|
|
130
|
+
if (formContext) {
|
|
131
|
+
formContext.setValue(name, newValue)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (onChange) {
|
|
135
|
+
onChange(name, newValue, field as any)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Handle dropdown toggle
|
|
140
|
+
const toggleDropdown = () => {
|
|
141
|
+
if (disabled) return
|
|
142
|
+
|
|
143
|
+
const newOpen = !isOpen()
|
|
144
|
+
setIsOpen(newOpen)
|
|
145
|
+
|
|
146
|
+
if (newOpen) {
|
|
147
|
+
setHighlightedIndex(-1)
|
|
148
|
+
if (searchable) {
|
|
149
|
+
setSearchQuery('')
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Handle search input
|
|
155
|
+
const handleSearch = (event: Event) => {
|
|
156
|
+
const target = event.target as HTMLInputElement
|
|
157
|
+
setSearchQuery(target.value)
|
|
158
|
+
setHighlightedIndex(-1)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Handle keyboard navigation
|
|
162
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
163
|
+
const filtered = filteredOptions()
|
|
164
|
+
|
|
165
|
+
switch (event.key) {
|
|
166
|
+
case 'Enter':
|
|
167
|
+
event.preventDefault()
|
|
168
|
+
if (!isOpen()) {
|
|
169
|
+
toggleDropdown()
|
|
170
|
+
} else if (highlightedIndex() >= 0 && filtered[highlightedIndex()]) {
|
|
171
|
+
handleOptionSelect(filtered[highlightedIndex()])
|
|
172
|
+
}
|
|
173
|
+
break
|
|
174
|
+
|
|
175
|
+
case ' ':
|
|
176
|
+
if (!searchable || !isOpen()) {
|
|
177
|
+
event.preventDefault()
|
|
178
|
+
toggleDropdown()
|
|
179
|
+
}
|
|
180
|
+
break
|
|
181
|
+
|
|
182
|
+
case 'Escape':
|
|
183
|
+
event.preventDefault()
|
|
184
|
+
setIsOpen(false)
|
|
185
|
+
setSearchQuery('')
|
|
186
|
+
break
|
|
187
|
+
|
|
188
|
+
case 'ArrowDown':
|
|
189
|
+
event.preventDefault()
|
|
190
|
+
if (!isOpen()) {
|
|
191
|
+
toggleDropdown()
|
|
192
|
+
} else {
|
|
193
|
+
const nextIndex = Math.min(
|
|
194
|
+
highlightedIndex() + 1,
|
|
195
|
+
filtered.length - 1
|
|
196
|
+
)
|
|
197
|
+
setHighlightedIndex(nextIndex)
|
|
198
|
+
}
|
|
199
|
+
break
|
|
200
|
+
|
|
201
|
+
case 'ArrowUp':
|
|
202
|
+
event.preventDefault()
|
|
203
|
+
if (isOpen()) {
|
|
204
|
+
const prevIndex = Math.max(highlightedIndex() - 1, -1)
|
|
205
|
+
setHighlightedIndex(prevIndex)
|
|
206
|
+
}
|
|
207
|
+
break
|
|
208
|
+
|
|
209
|
+
case 'Home':
|
|
210
|
+
if (isOpen()) {
|
|
211
|
+
event.preventDefault()
|
|
212
|
+
setHighlightedIndex(0)
|
|
213
|
+
}
|
|
214
|
+
break
|
|
215
|
+
|
|
216
|
+
case 'End':
|
|
217
|
+
if (isOpen()) {
|
|
218
|
+
event.preventDefault()
|
|
219
|
+
setHighlightedIndex(filtered.length - 1)
|
|
220
|
+
}
|
|
221
|
+
break
|
|
222
|
+
|
|
223
|
+
case 'Backspace':
|
|
224
|
+
if (clearable && !searchable && field.value() !== null) {
|
|
225
|
+
event.preventDefault()
|
|
226
|
+
handleClear()
|
|
227
|
+
}
|
|
228
|
+
break
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Handle focus
|
|
233
|
+
const handleFocus = () => {
|
|
234
|
+
setFocused(true)
|
|
235
|
+
field.onFocus()
|
|
236
|
+
|
|
237
|
+
if (onFocus) {
|
|
238
|
+
onFocus(name, field.value())
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Handle blur
|
|
243
|
+
const handleBlur = (event: FocusEvent) => {
|
|
244
|
+
// ENHANCED: Use requestAnimationFrame instead of setTimeout for better performance
|
|
245
|
+
requestAnimationFrame(() => {
|
|
246
|
+
const relatedTarget = event.relatedTarget as Element
|
|
247
|
+
const container = (event.target as Element).closest(
|
|
248
|
+
'[data-tachui-select-container]'
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
if (!container?.contains(relatedTarget)) {
|
|
252
|
+
setFocused(false)
|
|
253
|
+
setIsOpen(false)
|
|
254
|
+
field.onBlur()
|
|
255
|
+
|
|
256
|
+
if (onBlur) {
|
|
257
|
+
onBlur(name, field.value())
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Handle clear
|
|
264
|
+
const handleClear = () => {
|
|
265
|
+
const newValue = multiple ? [] : null
|
|
266
|
+
field.setValue(newValue)
|
|
267
|
+
|
|
268
|
+
if (formContext) {
|
|
269
|
+
formContext.setValue(name, newValue)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (onChange) {
|
|
273
|
+
onChange(name, newValue, field as any)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Check if option is selected
|
|
278
|
+
const isOptionSelected = (option: SelectOption) => {
|
|
279
|
+
const value = field.value()
|
|
280
|
+
|
|
281
|
+
if (multiple) {
|
|
282
|
+
return Array.isArray(value) && value.includes(option.value)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return value === option.value
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Determine error message
|
|
289
|
+
const errorMessage =
|
|
290
|
+
externalError || field.error() || formContext?.getError(name)
|
|
291
|
+
|
|
292
|
+
const componentInstance: ComponentInstance = {
|
|
293
|
+
type: 'component',
|
|
294
|
+
id: restProps.id || `select-${name}`,
|
|
295
|
+
cleanup: [],
|
|
296
|
+
render: () => ({
|
|
297
|
+
type: 'element',
|
|
298
|
+
tag: 'div',
|
|
299
|
+
props: {
|
|
300
|
+
...restProps,
|
|
301
|
+
class: `tachui-select ${restProps.class || ''}`.trim(),
|
|
302
|
+
'data-tachui-select-container': true,
|
|
303
|
+
'data-field-state': errorMessage
|
|
304
|
+
? 'error'
|
|
305
|
+
: field.validating()
|
|
306
|
+
? 'validating'
|
|
307
|
+
: 'valid',
|
|
308
|
+
'data-open': isOpen(),
|
|
309
|
+
'data-disabled': disabled,
|
|
310
|
+
'data-multiple': multiple,
|
|
311
|
+
'data-searchable': searchable,
|
|
312
|
+
},
|
|
313
|
+
children: [
|
|
314
|
+
// Label
|
|
315
|
+
...(label
|
|
316
|
+
? [
|
|
317
|
+
h(
|
|
318
|
+
'label',
|
|
319
|
+
{
|
|
320
|
+
for: restProps.id || name,
|
|
321
|
+
'data-tachui-label': true,
|
|
322
|
+
'data-required': required,
|
|
323
|
+
},
|
|
324
|
+
text(label),
|
|
325
|
+
...(required
|
|
326
|
+
? [
|
|
327
|
+
h(
|
|
328
|
+
'span',
|
|
329
|
+
{
|
|
330
|
+
'aria-label': 'required',
|
|
331
|
+
'data-required-indicator': true,
|
|
332
|
+
},
|
|
333
|
+
text(' *')
|
|
334
|
+
),
|
|
335
|
+
]
|
|
336
|
+
: [])
|
|
337
|
+
),
|
|
338
|
+
]
|
|
339
|
+
: []),
|
|
340
|
+
|
|
341
|
+
// Select trigger
|
|
342
|
+
{
|
|
343
|
+
type: 'element' as const,
|
|
344
|
+
tag: 'div',
|
|
345
|
+
props: {
|
|
346
|
+
id: restProps.id || name,
|
|
347
|
+
tabindex: disabled ? -1 : 0,
|
|
348
|
+
role: 'combobox',
|
|
349
|
+
'aria-expanded': isOpen(),
|
|
350
|
+
'aria-haspopup': 'listbox',
|
|
351
|
+
'aria-invalid': !!errorMessage,
|
|
352
|
+
'aria-describedby':
|
|
353
|
+
[
|
|
354
|
+
errorMessage ? `${name}-error` : null,
|
|
355
|
+
helperText ? `${name}-helper` : null,
|
|
356
|
+
]
|
|
357
|
+
.filter(Boolean)
|
|
358
|
+
.join(' ') || undefined,
|
|
359
|
+
onclick: toggleDropdown,
|
|
360
|
+
onkeydown: handleKeyDown,
|
|
361
|
+
onfocus: handleFocus,
|
|
362
|
+
onblur: handleBlur,
|
|
363
|
+
'data-tachui-select-trigger': true,
|
|
364
|
+
'data-focused': focused(),
|
|
365
|
+
'data-disabled': disabled,
|
|
366
|
+
'data-error': !!errorMessage,
|
|
367
|
+
},
|
|
368
|
+
children: [
|
|
369
|
+
// Display value
|
|
370
|
+
{
|
|
371
|
+
type: 'element' as const,
|
|
372
|
+
tag: 'div',
|
|
373
|
+
props: {
|
|
374
|
+
'data-tachui-select-value': true,
|
|
375
|
+
'data-placeholder':
|
|
376
|
+
!field.value() ||
|
|
377
|
+
(multiple && (!field.value() || field.value().length === 0)),
|
|
378
|
+
},
|
|
379
|
+
children: [text(getDisplayValue())],
|
|
380
|
+
},
|
|
381
|
+
|
|
382
|
+
// Actions (clear, dropdown arrow)
|
|
383
|
+
{
|
|
384
|
+
type: 'element' as const,
|
|
385
|
+
tag: 'div',
|
|
386
|
+
props: {
|
|
387
|
+
'data-tachui-select-actions': true,
|
|
388
|
+
},
|
|
389
|
+
children: [
|
|
390
|
+
// Clear button
|
|
391
|
+
...(clearable &&
|
|
392
|
+
field.value() &&
|
|
393
|
+
(!multiple ||
|
|
394
|
+
(Array.isArray(field.value()) && field.value().length > 0))
|
|
395
|
+
? [
|
|
396
|
+
{
|
|
397
|
+
type: 'element' as const,
|
|
398
|
+
tag: 'button',
|
|
399
|
+
props: {
|
|
400
|
+
type: 'button',
|
|
401
|
+
onclick: (e: Event) => {
|
|
402
|
+
e.stopPropagation()
|
|
403
|
+
handleClear()
|
|
404
|
+
},
|
|
405
|
+
'aria-label': 'Clear selection',
|
|
406
|
+
'data-tachui-select-clear': true,
|
|
407
|
+
},
|
|
408
|
+
children: [text('×')],
|
|
409
|
+
},
|
|
410
|
+
]
|
|
411
|
+
: []),
|
|
412
|
+
|
|
413
|
+
// Dropdown arrow
|
|
414
|
+
{
|
|
415
|
+
type: 'element' as const,
|
|
416
|
+
tag: 'div',
|
|
417
|
+
props: {
|
|
418
|
+
'data-tachui-select-arrow': true,
|
|
419
|
+
'data-open': isOpen(),
|
|
420
|
+
},
|
|
421
|
+
children: [text('▼')],
|
|
422
|
+
},
|
|
423
|
+
],
|
|
424
|
+
},
|
|
425
|
+
],
|
|
426
|
+
},
|
|
427
|
+
|
|
428
|
+
// Dropdown menu
|
|
429
|
+
...(isOpen()
|
|
430
|
+
? [
|
|
431
|
+
{
|
|
432
|
+
type: 'element' as const,
|
|
433
|
+
tag: 'div',
|
|
434
|
+
props: {
|
|
435
|
+
'data-tachui-select-dropdown': true,
|
|
436
|
+
style: {
|
|
437
|
+
maxHeight: `${maxMenuHeight}px`,
|
|
438
|
+
},
|
|
439
|
+
},
|
|
440
|
+
children: [
|
|
441
|
+
// Search input
|
|
442
|
+
...(searchable
|
|
443
|
+
? [
|
|
444
|
+
{
|
|
445
|
+
type: 'element' as const,
|
|
446
|
+
tag: 'div',
|
|
447
|
+
props: {
|
|
448
|
+
'data-tachui-select-search': true,
|
|
449
|
+
},
|
|
450
|
+
children: [
|
|
451
|
+
{
|
|
452
|
+
type: 'element' as const,
|
|
453
|
+
tag: 'input',
|
|
454
|
+
props: {
|
|
455
|
+
type: 'text',
|
|
456
|
+
placeholder: 'Search...',
|
|
457
|
+
value: searchQuery(),
|
|
458
|
+
oninput: handleSearch,
|
|
459
|
+
'data-tachui-select-search-input': true,
|
|
460
|
+
},
|
|
461
|
+
},
|
|
462
|
+
],
|
|
463
|
+
},
|
|
464
|
+
]
|
|
465
|
+
: []),
|
|
466
|
+
|
|
467
|
+
// Options list
|
|
468
|
+
{
|
|
469
|
+
type: 'element' as const,
|
|
470
|
+
tag: 'div',
|
|
471
|
+
props: {
|
|
472
|
+
role: 'listbox',
|
|
473
|
+
'aria-multiselectable': multiple,
|
|
474
|
+
'data-tachui-select-options': true,
|
|
475
|
+
},
|
|
476
|
+
children:
|
|
477
|
+
filteredOptions().length > 0
|
|
478
|
+
? filteredOptions().map((option, index) => ({
|
|
479
|
+
type: 'element' as const,
|
|
480
|
+
tag: 'div',
|
|
481
|
+
props: {
|
|
482
|
+
role: 'option',
|
|
483
|
+
'aria-selected': isOptionSelected(option),
|
|
484
|
+
'aria-disabled': option.disabled,
|
|
485
|
+
onclick: () => handleOptionSelect(option),
|
|
486
|
+
'data-tachui-select-option': true,
|
|
487
|
+
'data-selected': isOptionSelected(option),
|
|
488
|
+
'data-highlighted': highlightedIndex() === index,
|
|
489
|
+
'data-disabled': option.disabled,
|
|
490
|
+
'data-group': option.group,
|
|
491
|
+
},
|
|
492
|
+
children: [
|
|
493
|
+
// Selection indicator for multiple
|
|
494
|
+
...(multiple
|
|
495
|
+
? [
|
|
496
|
+
{
|
|
497
|
+
type: 'element' as const,
|
|
498
|
+
tag: 'div',
|
|
499
|
+
props: {
|
|
500
|
+
'data-tachui-select-checkbox': true,
|
|
501
|
+
'data-checked':
|
|
502
|
+
isOptionSelected(option),
|
|
503
|
+
},
|
|
504
|
+
children: [
|
|
505
|
+
text(
|
|
506
|
+
isOptionSelected(option) ? '✓' : ''
|
|
507
|
+
),
|
|
508
|
+
],
|
|
509
|
+
},
|
|
510
|
+
]
|
|
511
|
+
: []),
|
|
512
|
+
|
|
513
|
+
// Option label
|
|
514
|
+
text(option.label),
|
|
515
|
+
],
|
|
516
|
+
}))
|
|
517
|
+
: [
|
|
518
|
+
{
|
|
519
|
+
type: 'element' as const,
|
|
520
|
+
tag: 'div',
|
|
521
|
+
props: {
|
|
522
|
+
'data-tachui-select-no-options': true,
|
|
523
|
+
},
|
|
524
|
+
children: [
|
|
525
|
+
text(
|
|
526
|
+
loading() ? loadingMessage : noOptionsMessage
|
|
527
|
+
),
|
|
528
|
+
],
|
|
529
|
+
},
|
|
530
|
+
],
|
|
531
|
+
},
|
|
532
|
+
],
|
|
533
|
+
},
|
|
534
|
+
]
|
|
535
|
+
: []),
|
|
536
|
+
|
|
537
|
+
// Error message
|
|
538
|
+
...(errorMessage
|
|
539
|
+
? [
|
|
540
|
+
{
|
|
541
|
+
type: 'element' as const,
|
|
542
|
+
tag: 'div',
|
|
543
|
+
props: {
|
|
544
|
+
id: `${name}-error`,
|
|
545
|
+
role: 'alert',
|
|
546
|
+
'aria-live': 'polite',
|
|
547
|
+
'data-tachui-error': true,
|
|
548
|
+
},
|
|
549
|
+
children: [text(errorMessage)],
|
|
550
|
+
},
|
|
551
|
+
]
|
|
552
|
+
: []),
|
|
553
|
+
|
|
554
|
+
// Helper text
|
|
555
|
+
...(helperText && !errorMessage
|
|
556
|
+
? [
|
|
557
|
+
{
|
|
558
|
+
type: 'element' as const,
|
|
559
|
+
tag: 'div',
|
|
560
|
+
props: {
|
|
561
|
+
id: `${name}-helper`,
|
|
562
|
+
'data-tachui-helper': true,
|
|
563
|
+
},
|
|
564
|
+
children: [text(helperText)],
|
|
565
|
+
},
|
|
566
|
+
]
|
|
567
|
+
: []),
|
|
568
|
+
],
|
|
569
|
+
}),
|
|
570
|
+
props: props,
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Add form cleanup
|
|
574
|
+
if (!componentInstance.cleanup) componentInstance.cleanup = []
|
|
575
|
+
componentInstance.cleanup.push(() => {
|
|
576
|
+
if (formContext) {
|
|
577
|
+
formContext.unregister(name)
|
|
578
|
+
}
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
// ENHANCED: Set up lifecycle hooks for improved click outside detection
|
|
582
|
+
useLifecycle(componentInstance, {
|
|
583
|
+
onDOMReady: (_elements, primaryElement) => {
|
|
584
|
+
if (primaryElement) {
|
|
585
|
+
// Set up enhanced outside click detection instead of relying on setTimeout blur
|
|
586
|
+
setupOutsideClickDetection(
|
|
587
|
+
componentInstance,
|
|
588
|
+
() => {
|
|
589
|
+
if (isOpen()) {
|
|
590
|
+
setIsOpen(false)
|
|
591
|
+
setFocused(false)
|
|
592
|
+
field.onBlur()
|
|
593
|
+
|
|
594
|
+
if (onBlur) {
|
|
595
|
+
onBlur(name, field.value())
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
},
|
|
599
|
+
'[data-tachui-select-container]'
|
|
600
|
+
)
|
|
601
|
+
}
|
|
602
|
+
},
|
|
603
|
+
})
|
|
604
|
+
|
|
605
|
+
return componentInstance
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* MultiSelect variant for clearer multiple selection intent
|
|
610
|
+
*/
|
|
611
|
+
export const MultiSelect: Component<SelectProps> = props => {
|
|
612
|
+
return Select({ ...props, multiple: true })
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Combobox variant with always-searchable behavior
|
|
617
|
+
*/
|
|
618
|
+
export const Combobox: Component<SelectProps> = props => {
|
|
619
|
+
return Select({ ...props, searchable: true })
|
|
620
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Selection Components
|
|
3
|
+
*
|
|
4
|
+
* Checkbox, radio, select, and other selection input components
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Component implementations
|
|
8
|
+
export { Checkbox, CheckboxGroup, Switch } from './Checkbox'
|
|
9
|
+
export { Radio, RadioGroup } from './Radio'
|
|
10
|
+
export { Combobox, MultiSelect, Select } from './Select'
|
|
11
|
+
|
|
12
|
+
// Type aliases for convenience
|
|
13
|
+
export type CheckboxGroupProps = {
|
|
14
|
+
name: string
|
|
15
|
+
label?: string
|
|
16
|
+
options: Array<{
|
|
17
|
+
value: any
|
|
18
|
+
label: string
|
|
19
|
+
disabled?: boolean
|
|
20
|
+
}>
|
|
21
|
+
value?: any[]
|
|
22
|
+
defaultValue?: any[]
|
|
23
|
+
onChange?: (name: string, value: any[], selected: any) => void
|
|
24
|
+
validation?: any
|
|
25
|
+
error?: string
|
|
26
|
+
helperText?: string
|
|
27
|
+
disabled?: boolean
|
|
28
|
+
required?: boolean
|
|
29
|
+
direction?: 'horizontal' | 'vertical'
|
|
30
|
+
id?: string
|
|
31
|
+
[key: string]: any
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type RadioGroupProps = {
|
|
35
|
+
name: string
|
|
36
|
+
label?: string
|
|
37
|
+
options: Array<{
|
|
38
|
+
value: any
|
|
39
|
+
label: string
|
|
40
|
+
disabled?: boolean
|
|
41
|
+
}>
|
|
42
|
+
value?: any
|
|
43
|
+
defaultValue?: any
|
|
44
|
+
onChange?: (name: string, value: any) => void
|
|
45
|
+
validation?: any
|
|
46
|
+
error?: string
|
|
47
|
+
helperText?: string
|
|
48
|
+
disabled?: boolean
|
|
49
|
+
required?: boolean
|
|
50
|
+
direction?: 'horizontal' | 'vertical'
|
|
51
|
+
id?: string
|
|
52
|
+
[key: string]: any
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Additional component-specific props
|
|
56
|
+
export interface SwitchProps {
|
|
57
|
+
name: string
|
|
58
|
+
label?: string
|
|
59
|
+
checked?: boolean
|
|
60
|
+
defaultChecked?: boolean
|
|
61
|
+
indeterminate?: boolean
|
|
62
|
+
disabled?: boolean
|
|
63
|
+
required?: boolean
|
|
64
|
+
validation?: any
|
|
65
|
+
onChange?: any
|
|
66
|
+
onBlur?: any
|
|
67
|
+
onFocus?: any
|
|
68
|
+
error?: string
|
|
69
|
+
helperText?: string
|
|
70
|
+
size?: 'small' | 'medium' | 'large'
|
|
71
|
+
[key: string]: any
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Re-export types for convenience
|
|
75
|
+
export type {
|
|
76
|
+
CheckboxProps,
|
|
77
|
+
RadioProps,
|
|
78
|
+
SelectProps,
|
|
79
|
+
SelectOption,
|
|
80
|
+
} from '../../types'
|
|
81
|
+
export type OptionValue = any
|