ywana-core8 0.1.75 → 0.1.76

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.
Files changed (122) hide show
  1. package/ACCORDION_EVALUATION.md +583 -0
  2. package/CHECKBOX_EVALUATION.md +273 -0
  3. package/CHIP_EVALUATION.md +542 -0
  4. package/COLOR_EVALUATION.md +524 -0
  5. package/COMPONENTS_EVALUATION.md +477 -0
  6. package/FORM_EVALUATION.md +459 -0
  7. package/HEADER_EVALUATION.md +436 -0
  8. package/ICON_EVALUATION.md +254 -0
  9. package/LIST_EVALUATION.md +574 -0
  10. package/PROGRESS_EVALUATION.md +450 -0
  11. package/RADIO_EVALUATION.md +439 -0
  12. package/RADIO_VISUAL_FIX.md +183 -0
  13. package/SECTION_IMPROVEMENTS.md +153 -0
  14. package/SWITCH_EVALUATION.md +335 -0
  15. package/SWITCH_VISUAL_FIX.md +232 -0
  16. package/TAB_EVALUATION.md +626 -0
  17. package/TEXTFIELD_EVALUATION.md +747 -0
  18. package/TOOLTIP_FIX.md +157 -0
  19. package/TREE_EVALUATION.md +708 -0
  20. package/dist/index.cjs +7900 -1615
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.css +6094 -1122
  23. package/dist/index.css.map +1 -1
  24. package/dist/index.modern.js +7929 -1645
  25. package/dist/index.modern.js.map +1 -1
  26. package/dist/index.umd.js +7900 -1615
  27. package/dist/index.umd.js.map +1 -1
  28. package/jest.config.js +24 -0
  29. package/package.json +10 -1
  30. package/src/html/accordion.css +208 -4
  31. package/src/html/accordion.example.js +390 -0
  32. package/src/html/accordion.js +284 -28
  33. package/src/html/accordion.unit.test.js +334 -0
  34. package/src/html/button.css +157 -16
  35. package/src/html/button.example.js +374 -0
  36. package/src/html/button.js +240 -60
  37. package/src/html/button.test.js +422 -0
  38. package/src/html/checkbox.css +74 -2
  39. package/src/html/checkbox.example.js +316 -0
  40. package/src/html/checkbox.js +113 -26
  41. package/src/html/checkbox.test.js +285 -0
  42. package/src/html/chip.css +230 -19
  43. package/src/html/chip.example.js +355 -0
  44. package/src/html/chip.js +321 -25
  45. package/src/html/chip.test.js +425 -0
  46. package/src/html/color.css +435 -6
  47. package/src/html/color.example.js +527 -0
  48. package/src/html/color.js +458 -9
  49. package/src/html/color.test.js +362 -4
  50. package/src/html/components.example.js +492 -0
  51. package/src/html/components_enhanced.test.js +581 -0
  52. package/src/html/form.css +70 -3
  53. package/src/html/form.example.js +385 -0
  54. package/src/html/form.js +232 -34
  55. package/src/html/form.test.js +369 -0
  56. package/src/html/header2.css +264 -0
  57. package/src/html/header2.example.js +411 -0
  58. package/src/html/header2.js +203 -0
  59. package/src/html/header2.test.js +377 -0
  60. package/src/html/icon.css +20 -2
  61. package/src/html/icon.example.js +268 -0
  62. package/src/html/icon.js +86 -16
  63. package/src/html/icon.test.js +231 -0
  64. package/src/html/index.js +1 -1
  65. package/src/html/list.css +393 -1
  66. package/src/html/list.example.js +404 -0
  67. package/src/html/list.js +583 -40
  68. package/src/html/list.test.js +383 -0
  69. package/src/html/progress.css +707 -17
  70. package/src/html/progress.example.js +424 -0
  71. package/src/html/progress.js +906 -9
  72. package/src/html/progress.test.js +313 -0
  73. package/src/html/property.css +399 -0
  74. package/src/html/property.example.js +553 -0
  75. package/src/html/property.js +393 -15
  76. package/src/html/property.test.js +351 -2
  77. package/src/html/radio-visual-test.js +289 -0
  78. package/src/html/radio.css +137 -11
  79. package/src/html/radio.example.js +389 -0
  80. package/src/html/radio.js +234 -10
  81. package/src/html/radio.test.js +318 -0
  82. package/src/html/section.example.js +99 -0
  83. package/src/html/section.js +40 -3
  84. package/src/html/section.test.js +131 -0
  85. package/src/html/selector.css +329 -3
  86. package/src/html/selector.js +369 -23
  87. package/src/html/switch-debug.js +197 -0
  88. package/src/html/switch-test-visual.js +294 -0
  89. package/src/html/switch.css +200 -0
  90. package/src/html/switch.example.js +461 -0
  91. package/src/html/switch.js +283 -23
  92. package/src/html/switch.test.js +355 -0
  93. package/src/html/tab.css +288 -0
  94. package/src/html/tab.example.js +446 -0
  95. package/src/html/tab.js +387 -22
  96. package/src/html/tab_enhanced.js +378 -0
  97. package/src/html/tab_enhanced.test.js +504 -0
  98. package/src/html/table2.css +576 -0
  99. package/src/html/table2.example.js +703 -0
  100. package/src/html/table2.js +1252 -0
  101. package/src/html/table2.migration.md +328 -0
  102. package/src/html/table2.test.js +582 -0
  103. package/src/html/text.css +375 -0
  104. package/src/html/text.js +311 -20
  105. package/src/html/textfield2.css +842 -0
  106. package/src/html/textfield2.example.js +499 -0
  107. package/src/html/textfield2.js +1130 -0
  108. package/src/html/textfield2.test.js +950 -0
  109. package/src/html/thumbnail.css +289 -2
  110. package/src/html/thumbnail.js +214 -9
  111. package/src/html/tokenfield.css +449 -1
  112. package/src/html/tokenfield.example.js +503 -0
  113. package/src/html/tokenfield.js +561 -56
  114. package/src/html/tokenfield.test.js +423 -0
  115. package/src/html/tooltip-positioning-demo.js +187 -0
  116. package/src/html/tooltip.css +25 -2
  117. package/src/html/tree.css +228 -0
  118. package/src/html/tree.example.js +475 -0
  119. package/src/html/tree.js +712 -28
  120. package/src/html/tree_enhanced.test.js +495 -0
  121. package/table2.test.js +454 -0
  122. package/src/html/button.tsx +0 -38
