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.
- 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 +7900 -1615
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +6094 -1122
- package/dist/index.css.map +1 -1
- package/dist/index.modern.js +7929 -1645
- package/dist/index.modern.js.map +1 -1
- package/dist/index.umd.js +7900 -1615
- 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 +1 -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 +288 -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 +842 -0
- package/src/html/textfield2.example.js +499 -0
- package/src/html/textfield2.js +1130 -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 +228 -0
- package/src/html/tree.example.js +475 -0
- package/src/html/tree.js +712 -28
- package/src/html/tree_enhanced.test.js +495 -0
- package/table2.test.js +454 -0
- 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
|