ywana-core8 0.1.75 → 0.1.77
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/ACCORDION_EVALUATION.md +583 -0
- package/CHECKBOX_EVALUATION.md +273 -0
- package/CHIP_EVALUATION.md +542 -0
- package/COLOR_EVALUATION.md +524 -0
- package/COMPONENTS_EVALUATION.md +477 -0
- package/FORM_EVALUATION.md +459 -0
- package/HEADER_EVALUATION.md +436 -0
- package/ICON_EVALUATION.md +254 -0
- package/LIST_EVALUATION.md +574 -0
- package/PROGRESS_EVALUATION.md +450 -0
- package/RADIO_EVALUATION.md +439 -0
- package/RADIO_VISUAL_FIX.md +183 -0
- package/SECTION_IMPROVEMENTS.md +153 -0
- package/SWITCH_EVALUATION.md +335 -0
- package/SWITCH_VISUAL_FIX.md +232 -0
- package/TAB_EVALUATION.md +626 -0
- package/TEXTFIELD_EVALUATION.md +747 -0
- package/TOOLTIP_FIX.md +157 -0
- package/TREE_EVALUATION.md +708 -0
- package/dist/index.cjs +10893 -1969
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +7768 -1096
- package/dist/index.css.map +1 -1
- package/dist/index.modern.js +10921 -2005
- package/dist/index.modern.js.map +1 -1
- package/dist/index.umd.js +10893 -1969
- package/dist/index.umd.js.map +1 -1
- package/jest.config.js +24 -0
- package/package.json +10 -1
- package/src/html/accordion.css +208 -4
- package/src/html/accordion.example.js +390 -0
- package/src/html/accordion.js +284 -28
- package/src/html/accordion.unit.test.js +334 -0
- package/src/html/button.css +157 -16
- package/src/html/button.example.js +374 -0
- package/src/html/button.js +240 -60
- package/src/html/button.test.js +422 -0
- package/src/html/checkbox.css +74 -2
- package/src/html/checkbox.example.js +316 -0
- package/src/html/checkbox.js +113 -26
- package/src/html/checkbox.test.js +285 -0
- package/src/html/chip.css +230 -19
- package/src/html/chip.example.js +355 -0
- package/src/html/chip.js +321 -25
- package/src/html/chip.test.js +425 -0
- package/src/html/color.css +435 -6
- package/src/html/color.example.js +527 -0
- package/src/html/color.js +458 -9
- package/src/html/color.test.js +362 -4
- package/src/html/components.example.js +492 -0
- package/src/html/components_enhanced.test.js +581 -0
- package/src/html/form.css +70 -3
- package/src/html/form.example.js +385 -0
- package/src/html/form.js +232 -34
- package/src/html/form.test.js +369 -0
- package/src/html/header2.css +264 -0
- package/src/html/header2.example.js +411 -0
- package/src/html/header2.js +203 -0
- package/src/html/header2.test.js +377 -0
- package/src/html/icon.css +20 -2
- package/src/html/icon.example.js +268 -0
- package/src/html/icon.js +86 -16
- package/src/html/icon.test.js +231 -0
- package/src/html/index.js +4 -1
- package/src/html/list.css +393 -1
- package/src/html/list.example.js +404 -0
- package/src/html/list.js +583 -40
- package/src/html/list.test.js +383 -0
- package/src/html/progress.css +707 -17
- package/src/html/progress.example.js +424 -0
- package/src/html/progress.js +906 -9
- package/src/html/progress.test.js +313 -0
- package/src/html/property.css +399 -0
- package/src/html/property.example.js +553 -0
- package/src/html/property.js +393 -15
- package/src/html/property.test.js +351 -2
- package/src/html/radio-visual-test.js +289 -0
- package/src/html/radio.css +137 -11
- package/src/html/radio.example.js +389 -0
- package/src/html/radio.js +234 -10
- package/src/html/radio.test.js +318 -0
- package/src/html/section.example.js +99 -0
- package/src/html/section.js +40 -3
- package/src/html/section.test.js +131 -0
- package/src/html/selector.css +329 -3
- package/src/html/selector.js +369 -23
- package/src/html/switch-debug.js +197 -0
- package/src/html/switch-test-visual.js +294 -0
- package/src/html/switch.css +200 -0
- package/src/html/switch.example.js +461 -0
- package/src/html/switch.js +283 -23
- package/src/html/switch.test.js +355 -0
- package/src/html/tab.css +289 -0
- package/src/html/tab.example.js +446 -0
- package/src/html/tab.js +387 -22
- package/src/html/tab_enhanced.js +378 -0
- package/src/html/tab_enhanced.test.js +504 -0
- package/src/html/table2.css +576 -0
- package/src/html/table2.example.js +703 -0
- package/src/html/table2.js +1252 -0
- package/src/html/table2.migration.md +328 -0
- package/src/html/table2.test.js +582 -0
- package/src/html/text.css +375 -0
- package/src/html/text.js +311 -20
- package/src/html/textfield2.css +841 -0
- package/src/html/textfield2.example.js +1370 -0
- package/src/html/textfield2.js +1143 -0
- package/src/html/textfield2.test.js +950 -0
- package/src/html/thumbnail.css +289 -2
- package/src/html/thumbnail.js +214 -9
- package/src/html/tokenfield.css +449 -1
- package/src/html/tokenfield.example.js +503 -0
- package/src/html/tokenfield.js +561 -56
- package/src/html/tokenfield.test.js +423 -0
- package/src/html/tooltip-positioning-demo.js +187 -0
- package/src/html/tooltip.css +25 -2
- package/src/html/tree.css +240 -10
- package/src/html/tree.example.js +475 -0
- package/src/html/tree.js +714 -28
- package/src/html/tree_enhanced.test.js +495 -0
- package/table2.test.js +454 -0
- package/src/html/button.tsx +0 -38
package/src/html/tokenfield.js
CHANGED
@@ -1,101 +1,606 @@
|
|
1
|
-
import React, { useState } from 'react'
|
1
|
+
import React, { useState, useCallback, useRef, useEffect } from 'react'
|
2
|
+
import PropTypes from 'prop-types'
|
2
3
|
import { Icon } from './icon';
|
3
4
|
import { Text } from './text';
|
4
5
|
import { DropDown } from './textfield';
|
5
6
|
import './tokenfield.css'
|
6
7
|
|
7
8
|
/**
|
8
|
-
* Token Field
|
9
|
+
* Enhanced Token Field component with improved functionality while maintaining 100% compatibility
|
9
10
|
*/
|
10
|
-
export const TokenField = (
|
11
|
+
export const TokenField = (props) => {
|
12
|
+
const {
|
13
|
+
// Original props (100% compatible)
|
14
|
+
id,
|
15
|
+
label,
|
16
|
+
tokens = [],
|
17
|
+
readOnly,
|
18
|
+
options,
|
19
|
+
predictive = true,
|
20
|
+
onChange,
|
21
|
+
// New enhanced props (all optional for compatibility)
|
22
|
+
disabled = false,
|
23
|
+
required = false,
|
24
|
+
placeholder = "Add token...",
|
25
|
+
maxTokens,
|
26
|
+
minTokens = 0,
|
27
|
+
allowDuplicates = true,
|
28
|
+
validateToken,
|
29
|
+
tokenSeparators = [',', ';', '\n'],
|
30
|
+
size = 'medium',
|
31
|
+
variant = 'default',
|
32
|
+
error,
|
33
|
+
helperText,
|
34
|
+
onFocus,
|
35
|
+
onBlur,
|
36
|
+
onTokenAdd,
|
37
|
+
onTokenRemove,
|
38
|
+
onValidationError,
|
39
|
+
clearable = false,
|
40
|
+
onClear,
|
41
|
+
searchable = false,
|
42
|
+
sortable = false,
|
43
|
+
className,
|
44
|
+
style,
|
45
|
+
...restProps
|
46
|
+
} = props
|
11
47
|
|
12
|
-
const [value, setValue] = useState()
|
48
|
+
const [value, setValue] = useState('')
|
49
|
+
const [isFocused, setIsFocused] = useState(false)
|
50
|
+
const [validationErrors, setValidationErrors] = useState({})
|
51
|
+
const inputRef = useRef(null)
|
52
|
+
const containerRef = useRef(null)
|
53
|
+
|
54
|
+
// Validate props
|
55
|
+
useEffect(() => {
|
56
|
+
if (!Array.isArray(tokens)) {
|
57
|
+
console.warn('TokenField: tokens prop must be an array')
|
58
|
+
}
|
59
|
+
if (maxTokens && tokens.length > maxTokens) {
|
60
|
+
console.warn(`TokenField: tokens count (${tokens.length}) exceeds maxTokens (${maxTokens})`)
|
61
|
+
}
|
62
|
+
if (minTokens && tokens.length < minTokens) {
|
63
|
+
console.warn(`TokenField: tokens count (${tokens.length}) is below minTokens (${minTokens})`)
|
64
|
+
}
|
65
|
+
}, [tokens, maxTokens, minTokens])
|
66
|
+
|
67
|
+
// Enhanced remove function (maintaining original behavior)
|
68
|
+
const remove = useCallback((index) => {
|
69
|
+
if (disabled || readOnly) return
|
13
70
|
|
14
|
-
function remove(index) {
|
15
71
|
const next = tokens.slice()
|
16
|
-
next.splice(index, 1)
|
72
|
+
const removedToken = next.splice(index, 1)[0]
|
73
|
+
|
74
|
+
// Check minimum tokens
|
75
|
+
if (minTokens && next.length < minTokens) {
|
76
|
+
console.warn(`TokenField: Cannot remove token. Minimum ${minTokens} tokens required.`)
|
77
|
+
return
|
78
|
+
}
|
79
|
+
|
17
80
|
if (onChange) onChange(id, next)
|
18
|
-
|
81
|
+
if (onTokenRemove) onTokenRemove(removedToken, index)
|
82
|
+
}, [disabled, readOnly, tokens, minTokens, id, onChange, onTokenRemove])
|
19
83
|
|
20
|
-
function
|
21
|
-
|
22
|
-
|
23
|
-
|
84
|
+
// Enhanced change function (maintaining original behavior)
|
85
|
+
const change = useCallback((event) => {
|
86
|
+
if (disabled || readOnly) return
|
87
|
+
|
88
|
+
const newValue = event.target.value
|
89
|
+
setValue(newValue)
|
90
|
+
}, [disabled, readOnly])
|
91
|
+
|
92
|
+
// Validate token
|
93
|
+
const validateTokenValue = useCallback((tokenValue) => {
|
94
|
+
if (!tokenValue || (typeof tokenValue === 'string' && tokenValue.trim() === '')) {
|
95
|
+
return { isValid: false, error: 'Token cannot be empty' }
|
96
|
+
}
|
97
|
+
|
98
|
+
// Check duplicates
|
99
|
+
if (!allowDuplicates && tokens.includes(tokenValue)) {
|
100
|
+
return { isValid: false, error: 'Duplicate token not allowed' }
|
101
|
+
}
|
102
|
+
|
103
|
+
// Check max tokens
|
104
|
+
if (maxTokens && tokens.length >= maxTokens) {
|
105
|
+
return { isValid: false, error: `Maximum ${maxTokens} tokens allowed` }
|
106
|
+
}
|
107
|
+
|
108
|
+
// Custom validation
|
109
|
+
if (validateToken) {
|
110
|
+
const customValidation = validateToken(tokenValue, tokens)
|
111
|
+
if (customValidation && !customValidation.isValid) {
|
112
|
+
return customValidation
|
113
|
+
}
|
114
|
+
}
|
115
|
+
|
116
|
+
return { isValid: true }
|
117
|
+
}, [allowDuplicates, tokens, maxTokens, validateToken])
|
118
|
+
|
119
|
+
// Add token helper
|
120
|
+
const addToken = useCallback((tokenValue) => {
|
121
|
+
if (disabled || readOnly) return false
|
24
122
|
|
25
|
-
|
26
|
-
|
123
|
+
const validation = validateTokenValue(tokenValue)
|
124
|
+
if (!validation.isValid) {
|
125
|
+
setValidationErrors(prev => ({ ...prev, [tokenValue]: validation.error }))
|
126
|
+
if (onValidationError) onValidationError(validation.error, tokenValue)
|
127
|
+
return false
|
128
|
+
}
|
129
|
+
|
130
|
+
const next = Array.isArray(tokens) ? tokens.concat(tokenValue) : [tokenValue]
|
131
|
+
if (onChange) onChange(id, next)
|
132
|
+
if (onTokenAdd) onTokenAdd(tokenValue, tokens.length)
|
133
|
+
|
134
|
+
// Clear validation error
|
135
|
+
setValidationErrors(prev => {
|
136
|
+
const newErrors = { ...prev }
|
137
|
+
delete newErrors[tokenValue]
|
138
|
+
return newErrors
|
139
|
+
})
|
140
|
+
|
141
|
+
return true
|
142
|
+
}, [disabled, readOnly, validateTokenValue, tokens, id, onChange, onTokenAdd, onValidationError])
|
143
|
+
|
144
|
+
// Enhanced changeDropDown function (maintaining original behavior)
|
145
|
+
const changeDropDown = useCallback((fid, value) => {
|
27
146
|
const isNumericValue = !isNaN(value)
|
28
147
|
const valueIsNotEmpty = (value && value.length > 0) || isNumericValue
|
29
148
|
|
30
149
|
if (valueIsNotEmpty) {
|
31
|
-
|
32
|
-
if (onChange) onChange(id, next)
|
150
|
+
addToken(value)
|
33
151
|
}
|
34
|
-
|
152
|
+
|
35
153
|
setValue('')
|
36
|
-
}
|
154
|
+
}, [addToken])
|
155
|
+
|
156
|
+
// Enhanced onEnter function (maintaining original behavior + improvements)
|
157
|
+
const onEnter = useCallback((event) => {
|
158
|
+
if (disabled || readOnly) return
|
37
159
|
|
38
|
-
|
39
|
-
if (event.key === 'Enter') {
|
160
|
+
// Handle separators
|
161
|
+
if (tokenSeparators.includes(event.key) || event.key === 'Enter') {
|
40
162
|
event.preventDefault()
|
41
163
|
event.stopPropagation()
|
42
|
-
|
164
|
+
|
165
|
+
const token = event.target.value.trim()
|
43
166
|
if (token && token.length > 0) {
|
44
|
-
|
45
|
-
|
46
|
-
|
167
|
+
if (addToken(token)) {
|
168
|
+
setValue('')
|
169
|
+
}
|
47
170
|
}
|
171
|
+
return
|
48
172
|
}
|
49
173
|
|
174
|
+
// Handle backspace to remove last token (maintaining original behavior)
|
50
175
|
if (value === '' && tokens.length > 0 && event.key === 'Backspace') {
|
51
|
-
|
52
|
-
|
176
|
+
event.preventDefault()
|
177
|
+
remove(tokens.length - 1)
|
178
|
+
return
|
179
|
+
}
|
180
|
+
|
181
|
+
// Handle paste with multiple tokens
|
182
|
+
if (event.key === 'v' && (event.ctrlKey || event.metaKey)) {
|
183
|
+
// Let the paste event handle this
|
184
|
+
setTimeout(() => {
|
185
|
+
const pastedValue = event.target.value
|
186
|
+
const newTokens = pastedValue.split(new RegExp(`[${tokenSeparators.join('')}]`))
|
187
|
+
.map(token => token.trim())
|
188
|
+
.filter(token => token.length > 0)
|
189
|
+
|
190
|
+
if (newTokens.length > 1) {
|
191
|
+
event.target.value = ''
|
192
|
+
setValue('')
|
193
|
+
newTokens.forEach(token => addToken(token))
|
194
|
+
}
|
195
|
+
}, 0)
|
196
|
+
}
|
197
|
+
}, [disabled, readOnly, tokenSeparators, value, tokens, addToken, remove])
|
198
|
+
|
199
|
+
// Handle focus
|
200
|
+
const handleFocus = useCallback((event) => {
|
201
|
+
setIsFocused(true)
|
202
|
+
if (onFocus) onFocus(event)
|
203
|
+
}, [onFocus])
|
204
|
+
|
205
|
+
// Handle blur
|
206
|
+
const handleBlur = useCallback((event) => {
|
207
|
+
setIsFocused(false)
|
208
|
+
|
209
|
+
// Add token on blur if there's a value
|
210
|
+
const token = event.target.value.trim()
|
211
|
+
if (token && token.length > 0) {
|
212
|
+
if (addToken(token)) {
|
213
|
+
setValue('')
|
214
|
+
}
|
215
|
+
}
|
216
|
+
|
217
|
+
if (onBlur) onBlur(event)
|
218
|
+
}, [onBlur, addToken])
|
219
|
+
|
220
|
+
// Handle clear all
|
221
|
+
const handleClear = useCallback(() => {
|
222
|
+
if (disabled || readOnly || minTokens > 0) return
|
223
|
+
|
224
|
+
if (onChange) onChange(id, [])
|
225
|
+
if (onClear) onClear()
|
226
|
+
setValue('')
|
227
|
+
}, [disabled, readOnly, minTokens, id, onChange, onClear])
|
228
|
+
|
229
|
+
// Filter and sort options
|
230
|
+
const getFilteredOptions = useCallback(() => {
|
231
|
+
if (!options) return []
|
232
|
+
|
233
|
+
const tks = tokens || []
|
234
|
+
const usedValues = tks.map(token => token)
|
235
|
+
let filteredOptions = options.filter(option => !usedValues.includes(option.value))
|
236
|
+
|
237
|
+
if (searchable && value) {
|
238
|
+
filteredOptions = filteredOptions.filter(option =>
|
239
|
+
option.label?.toLowerCase().includes(value.toLowerCase()) ||
|
240
|
+
option.value?.toString().toLowerCase().includes(value.toLowerCase())
|
241
|
+
)
|
242
|
+
}
|
243
|
+
|
244
|
+
if (sortable) {
|
245
|
+
filteredOptions.sort((a, b) => {
|
246
|
+
if (!a.label || !b.label) return 0
|
247
|
+
try {
|
248
|
+
return a.label.localeCompare(b.label)
|
249
|
+
} catch (error) {
|
250
|
+
console.log('Error sorting options', error, a, b)
|
251
|
+
return 0
|
252
|
+
}
|
253
|
+
})
|
53
254
|
}
|
255
|
+
|
256
|
+
return filteredOptions
|
257
|
+
}, [options, tokens, searchable, value, sortable])
|
258
|
+
|
259
|
+
// Generate CSS classes
|
260
|
+
const cssClasses = [
|
261
|
+
'tokenField',
|
262
|
+
`tokenField--${size}`,
|
263
|
+
`tokenField--${variant}`,
|
264
|
+
disabled && 'tokenField--disabled',
|
265
|
+
readOnly && 'tokenField--readonly',
|
266
|
+
required && 'tokenField--required',
|
267
|
+
isFocused && 'tokenField--focused',
|
268
|
+
error && 'tokenField--error',
|
269
|
+
className
|
270
|
+
].filter(Boolean).join(' ')
|
271
|
+
|
272
|
+
// Generate accessibility attributes
|
273
|
+
const ariaAttributes = {
|
274
|
+
'aria-label': label || 'Token field',
|
275
|
+
'aria-disabled': disabled,
|
276
|
+
'aria-readonly': readOnly,
|
277
|
+
'aria-required': required,
|
278
|
+
'aria-invalid': !!error,
|
279
|
+
'aria-describedby': helperText ? `${id}-helper` : undefined,
|
280
|
+
role: 'group'
|
54
281
|
}
|
55
282
|
|
56
283
|
const tks = Array.isArray(tokens) ? tokens : []
|
57
|
-
const sortedOptions =
|
58
|
-
if (!a.label || !b.label) return 0
|
59
|
-
try {
|
60
|
-
return a.label.localeCompare(b.label)
|
61
|
-
} catch (error) {
|
62
|
-
console.log('Error sorting options', error, a, b)
|
63
|
-
return 0
|
64
|
-
}
|
65
|
-
}) : null
|
284
|
+
const sortedOptions = getFilteredOptions()
|
66
285
|
|
67
286
|
return (
|
68
|
-
<div
|
69
|
-
|
70
|
-
|
71
|
-
{
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
287
|
+
<div
|
288
|
+
className={cssClasses}
|
289
|
+
style={style}
|
290
|
+
ref={containerRef}
|
291
|
+
{...ariaAttributes}
|
292
|
+
{...restProps}
|
293
|
+
>
|
294
|
+
{/* Label (maintaining original structure) */}
|
295
|
+
<label htmlFor={id}>
|
296
|
+
{label}
|
297
|
+
{required && <span className="tokenField__required">*</span>}
|
298
|
+
</label>
|
79
299
|
|
80
|
-
{
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
300
|
+
{/* Tokens container */}
|
301
|
+
<div className="tokenField__tokens">
|
302
|
+
{tks.map((text, index) => {
|
303
|
+
let text2 = text
|
304
|
+
if (options) {
|
305
|
+
const lbl = options.find(opt => opt.value == text)
|
306
|
+
if (lbl) text2 = lbl.label
|
307
|
+
}
|
308
|
+
|
309
|
+
const hasError = validationErrors[text]
|
310
|
+
|
311
|
+
return (
|
312
|
+
<Token
|
313
|
+
key={`${text}-${index}`}
|
314
|
+
text={text2}
|
315
|
+
onDelete={() => remove(index)}
|
316
|
+
disabled={disabled || readOnly}
|
317
|
+
error={hasError}
|
318
|
+
size={size}
|
319
|
+
variant={variant}
|
320
|
+
/>
|
321
|
+
)
|
322
|
+
})}
|
323
|
+
|
324
|
+
{/* Input area */}
|
325
|
+
<div className="tokenField__input-container">
|
326
|
+
{options ? (
|
327
|
+
<DropDown
|
328
|
+
id={id}
|
329
|
+
onChange={changeDropDown}
|
330
|
+
options={sortedOptions}
|
331
|
+
predictive={predictive}
|
332
|
+
verbose={false}
|
333
|
+
disabled={disabled}
|
334
|
+
readOnly={readOnly}
|
335
|
+
placeholder={placeholder}
|
336
|
+
onFocus={handleFocus}
|
337
|
+
onBlur={handleBlur}
|
338
|
+
/>
|
339
|
+
) : (
|
340
|
+
<input
|
341
|
+
ref={inputRef}
|
342
|
+
id={id}
|
343
|
+
type='text'
|
344
|
+
value={value}
|
345
|
+
onChange={change}
|
346
|
+
onKeyDown={onEnter}
|
347
|
+
onFocus={handleFocus}
|
348
|
+
onBlur={handleBlur}
|
349
|
+
readOnly={readOnly}
|
350
|
+
disabled={disabled}
|
351
|
+
placeholder={placeholder}
|
352
|
+
className="tokenField__input"
|
353
|
+
aria-label={`Add ${label || 'token'}`}
|
354
|
+
/>
|
355
|
+
)}
|
356
|
+
|
357
|
+
{/* Clear all button */}
|
358
|
+
{clearable && tks.length > 0 && minTokens === 0 && (
|
359
|
+
<button
|
360
|
+
type="button"
|
361
|
+
onClick={handleClear}
|
362
|
+
disabled={disabled || readOnly}
|
363
|
+
className="tokenField__clear"
|
364
|
+
aria-label="Clear all tokens"
|
365
|
+
>
|
366
|
+
<Icon icon="clear" size="small" />
|
367
|
+
</button>
|
368
|
+
)}
|
369
|
+
</div>
|
370
|
+
</div>
|
371
|
+
|
372
|
+
{/* Token count and limits */}
|
373
|
+
{(maxTokens || minTokens > 0) && (
|
374
|
+
<div className="tokenField__count">
|
375
|
+
<Text size="sm" color="muted">
|
376
|
+
{tks.length}
|
377
|
+
{maxTokens && ` / ${maxTokens}`}
|
378
|
+
{maxTokens ? ' tokens' : ` of ${tokens.length} tokens`}
|
379
|
+
</Text>
|
380
|
+
{minTokens > 0 && tks.length < minTokens && (
|
381
|
+
<Text size="sm" color="error">
|
382
|
+
Minimum {minTokens} required
|
383
|
+
</Text>
|
384
|
+
)}
|
385
|
+
</div>
|
386
|
+
)}
|
387
|
+
|
388
|
+
{/* Helper text or error */}
|
389
|
+
{(helperText || error) && (
|
390
|
+
<div
|
391
|
+
id={`${id}-helper`}
|
392
|
+
className={`tokenField__helper ${error ? 'tokenField__helper--error' : ''}`}
|
393
|
+
>
|
394
|
+
<Text size="sm" color={error ? 'error' : 'muted'}>
|
395
|
+
{error || helperText}
|
396
|
+
</Text>
|
397
|
+
</div>
|
398
|
+
)}
|
85
399
|
</div>
|
86
400
|
)
|
87
401
|
}
|
88
402
|
|
89
403
|
/**
|
90
|
-
* Token
|
404
|
+
* Enhanced Token component with improved functionality while maintaining 100% compatibility
|
91
405
|
*/
|
92
|
-
const Token = (
|
406
|
+
const Token = (props) => {
|
407
|
+
const {
|
408
|
+
// Original props (100% compatible)
|
409
|
+
text,
|
410
|
+
onDelete,
|
411
|
+
// New enhanced props (all optional for compatibility)
|
412
|
+
disabled = false,
|
413
|
+
error = false,
|
414
|
+
size = 'medium',
|
415
|
+
variant = 'default',
|
416
|
+
icon,
|
417
|
+
className,
|
418
|
+
style,
|
419
|
+
...restProps
|
420
|
+
} = props
|
421
|
+
|
422
|
+
// Handle delete
|
423
|
+
const handleDelete = useCallback((event) => {
|
424
|
+
if (disabled) return
|
425
|
+
event.preventDefault()
|
426
|
+
event.stopPropagation()
|
427
|
+
if (onDelete) onDelete()
|
428
|
+
}, [disabled, onDelete])
|
429
|
+
|
430
|
+
// Handle keyboard interaction
|
431
|
+
const handleKeyDown = useCallback((event) => {
|
432
|
+
if (disabled) return
|
433
|
+
|
434
|
+
switch (event.key) {
|
435
|
+
case 'Delete':
|
436
|
+
case 'Backspace':
|
437
|
+
event.preventDefault()
|
438
|
+
if (onDelete) onDelete()
|
439
|
+
break
|
440
|
+
default:
|
441
|
+
break
|
442
|
+
}
|
443
|
+
}, [disabled, onDelete])
|
444
|
+
|
445
|
+
// Generate CSS classes
|
446
|
+
const cssClasses = [
|
447
|
+
'token',
|
448
|
+
`token--${size}`,
|
449
|
+
`token--${variant}`,
|
450
|
+
disabled && 'token--disabled',
|
451
|
+
error && 'token--error',
|
452
|
+
className
|
453
|
+
].filter(Boolean).join(' ')
|
454
|
+
|
93
455
|
return (
|
94
|
-
<div
|
95
|
-
|
456
|
+
<div
|
457
|
+
className={cssClasses}
|
458
|
+
style={style}
|
459
|
+
tabIndex={disabled ? -1 : 0}
|
460
|
+
onKeyDown={handleKeyDown}
|
461
|
+
role="button"
|
462
|
+
aria-label={`Token: ${text}. Press Delete to remove.`}
|
463
|
+
aria-disabled={disabled}
|
464
|
+
{...restProps}
|
465
|
+
>
|
466
|
+
{/* Icon (optional) */}
|
467
|
+
{icon && (
|
468
|
+
<Icon
|
469
|
+
icon={icon}
|
470
|
+
size={size === 'small' ? 'small' : 'medium'}
|
471
|
+
className="token__icon"
|
472
|
+
/>
|
473
|
+
)}
|
474
|
+
|
475
|
+
{/* Text content (maintaining original structure) */}
|
476
|
+
<Text use='caption' tag='div' className="token__text">
|
96
477
|
{text}
|
97
478
|
</Text>
|
98
|
-
|
479
|
+
|
480
|
+
{/* Delete button (maintaining original structure) */}
|
481
|
+
<Icon
|
482
|
+
icon='close'
|
483
|
+
clickable
|
484
|
+
action={handleDelete}
|
485
|
+
size="small"
|
486
|
+
className="token__delete"
|
487
|
+
disabled={disabled}
|
488
|
+
aria-label="Remove token"
|
489
|
+
/>
|
99
490
|
</div>
|
100
491
|
)
|
101
|
-
}
|
492
|
+
}
|
493
|
+
|
494
|
+
// PropTypes for TokenField
|
495
|
+
TokenField.propTypes = {
|
496
|
+
/** Field ID */
|
497
|
+
id: PropTypes.string,
|
498
|
+
/** Field label */
|
499
|
+
label: PropTypes.string,
|
500
|
+
/** Array of tokens */
|
501
|
+
tokens: PropTypes.array,
|
502
|
+
/** Read-only state */
|
503
|
+
readOnly: PropTypes.bool,
|
504
|
+
/** Options for dropdown */
|
505
|
+
options: PropTypes.arrayOf(PropTypes.shape({
|
506
|
+
label: PropTypes.string,
|
507
|
+
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
|
508
|
+
})),
|
509
|
+
/** Enable predictive input */
|
510
|
+
predictive: PropTypes.bool,
|
511
|
+
/** Change callback */
|
512
|
+
onChange: PropTypes.func,
|
513
|
+
/** Disabled state */
|
514
|
+
disabled: PropTypes.bool,
|
515
|
+
/** Required field */
|
516
|
+
required: PropTypes.bool,
|
517
|
+
/** Input placeholder */
|
518
|
+
placeholder: PropTypes.string,
|
519
|
+
/** Maximum tokens allowed */
|
520
|
+
maxTokens: PropTypes.number,
|
521
|
+
/** Minimum tokens required */
|
522
|
+
minTokens: PropTypes.number,
|
523
|
+
/** Allow duplicate tokens */
|
524
|
+
allowDuplicates: PropTypes.bool,
|
525
|
+
/** Token validation function */
|
526
|
+
validateToken: PropTypes.func,
|
527
|
+
/** Token separator characters */
|
528
|
+
tokenSeparators: PropTypes.arrayOf(PropTypes.string),
|
529
|
+
/** Field size */
|
530
|
+
size: PropTypes.oneOf(['small', 'medium', 'large']),
|
531
|
+
/** Visual variant */
|
532
|
+
variant: PropTypes.oneOf(['default', 'outlined', 'filled']),
|
533
|
+
/** Error message */
|
534
|
+
error: PropTypes.string,
|
535
|
+
/** Helper text */
|
536
|
+
helperText: PropTypes.string,
|
537
|
+
/** Focus callback */
|
538
|
+
onFocus: PropTypes.func,
|
539
|
+
/** Blur callback */
|
540
|
+
onBlur: PropTypes.func,
|
541
|
+
/** Token add callback */
|
542
|
+
onTokenAdd: PropTypes.func,
|
543
|
+
/** Token remove callback */
|
544
|
+
onTokenRemove: PropTypes.func,
|
545
|
+
/** Validation error callback */
|
546
|
+
onValidationError: PropTypes.func,
|
547
|
+
/** Show clear button */
|
548
|
+
clearable: PropTypes.bool,
|
549
|
+
/** Clear callback */
|
550
|
+
onClear: PropTypes.func,
|
551
|
+
/** Enable search in options */
|
552
|
+
searchable: PropTypes.bool,
|
553
|
+
/** Sort options alphabetically */
|
554
|
+
sortable: PropTypes.bool,
|
555
|
+
/** Additional CSS classes */
|
556
|
+
className: PropTypes.string,
|
557
|
+
/** Inline styles */
|
558
|
+
style: PropTypes.object
|
559
|
+
}
|
560
|
+
|
561
|
+
TokenField.defaultProps = {
|
562
|
+
tokens: [],
|
563
|
+
predictive: true,
|
564
|
+
disabled: false,
|
565
|
+
required: false,
|
566
|
+
placeholder: "Add token...",
|
567
|
+
minTokens: 0,
|
568
|
+
allowDuplicates: true,
|
569
|
+
tokenSeparators: [',', ';', '\n'],
|
570
|
+
size: 'medium',
|
571
|
+
variant: 'default',
|
572
|
+
clearable: false,
|
573
|
+
searchable: false,
|
574
|
+
sortable: false
|
575
|
+
}
|
576
|
+
|
577
|
+
// PropTypes for Token
|
578
|
+
Token.propTypes = {
|
579
|
+
/** Token text */
|
580
|
+
text: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,
|
581
|
+
/** Delete callback */
|
582
|
+
onDelete: PropTypes.func,
|
583
|
+
/** Disabled state */
|
584
|
+
disabled: PropTypes.bool,
|
585
|
+
/** Error state */
|
586
|
+
error: PropTypes.bool,
|
587
|
+
/** Token size */
|
588
|
+
size: PropTypes.oneOf(['small', 'medium', 'large']),
|
589
|
+
/** Visual variant */
|
590
|
+
variant: PropTypes.oneOf(['default', 'outlined', 'filled']),
|
591
|
+
/** Optional icon */
|
592
|
+
icon: PropTypes.string,
|
593
|
+
/** Additional CSS classes */
|
594
|
+
className: PropTypes.string,
|
595
|
+
/** Inline styles */
|
596
|
+
style: PropTypes.object
|
597
|
+
}
|
598
|
+
|
599
|
+
Token.defaultProps = {
|
600
|
+
disabled: false,
|
601
|
+
error: false,
|
602
|
+
size: 'medium',
|
603
|
+
variant: 'default'
|
604
|
+
}
|
605
|
+
|
606
|
+
export default TokenField
|