@@ -0,0 +1,1130 @@
1
+ import React, { useContext, useEffect, useState, useCallback, useRef, useMemo } from 'react'
2
+ import PropTypes from 'prop-types'
3
+ import { SiteContext } from '../site/siteContext'
4
+ import { Icon } from './icon'
5
+ import { Text } from './text'
6
+ import './textfield2.css'
7
+
8
+ /**
9
+ * Enhanced TextField component with improved validation and accessibility
10
+ */
11
+ export const TextField2 = (props) => {
12
+ const {
13
+ id,
14
+ type = 'text',
15
+ className,
16
+ label,
17
+ labelPosition = 'top',
18
+ placeholder,
19
+ value,
20
+ outlined = false,
21
+ readOnly = false,
22
+ disabled = false,
23
+ required = false,
24
+ canClear = true,
25
+ showPasswordToggle = true,
26
+ autoComplete = 'off',
27
+ error,
28
+ helperText,
29
+ maxLength,
30
+ minLength,
31
+ pattern,
32
+ step,
33
+ min,
34
+ max,
35
+ rows = 3,
36
+ validation,
37
+ debounceMs = 0,
38
+ ariaLabel,
39
+ ariaDescribedBy,
40
+ onChange,
41
+ onEnter,
42
+ onClick,
43
+ onFocus,
44
+ onBlur,
45
+ onValidation,
46
+ ...restProps
47
+ } = props
48
+
49
+ const site = useContext(SiteContext)
50
+ const [isPasswordVisible, setIsPasswordVisible] = useState(false)
51
+ const [isFocused, setIsFocused] = useState(false)
52
+ const [internalError, setInternalError] = useState('')
53
+ const [isValid, setIsValid] = useState(true)
54
+ const inputRef = useRef(null)
55
+ const debounceRef = useRef(null)
56
+
57
+ // Validate required props
58
+ if (!id) {
59
+ console.warn('TextField2 component: id prop is required')
60
+ }
61
+
62
+ // Validate value and set error states
63
+ useEffect(() => {
64
+ if (validation && value !== undefined) {
65
+ const validationResult = validation(value)
66
+ const valid = typeof validationResult === 'boolean' ? validationResult : validationResult.valid
67
+ const errorMessage = typeof validationResult === 'object' ? validationResult.message : ''
68
+
69
+ setIsValid(valid)
70
+ setInternalError(valid ? '' : errorMessage || (required && !value ? 'This field is required' : 'Invalid value'))
71
+
72
+ if (onValidation) {
73
+ onValidation(id, valid, errorMessage)
74
+ }
75
+ } else if (required && !value) {
76
+ setIsValid(false)
77
+ setInternalError('This field is required')
78
+ } else {
79
+ setIsValid(true)
80
+ setInternalError('')
81
+ }
82
+ }, [value, validation, required, id, onValidation])
83
+
84
+ // Handle input changes with debouncing
85
+ const handleChange = useCallback((event) => {
86
+ if (disabled || readOnly) return
87
+
88
+ event.stopPropagation()
89
+ const newValue = event.target.value
90
+
91
+ // Clear previous debounce
92
+ if (debounceRef.current) {
93
+ clearTimeout(debounceRef.current)
94
+ }
95
+
96
+ if (debounceMs > 0) {
97
+ debounceRef.current = setTimeout(() => {
98
+ if (onChange) onChange(id, newValue, event)
99
+ }, debounceMs)
100
+ } else {
101
+ if (onChange) onChange(id, newValue, event)
102
+ }
103
+ }, [disabled, readOnly, id, onChange, debounceMs])
104
+
105
+ // Handle key press events
106
+ const handleKeyPress = useCallback((event) => {
107
+ if (disabled) return
108
+
109
+ const key = event.charCode || event.keyCode || 0
110
+ if (key === 13 && onEnter) {
111
+ event.preventDefault()
112
+ onEnter(event)
113
+ }
114
+ }, [disabled, onEnter])
115
+
116
+ // Handle focus events
117
+ const handleFocus = useCallback((event) => {
118
+ if (disabled) return
119
+
120
+ setIsFocused(true)
121
+ if (onFocus) onFocus(event)
122
+
123
+ // Site context focus management
124
+ if (site && site.changeFocus) {
125
+ site.changeFocus({
126
+ lose: () => setIsFocused(false)
127
+ })
128
+ }
129
+ }, [disabled, onFocus, site])
130
+
131
+ // Handle blur events
132
+ const handleBlur = useCallback((event) => {
133
+ if (disabled) return
134
+
135
+ setIsFocused(false)
136
+ if (onBlur) onBlur(event)
137
+ }, [disabled, onBlur])
138
+
139
+ // Handle clear action
140
+ const handleClear = useCallback(() => {
141
+ if (disabled || readOnly) return
142
+
143
+ if (onChange) onChange(id, '', { target: { value: '' } })
144
+ if (inputRef.current) {
145
+ inputRef.current.focus()
146
+ }
147
+ }, [disabled, readOnly, id, onChange])
148
+
149
+ // Handle password visibility toggle
150
+ const handlePasswordToggle = useCallback(() => {
151
+ if (disabled) return
152
+ setIsPasswordVisible(!isPasswordVisible)
153
+ }, [disabled, isPasswordVisible])
154
+
155
+ // Generate CSS classes
156
+ const borderStyle = outlined ? 'textfield2-outlined' : 'textfield2'
157
+ const labelStyle = label ? '' : 'no-label'
158
+ const labelPositionStyle = labelPosition === 'left' ? 'label-left' : 'label-top'
159
+ const safeClassName = className || ''
160
+
161
+ const cssClasses = [
162
+ 'textfield2',
163
+ borderStyle,
164
+ labelStyle,
165
+ labelPositionStyle,
166
+ `textfield2-${type}`,
167
+ isFocused && 'focused',
168
+ disabled && 'disabled',
169
+ readOnly && 'readonly',
170
+ error || internalError ? 'error' : '',
171
+ !isValid && 'invalid',
172
+ safeClassName,
173
+ id
174
+ ].filter(Boolean).join(' ')
175
+
176
+ // Accessibility attributes
177
+ const ariaAttributes = {
178
+ 'aria-label': ariaLabel || label,
179
+ 'aria-describedby': ariaDescribedBy || (error || internalError || helperText ? `${id}-helper` : undefined),
180
+ 'aria-invalid': !isValid || !!(error || internalError),
181
+ 'aria-required': required,
182
+ 'aria-disabled': disabled,
183
+ 'aria-readonly': readOnly
184
+ }
185
+
186
+ // Input attributes
187
+ const inputAttributes = {
188
+ id,
189
+ type: type === 'password' && isPasswordVisible ? 'text' : type,
190
+ placeholder: site?.translate ? site.translate(placeholder) : placeholder,
191
+ value: value || '',
192
+ required,
193
+ disabled,
194
+ readOnly,
195
+ maxLength,
196
+ minLength,
197
+ pattern,
198
+ step,
199
+ min,
200
+ max,
201
+ autoComplete,
202
+ ...ariaAttributes,
203
+ ...restProps
204
+ }
205
+
206
+ // Label text
207
+ const labelTxt = label ? <Text>{label}</Text> : null
208
+ const placeholderTxt = site?.translate ? site.translate(placeholder) : placeholder
209
+
210
+ // Error/helper text
211
+ const displayError = error || internalError
212
+ const displayHelperText = helperText && !displayError
213
+
214
+ return (
215
+ <div className={cssClasses} onClick={onClick}>
216
+ {type === 'textarea' ? (
217
+ <textarea
218
+ ref={inputRef}
219
+ rows={rows}
220
+ placeholder={placeholderTxt}
221
+ onChange={handleChange}
222
+ onKeyDown={handleKeyPress}
223
+ onFocus={handleFocus}
224
+ onBlur={handleBlur}
225
+ {...inputAttributes}
226
+ />
227
+ ) : (
228
+ <input
229
+ ref={inputRef}
230
+ onChange={handleChange}
231
+ onKeyDown={handleKeyPress}
232
+ onFocus={handleFocus}
233
+ onBlur={handleBlur}
234
+ {...inputAttributes}
235
+ />
236
+ )}
237
+
238
+ {/* Clear button */}
239
+ {!readOnly && !disabled && canClear && value && value.length > 0 && (
240
+ <Icon
241
+ icon="close"
242
+ clickable
243
+ size="small"
244
+ action={handleClear}
245
+ className="textfield2-clear"
246
+ ariaLabel="Clear field"
247
+ />
248
+ )}
249
+
250
+ {/* Password toggle */}
251
+ {type === 'password' && showPasswordToggle && !disabled && (
252
+ <Icon
253
+ icon={isPasswordVisible ? 'visibility' : 'visibility_off'}
254
+ clickable
255
+ size="small"
256
+ action={handlePasswordToggle}
257
+ className="textfield2-password-toggle"
258
+ ariaLabel={isPasswordVisible ? 'Hide password' : 'Show password'}
259
+ />
260
+ )}
261
+
262
+ {/* Focus bar */}
263
+ <span className="textfield2-bar"></span>
264
+
265
+ {/* Label */}
266
+ {label && <label htmlFor={id}>{labelTxt}</label>}
267
+
268
+ {/* Error/Helper text */}
269
+ {(displayError || displayHelperText) && (
270
+ <div
271
+ id={`${id}-helper`}
272
+ className={`textfield2-helper ${displayError ? 'error' : 'helper'}`}
273
+ role={displayError ? 'alert' : 'status'}
274
+ aria-live={displayError ? 'assertive' : 'polite'}
275
+ >
276
+ {displayError && <Icon icon="error" size="small" />}
277
+ <Text>{displayError || helperText}</Text>
278
+ </div>
279
+ )}
280
+ </div>
281
+ )
282
+ }
283
+
284
+ /**
285
+ * Enhanced TextArea component
286
+ */
287
+ export const TextArea2 = (props) => {
288
+ return <TextField2 {...props} type="textarea" />
289
+ }
290
+
291
+ /**
292
+ * Enhanced PasswordField component
293
+ */
294
+ export const PasswordField2 = (props) => {
295
+ return <TextField2 {...props} type="password" />
296
+ }
297
+
298
+ // PropTypes for TextField2
299
+ TextField2.propTypes = {
300
+ /** Unique identifier for the field */
301
+ id: PropTypes.string.isRequired,
302
+ /** Input type */
303
+ type: PropTypes.oneOf(['text', 'email', 'password', 'number', 'tel', 'url', 'search', 'date', 'time', 'datetime-local', 'month', 'week', 'textarea']),
304
+ /** Additional CSS classes */
305
+ className: PropTypes.string,
306
+ /** Field label */
307
+ label: PropTypes.string,
308
+ /** Label position */
309
+ labelPosition: PropTypes.oneOf(['top', 'left']),
310
+ /** Placeholder text */
311
+ placeholder: PropTypes.string,
312
+ /** Field value */
313
+ value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
314
+ /** Whether field has outlined style */
315
+ outlined: PropTypes.bool,
316
+ /** Whether field is read-only */
317
+ readOnly: PropTypes.bool,
318
+ /** Whether field is disabled */
319
+ disabled: PropTypes.bool,
320
+ /** Whether field is required */
321
+ required: PropTypes.bool,
322
+ /** Whether to show clear button */
323
+ canClear: PropTypes.bool,
324
+ /** Whether to show password toggle for password fields */
325
+ showPasswordToggle: PropTypes.bool,
326
+ /** HTML autocomplete attribute */
327
+ autoComplete: PropTypes.string,
328
+ /** Error message to display */
329
+ error: PropTypes.string,
330
+ /** Helper text to display */
331
+ helperText: PropTypes.string,
332
+ /** Maximum length */
333
+ maxLength: PropTypes.number,
334
+ /** Minimum length */
335
+ minLength: PropTypes.number,
336
+ /** Input pattern */
337
+ pattern: PropTypes.string,
338
+ /** Step for number inputs */
339
+ step: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
340
+ /** Minimum value for number inputs */
341
+ min: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
342
+ /** Maximum value for number inputs */
343
+ max: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
344
+ /** Number of rows for textarea */
345
+ rows: PropTypes.number,
346
+ /** Validation function */
347
+ validation: PropTypes.func,
348
+ /** Debounce delay in milliseconds */
349
+ debounceMs: PropTypes.number,
350
+ /** ARIA label */
351
+ ariaLabel: PropTypes.string,
352
+ /** ARIA described by */
353
+ ariaDescribedBy: PropTypes.string,
354
+ /** Change handler */
355
+ onChange: PropTypes.func,
356
+ /** Enter key handler */
357
+ onEnter: PropTypes.func,
358
+ /** Click handler */
359
+ onClick: PropTypes.func,
360
+ /** Focus handler */
361
+ onFocus: PropTypes.func,
362
+ /** Blur handler */
363
+ onBlur: PropTypes.func,
364
+ /** Validation handler */
365
+ onValidation: PropTypes.func
366
+ }
367
+
368
+ TextField2.defaultProps = {
369
+ type: 'text',
370
+ labelPosition: 'top',
371
+ outlined: false,
372
+ readOnly: false,
373
+ disabled: false,
374
+ required: false,
375
+ canClear: true,
376
+ showPasswordToggle: true,
377
+ autoComplete: 'off',
378
+ rows: 3,
379
+ debounceMs: 0,
380
+ className: ''
381
+ }
382
+
383
+ // PropTypes for TextArea2
384
+ TextArea2.propTypes = {
385
+ ...TextField2.propTypes
386
+ }
387
+
388
+ TextArea2.defaultProps = {
389
+ ...TextField2.defaultProps,
390
+ type: 'textarea'
391
+ }
392
+
393
+ // PropTypes for PasswordField2
394
+ PasswordField2.propTypes = {
395
+ ...TextField2.propTypes
396
+ }
397
+
398
+ PasswordField2.defaultProps = {
399
+ ...TextField2.defaultProps,
400
+ type: 'password'
401
+ }
402
+
403
+ /**
404
+ * Enhanced DropDown component with improved accessibility and functionality
405
+ */
406
+ export const DropDown2 = (props) => {
407
+ const {
408
+ id,
409
+ options = [],
410
+ value,
411
+ placeholder,
412
+ label,
413
+ outlined = false,
414
+ disabled = false,
415
+ readOnly = false,
416
+ required = false,
417
+ searchable = false,
418
+ clearable = false,
419
+ multiple = false,
420
+ groupBy,
421
+ filterFunction,
422
+ renderOption,
423
+ renderValue,
424
+ position = 'bottom',
425
+ maxHeight = '200px',
426
+ error,
427
+ helperText,
428
+ className,
429
+ ariaLabel,
430
+ onChange,
431
+ onOpen,
432
+ onClose,
433
+ onSearch,
434
+ ...restProps
435
+ } = props
436
+
437
+ const site = useContext(SiteContext)
438
+ const [isOpen, setIsOpen] = useState(false)
439
+ const [searchTerm, setSearchTerm] = useState('')
440
+ const [focusedIndex, setFocusedIndex] = useState(-1)
441
+ const [internalError, setInternalError] = useState('')
442
+ const dropdownRef = useRef(null)
443
+ const inputRef = useRef(null)
444
+ const listRef = useRef(null)
445
+
446
+ // Validate required props
447
+ if (!id) {
448
+ console.warn('DropDown2 component: id prop is required')
449
+ }
450
+
451
+ if (!Array.isArray(options)) {
452
+ console.warn('DropDown2 component: options must be an array')
453
+ }
454
+
455
+ // Validate required field
456
+ useEffect(() => {
457
+ if (required && (!value || (Array.isArray(value) && value.length === 0))) {
458
+ setInternalError('This field is required')
459
+ } else {
460
+ setInternalError('')
461
+ }
462
+ }, [value, required])
463
+
464
+ // Get display value
465
+ const getDisplayValue = useCallback(() => {
466
+ if (!value) return ''
467
+
468
+ if (multiple && Array.isArray(value)) {
469
+ if (value.length === 0) return ''
470
+ if (value.length === 1) {
471
+ const option = options.find(opt => opt.value === value[0])
472
+ return option ? option.label : value[0]
473
+ }
474
+ return `${value.length} items selected`
475
+ }
476
+
477
+ const option = options.find(opt => opt.value === value)
478
+ if (renderValue && option) {
479
+ return renderValue(option)
480
+ }
481
+ return option ? option.label : value
482
+ }, [value, options, multiple, renderValue])
483
+
484
+ // Filter options based on search term
485
+ const filteredOptions = useMemo(() => {
486
+ if (!searchTerm || !searchable) return options
487
+
488
+ if (filterFunction) {
489
+ return options.filter(option => filterFunction(option, searchTerm))
490
+ }
491
+
492
+ return options.filter(option =>
493
+ option.label.toLowerCase().includes(searchTerm.toLowerCase())
494
+ )
495
+ }, [options, searchTerm, searchable, filterFunction])
496
+
497
+ // Group options if groupBy is provided
498
+ const groupedOptions = useMemo(() => {
499
+ if (!groupBy) return [{ options: filteredOptions }]
500
+
501
+ const groups = filteredOptions.reduce((acc, option) => {
502
+ const groupKey = typeof groupBy === 'function' ? groupBy(option) : option[groupBy]
503
+ if (!acc[groupKey]) {
504
+ acc[groupKey] = []
505
+ }
506
+ acc[groupKey].push(option)
507
+ return acc
508
+ }, {})
509
+
510
+ return Object.entries(groups).map(([label, options]) => ({ label, options }))
511
+ }, [filteredOptions, groupBy])
512
+
513
+ // Handle option selection
514
+ const handleSelect = useCallback((selectedValue, option) => {
515
+ if (disabled || readOnly) return
516
+
517
+ let newValue
518
+ if (multiple) {
519
+ const currentValues = Array.isArray(value) ? value : []
520
+ if (currentValues.includes(selectedValue)) {
521
+ newValue = currentValues.filter(v => v !== selectedValue)
522
+ } else {
523
+ newValue = [...currentValues, selectedValue]
524
+ }
525
+ } else {
526
+ newValue = selectedValue
527
+ setIsOpen(false)
528
+ }
529
+
530
+ if (onChange) {
531
+ onChange(id, newValue, option)
532
+ }
533
+
534
+ if (!multiple) {
535
+ setSearchTerm('')
536
+ setFocusedIndex(-1)
537
+ }
538
+ }, [disabled, readOnly, multiple, value, id, onChange])
539
+
540
+ // Handle dropdown open/close
541
+ const handleToggle = useCallback(() => {
542
+ if (disabled || readOnly) return
543
+
544
+ const newOpen = !isOpen
545
+ setIsOpen(newOpen)
546
+
547
+ if (newOpen) {
548
+ setFocusedIndex(-1)
549
+ if (onOpen) onOpen()
550
+ } else {
551
+ setSearchTerm('')
552
+ setFocusedIndex(-1)
553
+ if (onClose) onClose()
554
+ }
555
+ }, [disabled, readOnly, isOpen, onOpen, onClose])
556
+
557
+ // Handle search input
558
+ const handleSearch = useCallback((event) => {
559
+ if (!searchable) return
560
+
561
+ const term = event.target.value
562
+ setSearchTerm(term)
563
+ setFocusedIndex(-1)
564
+
565
+ if (!isOpen) {
566
+ setIsOpen(true)
567
+ }
568
+
569
+ if (onSearch) {
570
+ onSearch(term)
571
+ }
572
+ }, [searchable, isOpen, onSearch])
573
+
574
+ // Handle clear
575
+ const handleClear = useCallback((event) => {
576
+ event.stopPropagation()
577
+ if (disabled || readOnly) return
578
+
579
+ const newValue = multiple ? [] : ''
580
+ if (onChange) {
581
+ onChange(id, newValue)
582
+ }
583
+ setSearchTerm('')
584
+ }, [disabled, readOnly, multiple, id, onChange])
585
+
586
+ // Keyboard navigation
587
+ const handleKeyDown = useCallback((event) => {
588
+ if (disabled) return
589
+
590
+ const flatOptions = groupedOptions.flatMap(group => group.options)
591
+
592
+ switch (event.key) {
593
+ case 'ArrowDown':
594
+ event.preventDefault()
595
+ if (!isOpen) {
596
+ setIsOpen(true)
597
+ } else {
598
+ setFocusedIndex(prev =>
599
+ prev < flatOptions.length - 1 ? prev + 1 : 0
600
+ )
601
+ }
602
+ break
603
+
604
+ case 'ArrowUp':
605
+ event.preventDefault()
606
+ if (isOpen) {
607
+ setFocusedIndex(prev =>
608
+ prev > 0 ? prev - 1 : flatOptions.length - 1
609
+ )
610
+ }
611
+ break
612
+
613
+ case 'Enter':
614
+ event.preventDefault()
615
+ if (isOpen && focusedIndex >= 0) {
616
+ const option = flatOptions[focusedIndex]
617
+ if (option) {
618
+ handleSelect(option.value, option)
619
+ }
620
+ } else {
621
+ handleToggle()
622
+ }
623
+ break
624
+
625
+ case 'Escape':
626
+ if (isOpen) {
627
+ event.preventDefault()
628
+ setIsOpen(false)
629
+ setSearchTerm('')
630
+ setFocusedIndex(-1)
631
+ }
632
+ break
633
+
634
+ case 'Tab':
635
+ if (isOpen) {
636
+ setIsOpen(false)
637
+ setSearchTerm('')
638
+ setFocusedIndex(-1)
639
+ }
640
+ break
641
+ }
642
+ }, [disabled, isOpen, focusedIndex, groupedOptions, handleSelect, handleToggle])
643
+
644
+ // Click outside to close
645
+ useEffect(() => {
646
+ const handleClickOutside = (event) => {
647
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
648
+ setIsOpen(false)
649
+ setSearchTerm('')
650
+ setFocusedIndex(-1)
651
+ }
652
+ }
653
+
654
+ if (isOpen) {
655
+ document.addEventListener('mousedown', handleClickOutside)
656
+ return () => document.removeEventListener('mousedown', handleClickOutside)
657
+ }
658
+ }, [isOpen])
659
+
660
+ // Generate CSS classes
661
+ const safeClassName = className || ''
662
+ const cssClasses = [
663
+ 'dropdown2',
664
+ outlined && 'outlined',
665
+ disabled && 'disabled',
666
+ readOnly && 'readonly',
667
+ isOpen && 'open',
668
+ error || internalError ? 'error' : '',
669
+ multiple && 'multiple',
670
+ safeClassName
671
+ ].filter(Boolean).join(' ')
672
+
673
+ // Accessibility attributes
674
+ const ariaAttributes = {
675
+ 'aria-label': ariaLabel || label,
676
+ 'aria-expanded': isOpen,
677
+ 'aria-haspopup': 'listbox',
678
+ 'aria-disabled': disabled,
679
+ 'aria-readonly': readOnly,
680
+ 'aria-required': required,
681
+ 'aria-invalid': !!(error || internalError),
682
+ 'aria-describedby': error || internalError || helperText ? `${id}-helper` : undefined
683
+ }
684
+
685
+ const displayValue = getDisplayValue()
686
+ const displayError = error || internalError
687
+ const displayHelperText = helperText && !displayError
688
+ const showClear = clearable && displayValue && !disabled && !readOnly
689
+
690
+ return (
691
+ <div ref={dropdownRef} className={cssClasses} {...restProps}>
692
+ <div
693
+ className="dropdown2-control"
694
+ onClick={handleToggle}
695
+ onKeyDown={handleKeyDown}
696
+ tabIndex={disabled ? -1 : 0}
697
+ {...ariaAttributes}
698
+ >
699
+ {searchable && isOpen ? (
700
+ <input
701
+ ref={inputRef}
702
+ type="text"
703
+ value={searchTerm}
704
+ placeholder={placeholder || 'Search...'}
705
+ onChange={handleSearch}
706
+ onKeyDown={handleKeyDown}
707
+ className="dropdown2-search"
708
+ disabled={disabled}
709
+ autoFocus
710
+ />
711
+ ) : (
712
+ <span className={`dropdown2-value ${!displayValue ? 'placeholder' : ''}`}>
713
+ {displayValue || placeholder || 'Select...'}
714
+ </span>
715
+ )}
716
+
717
+ <div className="dropdown2-indicators">
718
+ {showClear && (
719
+ <Icon
720
+ icon="close"
721
+ size="small"
722
+ clickable
723
+ action={handleClear}
724
+ className="dropdown2-clear"
725
+ ariaLabel="Clear selection"
726
+ />
727
+ )}
728
+ <Icon
729
+ icon={isOpen ? 'expand_less' : 'expand_more'}
730
+ size="small"
731
+ className="dropdown2-arrow"
732
+ />
733
+ </div>
734
+ </div>
735
+
736
+ {label && (
737
+ <label htmlFor={id} className={`dropdown2-label ${isOpen || displayValue ? 'active' : ''}`}>
738
+ <Text>{label}</Text>
739
+ </label>
740
+ )}
741
+
742
+ {isOpen && (
743
+ <div
744
+ className={`dropdown2-menu ${position}`}
745
+ style={{ maxHeight }}
746
+ role="listbox"
747
+ aria-multiselectable={multiple}
748
+ >
749
+ <ul ref={listRef}>
750
+ {groupedOptions.map((group, groupIndex) => (
751
+ <React.Fragment key={groupIndex}>
752
+ {group.label && (
753
+ <li className="dropdown2-group-label" role="group">
754
+ <Text>{group.label}</Text>
755
+ </li>
756
+ )}
757
+ {group.options.map((option, optionIndex) => {
758
+ const flatIndex = groupedOptions
759
+ .slice(0, groupIndex)
760
+ .reduce((acc, g) => acc + g.options.length, 0) + optionIndex
761
+
762
+ const isSelected = multiple
763
+ ? Array.isArray(value) && value.includes(option.value)
764
+ : value === option.value
765
+
766
+ const isFocused = flatIndex === focusedIndex
767
+
768
+ return (
769
+ <li
770
+ key={option.value}
771
+ className={`dropdown2-option ${isSelected ? 'selected' : ''} ${isFocused ? 'focused' : ''}`}
772
+ onClick={() => handleSelect(option.value, option)}
773
+ role="option"
774
+ aria-selected={isSelected}
775
+ >
776
+ {multiple && (
777
+ <Icon
778
+ icon={isSelected ? 'check_box' : 'check_box_outline_blank'}
779
+ size="small"
780
+ className="dropdown2-checkbox"
781
+ />
782
+ )}
783
+ {option.icon && (
784
+ <Icon
785
+ icon={option.icon}
786
+ size="small"
787
+ className="dropdown2-option-icon"
788
+ />
789
+ )}
790
+ <span className="dropdown2-option-text">
791
+ {renderOption ? renderOption(option) : <Text>{option.label}</Text>}
792
+ </span>
793
+ {isSelected && !multiple && (
794
+ <Icon
795
+ icon="check"
796
+ size="small"
797
+ className="dropdown2-check"
798
+ />
799
+ )}
800
+ </li>
801
+ )
802
+ })}
803
+ </React.Fragment>
804
+ ))}
805
+ {filteredOptions.length === 0 && (
806
+ <li className="dropdown2-no-options">
807
+ <Text>No options found</Text>
808
+ </li>
809
+ )}
810
+ </ul>
811
+ </div>
812
+ )}
813
+
814
+ {/* Error/Helper text */}
815
+ {(displayError || displayHelperText) && (
816
+ <div
817
+ id={`${id}-helper`}
818
+ className={`dropdown2-helper ${displayError ? 'error' : 'helper'}`}
819
+ role={displayError ? 'alert' : 'status'}
820
+ aria-live={displayError ? 'assertive' : 'polite'}
821
+ >
822
+ {displayError && <Icon icon="error" size="small" />}
823
+ <Text>{displayError || helperText}</Text>
824
+ </div>
825
+ )}
826
+ </div>
827
+ )
828
+ }
829
+
830
+ // PropTypes for DropDown2
831
+ DropDown2.propTypes = {
832
+ /** Unique identifier for the dropdown */
833
+ id: PropTypes.string.isRequired,
834
+ /** Array of option objects */
835
+ options: PropTypes.arrayOf(PropTypes.shape({
836
+ value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
837
+ label: PropTypes.string.isRequired,
838
+ icon: PropTypes.string,
839
+ disabled: PropTypes.bool
840
+ })).isRequired,
841
+ /** Selected value(s) */
842
+ value: PropTypes.oneOfType([
843
+ PropTypes.string,
844
+ PropTypes.number,
845
+ PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number]))
846
+ ]),
847
+ /** Placeholder text */
848
+ placeholder: PropTypes.string,
849
+ /** Field label */
850
+ label: PropTypes.string,
851
+ /** Whether dropdown has outlined style */
852
+ outlined: PropTypes.bool,
853
+ /** Whether dropdown is disabled */
854
+ disabled: PropTypes.bool,
855
+ /** Whether dropdown is read-only */
856
+ readOnly: PropTypes.bool,
857
+ /** Whether dropdown is required */
858
+ required: PropTypes.bool,
859
+ /** Whether dropdown is searchable */
860
+ searchable: PropTypes.bool,
861
+ /** Whether dropdown is clearable */
862
+ clearable: PropTypes.bool,
863
+ /** Whether multiple selection is allowed */
864
+ multiple: PropTypes.bool,
865
+ /** Function or property name to group options by */
866
+ groupBy: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
867
+ /** Custom filter function */
868
+ filterFunction: PropTypes.func,
869
+ /** Custom option renderer */
870
+ renderOption: PropTypes.func,
871
+ /** Custom value renderer */
872
+ renderValue: PropTypes.func,
873
+ /** Dropdown position */
874
+ position: PropTypes.oneOf(['top', 'bottom']),
875
+ /** Maximum height of dropdown menu */
876
+ maxHeight: PropTypes.string,
877
+ /** Error message */
878
+ error: PropTypes.string,
879
+ /** Helper text */
880
+ helperText: PropTypes.string,
881
+ /** Additional CSS classes */
882
+ className: PropTypes.string,
883
+ /** ARIA label */
884
+ ariaLabel: PropTypes.string,
885
+ /** Change handler */
886
+ onChange: PropTypes.func,
887
+ /** Open handler */
888
+ onOpen: PropTypes.func,
889
+ /** Close handler */
890
+ onClose: PropTypes.func,
891
+ /** Search handler */
892
+ onSearch: PropTypes.func
893
+ }
894
+
895
+ DropDown2.defaultProps = {
896
+ options: [],
897
+ outlined: false,
898
+ disabled: false,
899
+ readOnly: false,
900
+ required: false,
901
+ searchable: false,
902
+ clearable: false,
903
+ multiple: false,
904
+ position: 'bottom',
905
+ maxHeight: '200px',
906
+ className: ''
907
+ }
908
+
909
+ /**
910
+ * Enhanced DateRange component with improved validation and accessibility
911
+ */
912
+ export const DateRange2 = (props) => {
913
+ const {
914
+ id,
915
+ label,
916
+ value,
917
+ outlined = false,
918
+ disabled = false,
919
+ readOnly = false,
920
+ required = false,
921
+ minDate,
922
+ maxDate,
923
+ error,
924
+ helperText,
925
+ className,
926
+ ariaLabel,
927
+ onChange,
928
+ onValidation,
929
+ ...restProps
930
+ } = props
931
+
932
+ const [form, setForm] = useState({ from: '', to: '' })
933
+ const [internalError, setInternalError] = useState('')
934
+ const [isValid, setIsValid] = useState(true)
935
+
936
+ // Validate required props
937
+ if (!id) {
938
+ console.warn('DateRange2 component: id prop is required')
939
+ }
940
+
941
+ // Initialize form from value
942
+ useEffect(() => {
943
+ if (value && typeof value === 'object') {
944
+ setForm({
945
+ from: value.from || '',
946
+ to: value.to || ''
947
+ })
948
+ }
949
+ }, [value])
950
+
951
+ // Validate date range
952
+ useEffect(() => {
953
+ let valid = true
954
+ let errorMessage = ''
955
+
956
+ if (required && (!form.from || !form.to)) {
957
+ valid = false
958
+ errorMessage = 'Both dates are required'
959
+ } else if (form.from && form.to) {
960
+ const fromDate = new Date(form.from)
961
+ const toDate = new Date(form.to)
962
+
963
+ if (fromDate > toDate) {
964
+ valid = false
965
+ errorMessage = 'From date must be before To date'
966
+ } else if (minDate && fromDate < new Date(minDate)) {
967
+ valid = false
968
+ errorMessage = `From date must be after ${minDate}`
969
+ } else if (maxDate && toDate > new Date(maxDate)) {
970
+ valid = false
971
+ errorMessage = `To date must be before ${maxDate}`
972
+ }
973
+ }
974
+
975
+ setIsValid(valid)
976
+ setInternalError(valid ? '' : errorMessage)
977
+
978
+ if (onValidation) {
979
+ onValidation(id, valid, errorMessage)
980
+ }
981
+ }, [form, required, minDate, maxDate, id, onValidation])
982
+
983
+ // Handle form changes
984
+ useEffect(() => {
985
+ if (form.from && form.to && onChange) {
986
+ onChange(id, form)
987
+ }
988
+ }, [form, id, onChange])
989
+
990
+ // Handle field changes
991
+ const handleChange = useCallback((fieldId, fieldValue) => {
992
+ if (disabled || readOnly) return
993
+
994
+ setForm(prevForm => ({
995
+ ...prevForm,
996
+ [fieldId]: fieldValue
997
+ }))
998
+ }, [disabled, readOnly])
999
+
1000
+ // Generate CSS classes
1001
+ const safeClassName = className || ''
1002
+ const cssClasses = [
1003
+ 'date-range2',
1004
+ outlined && 'outlined',
1005
+ disabled && 'disabled',
1006
+ readOnly && 'readonly',
1007
+ error || internalError ? 'error' : '',
1008
+ !isValid && 'invalid',
1009
+ safeClassName
1010
+ ].filter(Boolean).join(' ')
1011
+
1012
+ // Accessibility attributes
1013
+ const ariaAttributes = {
1014
+ 'aria-label': ariaLabel || label,
1015
+ 'aria-invalid': !isValid || !!(error || internalError),
1016
+ 'aria-required': required,
1017
+ 'aria-disabled': disabled,
1018
+ 'aria-readonly': readOnly,
1019
+ 'aria-describedby': error || internalError || helperText ? `${id}-helper` : undefined
1020
+ }
1021
+
1022
+ const displayError = error || internalError
1023
+ const displayHelperText = helperText && !displayError
1024
+
1025
+ return (
1026
+ <div className={cssClasses} {...ariaAttributes} {...restProps}>
1027
+ {label && (
1028
+ <label className="date-range2-label">
1029
+ <Text>{label}</Text>
1030
+ </label>
1031
+ )}
1032
+
1033
+ <div className="date-range2-fields">
1034
+ <TextField2
1035
+ id={`${id}-from`}
1036
+ type="date"
1037
+ label="From"
1038
+ value={form.from}
1039
+ outlined={outlined}
1040
+ disabled={disabled}
1041
+ readOnly={readOnly}
1042
+ required={required}
1043
+ min={minDate}
1044
+ max={form.to || maxDate}
1045
+ onChange={handleChange}
1046
+ className="date-range2-from"
1047
+ />
1048
+
1049
+ <span className="date-range2-separator">
1050
+ <Text>to</Text>
1051
+ </span>
1052
+
1053
+ <TextField2
1054
+ id={`${id}-to`}
1055
+ type="date"
1056
+ label="To"
1057
+ value={form.to}
1058
+ outlined={outlined}
1059
+ disabled={disabled}
1060
+ readOnly={readOnly}
1061
+ required={required}
1062
+ min={form.from || minDate}
1063
+ max={maxDate}
1064
+ onChange={handleChange}
1065
+ className="date-range2-to"
1066
+ />
1067
+ </div>
1068
+
1069
+ {/* Error/Helper text */}
1070
+ {(displayError || displayHelperText) && (
1071
+ <div
1072
+ id={`${id}-helper`}
1073
+ className={`date-range2-helper ${displayError ? 'error' : 'helper'}`}
1074
+ role={displayError ? 'alert' : 'status'}
1075
+ aria-live={displayError ? 'assertive' : 'polite'}
1076
+ >
1077
+ {displayError && <Icon icon="error" size="small" />}
1078
+ <Text>{displayError || helperText}</Text>
1079
+ </div>
1080
+ )}
1081
+ </div>
1082
+ )
1083
+ }
1084
+
1085
+ // PropTypes for DateRange2
1086
+ DateRange2.propTypes = {
1087
+ /** Unique identifier for the date range */
1088
+ id: PropTypes.string.isRequired,
1089
+ /** Field label */
1090
+ label: PropTypes.string,
1091
+ /** Date range value object with from and to properties */
1092
+ value: PropTypes.shape({
1093
+ from: PropTypes.string,
1094
+ to: PropTypes.string
1095
+ }),
1096
+ /** Whether fields have outlined style */
1097
+ outlined: PropTypes.bool,
1098
+ /** Whether fields are disabled */
1099
+ disabled: PropTypes.bool,
1100
+ /** Whether fields are read-only */
1101
+ readOnly: PropTypes.bool,
1102
+ /** Whether date range is required */
1103
+ required: PropTypes.bool,
1104
+ /** Minimum allowed date */
1105
+ minDate: PropTypes.string,
1106
+ /** Maximum allowed date */
1107
+ maxDate: PropTypes.string,
1108
+ /** Error message */
1109
+ error: PropTypes.string,
1110
+ /** Helper text */
1111
+ helperText: PropTypes.string,
1112
+ /** Additional CSS classes */
1113
+ className: PropTypes.string,
1114
+ /** ARIA label */
1115
+ ariaLabel: PropTypes.string,
1116
+ /** Change handler */
1117
+ onChange: PropTypes.func,
1118
+ /** Validation handler */
1119
+ onValidation: PropTypes.func
1120
+ }
1121
+
1122
+ DateRange2.defaultProps = {
1123
+ outlined: false,
1124
+ disabled: false,
1125
+ readOnly: false,
1126
+ required: false,
1127
+ className: ''
1128
+ }
1129
+
1130
+ export default TextField2