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