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
package/src/html/form.js CHANGED
@@ -1,67 +1,203 @@
1
- import React, { Fragment, useState, useEffect } from 'react'
2
- import { Text } from './text';
1
+ import React, { Fragment, useState, useEffect, useCallback, useRef } from 'react'
2
+ import PropTypes from 'prop-types'
3
+ import { Text } from './text'
3
4
  import './form.css'
4
5
 
5
6
  /**
6
- * Form
7
+ * Form component with enhanced validation and accessibility
7
8
  */
8
- export const Form = ({ title, columns = 1, children, outlined, onChange }) => {
9
+ export const Form = (props) => {
10
+ const {
11
+ id,
12
+ title,
13
+ columns = 1,
14
+ children,
15
+ outlined = false,
16
+ disabled = false,
17
+ loading = false,
18
+ autoComplete = 'on',
19
+ noValidate = false,
20
+ className,
21
+ onChange,
22
+ onSubmit,
23
+ onReset,
24
+ onValidationChange,
25
+ ...restProps
26
+ } = props
9
27
 
10
28
  const [fields, setFields] = useState([])
11
- const isEmpty = (value) => value === void 0 || value === null || value === '';
29
+ const [isSubmitting, setIsSubmitting] = useState(false)
30
+ const formRef = useRef(null)
12
31
 
32
+ // Validate required props
33
+ if (!children) {
34
+ console.warn('Form component: children prop is required')
35
+ }
36
+
37
+ const isEmpty = (value) => value === void 0 || value === null || value === ''
38
+
39
+ // Initialize fields from children
13
40
  useEffect(() => {
14
41
  const initFields = React.Children
15
42
  .toArray(children)
16
43
  .filter(child => child !== null && child !== '')
17
44
  .map(child => {
45
+ if (!child.props) return null
18
46
  const { id, value, required = false, validation } = child.props
19
- const valid = required ? validation ? validation(value) : !isEmpty(value) : true
47
+ if (!id) return null
48
+
49
+ const valid = required ?
50
+ validation ? validation(value) : !isEmpty(value) :
51
+ true
20
52
  return { id, value, required, validation, valid }
21
53
  })
22
- .filter(field => field.id !== void 0)
54
+ .filter(field => field !== null)
23
55
  setFields(initFields)
24
- }, [])
56
+ }, [children])
25
57
 
58
+ // Notify parent of form changes and validation state
26
59
  useEffect(() => {
27
- if (onChange) {
60
+ if (onChange || onValidationChange) {
28
61
  const valid = fields.every(({ valid }) => valid === true)
29
- const form = fields.reduce((form, { id, value }) => {
30
- if (value || value === false) form[id] = value
31
- return form
62
+ const form = fields.reduce((formData, { id, value }) => {
63
+ if (value !== undefined && value !== null) {
64
+ formData[id] = value
65
+ }
66
+ return formData
32
67
  }, {})
33
- onChange(form, valid)
68
+
69
+ if (onChange) onChange(form, valid)
70
+ if (onValidationChange) onValidationChange(valid, fields)
34
71
  }
35
- }, [fields])
36
-
37
- const changeField = (id, value) => {
38
- const field = fields.find(f => f.id === id)
39
- if (field) {
40
- const valid = field.required ? field.validation ? field.validation(value) : !isEmpty(value) : true
41
- Object.assign(field, { value, valid })
42
- setFields(fields.slice())
72
+ }, [fields, onChange, onValidationChange])
73
+
74
+ // Handle field value changes
75
+ const changeField = useCallback((id, value) => {
76
+ if (disabled) return
77
+
78
+ setFields(prevFields => {
79
+ return prevFields.map(field => {
80
+ if (field.id === id) {
81
+ const valid = field.required ?
82
+ field.validation ? field.validation(value) : !isEmpty(value) :
83
+ true
84
+ return { ...field, value, valid }
85
+ }
86
+ return field
87
+ })
88
+ })
89
+ }, [disabled])
90
+
91
+ // Handle form submission
92
+ const handleSubmit = useCallback(async (event) => {
93
+ event.preventDefault()
94
+ event.stopPropagation()
95
+
96
+ if (disabled || loading || isSubmitting) return
97
+
98
+ const valid = fields.every(({ valid }) => valid === true)
99
+ if (!valid && !noValidate) return
100
+
101
+ const formData = fields.reduce((data, { id, value }) => {
102
+ if (value !== undefined && value !== null) {
103
+ data[id] = value
104
+ }
105
+ return data
106
+ }, {})
107
+
108
+ if (onSubmit) {
109
+ setIsSubmitting(true)
110
+ try {
111
+ await onSubmit(formData, event)
112
+ } catch (error) {
113
+ console.error('Form submission error:', error)
114
+ } finally {
115
+ setIsSubmitting(false)
116
+ }
43
117
  }
44
- }
118
+ }, [disabled, loading, isSubmitting, fields, noValidate, onSubmit])
119
+
120
+ // Handle form reset
121
+ const handleReset = useCallback((event) => {
122
+ if (disabled) return
123
+
124
+ setFields(prevFields => {
125
+ return prevFields.map(field => ({
126
+ ...field,
127
+ value: '',
128
+ valid: !field.required
129
+ }))
130
+ })
45
131
 
132
+ if (onReset) onReset(event)
133
+ }, [disabled, onReset])
134
+
135
+ // Render form fields with enhanced props
46
136
  const items = React.Children
47
137
  .toArray(children)
48
138
  .filter(child => child !== null && child !== '')
49
- .map(child => {
50
- const { span = 1 } = child.props
51
- const field = React.cloneElement(child, {
139
+ .map((child, index) => {
140
+ if (!child.props) return null
141
+
142
+ const { span = 1, id } = child.props
143
+ const field = fields.find(f => f.id === id)
144
+
145
+ const enhancedChild = React.cloneElement(child, {
52
146
  onChange: changeField,
53
- outlined: !!outlined
147
+ outlined: outlined,
148
+ disabled: disabled || child.props.disabled,
149
+ error: field && !field.valid ? (field.required ? 'This field is required' : false) : child.props.error,
150
+ key: id || index
54
151
  })
55
- const columnLayout = { gridColumn: `span ${span}`}
56
- return (<FieldWrapper style={columnLayout}>{field}</FieldWrapper>)
152
+
153
+ const columnLayout = { gridColumn: `span ${span}` }
154
+ return (
155
+ <FieldWrapper key={id || index} style={columnLayout}>
156
+ {enhancedChild}
157
+ </FieldWrapper>
158
+ )
57
159
  })
160
+ .filter(item => item !== null)
58
161
 
162
+ // Generate CSS classes
163
+ const safeClassName = className || ''
164
+ const cssClasses = [
165
+ 'form-grid',
166
+ disabled && 'disabled',
167
+ loading && 'loading',
168
+ safeClassName
169
+ ].filter(Boolean).join(' ')
170
+
171
+ const gridLayout = {
172
+ gridTemplateColumns: `repeat(${columns}, 1fr)`,
173
+ gridAutoRows: 'auto' // Changed from fixed height
174
+ }
175
+
176
+ // Accessibility attributes
177
+ const ariaAttributes = {
178
+ 'aria-busy': loading || isSubmitting,
179
+ 'aria-disabled': disabled,
180
+ noValidate: noValidate,
181
+ autoComplete: autoComplete
182
+ }
59
183
 
60
- const gridLayout = { gridTemplateColumns: `repeat(${columns}, 1fr)` }
61
184
  return (
62
185
  <Fragment>
63
- {title ? (<header className="form-header"><Text use="headline6">{title}</Text></header>) : ''}
64
- <form className="form-grid" style={gridLayout}>
186
+ {title && (
187
+ <header className="form-header">
188
+ <Text use="headline6">{title}</Text>
189
+ </header>
190
+ )}
191
+ <form
192
+ ref={formRef}
193
+ id={id}
194
+ className={cssClasses}
195
+ style={gridLayout}
196
+ onSubmit={handleSubmit}
197
+ onReset={handleReset}
198
+ {...ariaAttributes}
199
+ {...restProps}
200
+ >
65
201
  {items}
66
202
  </form>
67
203
  </Fragment>
@@ -69,12 +205,74 @@ export const Form = ({ title, columns = 1, children, outlined, onChange }) => {
69
205
  }
70
206
 
71
207
  /**
72
- * Form Field Wrapper
208
+ * FieldWrapper component for individual form fields
73
209
  */
74
- const FieldWrapper = ({ style, children }) => {
210
+ const FieldWrapper = ({ style, children, className }) => {
211
+ const safeClassName = className || ''
212
+ const cssClasses = [
213
+ 'field-wrapper',
214
+ safeClassName
215
+ ].filter(Boolean).join(' ')
216
+
75
217
  return (
76
- <div className="field-wrapper" style={style}>
218
+ <div className={cssClasses} style={style}>
77
219
  {children}
78
220
  </div>
79
221
  )
222
+ }
223
+
224
+ // PropTypes para Form
225
+ Form.propTypes = {
226
+ /** Unique identifier for the form */
227
+ id: PropTypes.string,
228
+ /** Form title displayed in header */
229
+ title: PropTypes.string,
230
+ /** Number of grid columns */
231
+ columns: PropTypes.number,
232
+ /** Form field components */
233
+ children: PropTypes.node,
234
+ /** Whether fields should have outlined style */
235
+ outlined: PropTypes.bool,
236
+ /** Whether the form is disabled */
237
+ disabled: PropTypes.bool,
238
+ /** Whether the form is in loading state */
239
+ loading: PropTypes.bool,
240
+ /** HTML autocomplete attribute */
241
+ autoComplete: PropTypes.oneOf(['on', 'off']),
242
+ /** Whether to disable HTML5 validation */
243
+ noValidate: PropTypes.bool,
244
+ /** Additional CSS classes */
245
+ className: PropTypes.string,
246
+ /** Form data change handler */
247
+ onChange: PropTypes.func,
248
+ /** Form submission handler */
249
+ onSubmit: PropTypes.func,
250
+ /** Form reset handler */
251
+ onReset: PropTypes.func,
252
+ /** Validation state change handler */
253
+ onValidationChange: PropTypes.func
254
+ }
255
+
256
+ Form.defaultProps = {
257
+ columns: 1,
258
+ outlined: false,
259
+ disabled: false,
260
+ loading: false,
261
+ autoComplete: 'on',
262
+ noValidate: false,
263
+ className: ''
264
+ }
265
+
266
+ // PropTypes para FieldWrapper
267
+ FieldWrapper.propTypes = {
268
+ /** Inline styles for the wrapper */
269
+ style: PropTypes.object,
270
+ /** Child form field components */
271
+ children: PropTypes.node,
272
+ /** Additional CSS classes */
273
+ className: PropTypes.string
274
+ }
275
+
276
+ FieldWrapper.defaultProps = {
277
+ className: ''
80
278
  }
@@ -0,0 +1,369 @@
1
+ import React from 'react'
2
+ import { Form } from './form'
3
+
4
+ // Pruebas unitarias para el componente Form
5
+ describe('Form Component', () => {
6
+ // Mock de los componentes dependientes
7
+ const mockText = jest.fn()
8
+
9
+ beforeEach(() => {
10
+ jest.clearAllMocks()
11
+
12
+ // Mock del componente Text
13
+ jest.doMock('./text', () => ({
14
+ Text: mockText
15
+ }))
16
+
17
+ // Mock de console.warn para las pruebas
18
+ jest.spyOn(console, 'warn').mockImplementation(() => {})
19
+ jest.spyOn(console, 'error').mockImplementation(() => {})
20
+ })
21
+
22
+ afterEach(() => {
23
+ console.warn.mockRestore()
24
+ console.error.mockRestore()
25
+ })
26
+
27
+ test('component exports correctly', () => {
28
+ expect(Form).toBeDefined()
29
+ expect(typeof Form).toBe('function')
30
+ })
31
+
32
+ test('component has correct PropTypes', () => {
33
+ expect(Form.propTypes).toBeDefined()
34
+ expect(Form.propTypes.id).toBeDefined()
35
+ expect(Form.propTypes.title).toBeDefined()
36
+ expect(Form.propTypes.columns).toBeDefined()
37
+ expect(Form.propTypes.children).toBeDefined()
38
+ expect(Form.propTypes.outlined).toBeDefined()
39
+ expect(Form.propTypes.disabled).toBeDefined()
40
+ expect(Form.propTypes.loading).toBeDefined()
41
+ expect(Form.propTypes.autoComplete).toBeDefined()
42
+ expect(Form.propTypes.noValidate).toBeDefined()
43
+ expect(Form.propTypes.className).toBeDefined()
44
+ expect(Form.propTypes.onChange).toBeDefined()
45
+ expect(Form.propTypes.onSubmit).toBeDefined()
46
+ expect(Form.propTypes.onReset).toBeDefined()
47
+ expect(Form.propTypes.onValidationChange).toBeDefined()
48
+ })
49
+
50
+ test('component has correct defaultProps', () => {
51
+ expect(Form.defaultProps).toBeDefined()
52
+ expect(Form.defaultProps.columns).toBe(1)
53
+ expect(Form.defaultProps.outlined).toBe(false)
54
+ expect(Form.defaultProps.disabled).toBe(false)
55
+ expect(Form.defaultProps.loading).toBe(false)
56
+ expect(Form.defaultProps.autoComplete).toBe('on')
57
+ expect(Form.defaultProps.noValidate).toBe(false)
58
+ expect(Form.defaultProps.className).toBe('')
59
+ })
60
+
61
+ test('warns when children prop is missing', () => {
62
+ const validateChildren = (children) => {
63
+ if (!children) {
64
+ console.warn('Form component: children prop is required')
65
+ }
66
+ }
67
+
68
+ validateChildren(null)
69
+ expect(console.warn).toHaveBeenCalledWith('Form component: children prop is required')
70
+
71
+ console.warn.mockClear()
72
+ validateChildren('some children')
73
+ expect(console.warn).not.toHaveBeenCalled()
74
+ })
75
+
76
+ test('isEmpty function works correctly', () => {
77
+ const isEmpty = (value) => value === void 0 || value === null || value === ''
78
+
79
+ expect(isEmpty(undefined)).toBe(true)
80
+ expect(isEmpty(null)).toBe(true)
81
+ expect(isEmpty('')).toBe(true)
82
+ expect(isEmpty('text')).toBe(false)
83
+ expect(isEmpty(0)).toBe(false)
84
+ expect(isEmpty(false)).toBe(false)
85
+ expect(isEmpty([])).toBe(false)
86
+ expect(isEmpty({})).toBe(false)
87
+ })
88
+
89
+ test('field initialization works correctly', () => {
90
+ const mockChildren = [
91
+ { props: { id: 'field1', value: 'test', required: true } },
92
+ { props: { id: 'field2', value: '', required: false } },
93
+ { props: { id: 'field3', value: 'valid', required: true, validation: (v) => v.length > 3 } },
94
+ { props: {} }, // No id, should be filtered out
95
+ null // Null child, should be filtered out
96
+ ]
97
+
98
+ const initFields = mockChildren
99
+ .filter(child => child !== null && child !== '')
100
+ .map(child => {
101
+ if (!child.props) return null
102
+ const { id, value, required = false, validation } = child.props
103
+ if (!id) return null
104
+
105
+ const isEmpty = (value) => value === void 0 || value === null || value === ''
106
+ const valid = required ?
107
+ validation ? validation(value) : !isEmpty(value) :
108
+ true
109
+ return { id, value, required, validation, valid }
110
+ })
111
+ .filter(field => field !== null)
112
+
113
+ expect(initFields).toHaveLength(3)
114
+ expect(initFields[0]).toEqual({ id: 'field1', value: 'test', required: true, validation: undefined, valid: true })
115
+ expect(initFields[1]).toEqual({ id: 'field2', value: '', required: false, validation: undefined, valid: true })
116
+ expect(initFields[2]).toEqual({ id: 'field3', value: 'valid', required: true, validation: expect.any(Function), valid: true })
117
+ })
118
+
119
+ test('field change logic works correctly', () => {
120
+ const fields = [
121
+ { id: 'field1', value: 'test', required: true, validation: undefined, valid: true },
122
+ { id: 'field2', value: '', required: false, validation: undefined, valid: true }
123
+ ]
124
+
125
+ const changeField = (fields, id, value) => {
126
+ const isEmpty = (value) => value === void 0 || value === null || value === ''
127
+
128
+ return fields.map(field => {
129
+ if (field.id === id) {
130
+ const valid = field.required ?
131
+ field.validation ? field.validation(value) : !isEmpty(value) :
132
+ true
133
+ return { ...field, value, valid }
134
+ }
135
+ return field
136
+ })
137
+ }
138
+
139
+ // Change field1 to empty (should be invalid because required)
140
+ const result1 = changeField(fields, 'field1', '')
141
+ expect(result1[0].value).toBe('')
142
+ expect(result1[0].valid).toBe(false)
143
+
144
+ // Change field2 to empty (should be valid because not required)
145
+ const result2 = changeField(fields, 'field2', '')
146
+ expect(result2[1].value).toBe('')
147
+ expect(result2[1].valid).toBe(true)
148
+
149
+ // Change field1 to valid value
150
+ const result3 = changeField(fields, 'field1', 'new value')
151
+ expect(result3[0].value).toBe('new value')
152
+ expect(result3[0].valid).toBe(true)
153
+ })
154
+
155
+ test('form data generation works correctly', () => {
156
+ const fields = [
157
+ { id: 'field1', value: 'test', required: true, valid: true },
158
+ { id: 'field2', value: '', required: false, valid: true },
159
+ { id: 'field3', value: null, required: false, valid: true },
160
+ { id: 'field4', value: undefined, required: false, valid: true },
161
+ { id: 'field5', value: 0, required: false, valid: true },
162
+ { id: 'field6', value: false, required: false, valid: true }
163
+ ]
164
+
165
+ const formData = fields.reduce((data, { id, value }) => {
166
+ if (value !== undefined && value !== null) {
167
+ data[id] = value
168
+ }
169
+ return data
170
+ }, {})
171
+
172
+ expect(formData).toEqual({
173
+ field1: 'test',
174
+ field2: '',
175
+ field5: 0,
176
+ field6: false
177
+ })
178
+ })
179
+
180
+ test('form validation works correctly', () => {
181
+ const fields1 = [
182
+ { id: 'field1', value: 'test', required: true, valid: true },
183
+ { id: 'field2', value: '', required: false, valid: true }
184
+ ]
185
+
186
+ const fields2 = [
187
+ { id: 'field1', value: 'test', required: true, valid: true },
188
+ { id: 'field2', value: '', required: true, valid: false }
189
+ ]
190
+
191
+ const isFormValid = (fields) => fields.every(({ valid }) => valid === true)
192
+
193
+ expect(isFormValid(fields1)).toBe(true)
194
+ expect(isFormValid(fields2)).toBe(false)
195
+ })
196
+
197
+ test('CSS classes generation works correctly', () => {
198
+ const generateClasses = (disabled, loading, className) => {
199
+ return [
200
+ 'form-grid',
201
+ disabled && 'disabled',
202
+ loading && 'loading',
203
+ className || ''
204
+ ].filter(Boolean).join(' ')
205
+ }
206
+
207
+ expect(generateClasses(false, false, '')).toBe('form-grid')
208
+ expect(generateClasses(true, false, '')).toBe('form-grid disabled')
209
+ expect(generateClasses(false, true, '')).toBe('form-grid loading')
210
+ expect(generateClasses(true, true, '')).toBe('form-grid disabled loading')
211
+ expect(generateClasses(false, false, 'custom')).toBe('form-grid custom')
212
+ })
213
+
214
+ test('grid layout generation works correctly', () => {
215
+ const generateGridLayout = (columns) => {
216
+ return {
217
+ gridTemplateColumns: `repeat(${columns}, 1fr)`,
218
+ gridAutoRows: 'auto'
219
+ }
220
+ }
221
+
222
+ expect(generateGridLayout(1)).toEqual({
223
+ gridTemplateColumns: 'repeat(1, 1fr)',
224
+ gridAutoRows: 'auto'
225
+ })
226
+
227
+ expect(generateGridLayout(3)).toEqual({
228
+ gridTemplateColumns: 'repeat(3, 1fr)',
229
+ gridAutoRows: 'auto'
230
+ })
231
+ })
232
+
233
+ test('accessibility attributes generation works correctly', () => {
234
+ const generateAriaAttributes = (loading, isSubmitting, disabled, noValidate, autoComplete) => {
235
+ return {
236
+ 'aria-busy': loading || isSubmitting,
237
+ 'aria-disabled': disabled,
238
+ noValidate: noValidate,
239
+ autoComplete: autoComplete
240
+ }
241
+ }
242
+
243
+ const basic = generateAriaAttributes(false, false, false, false, 'on')
244
+ expect(basic['aria-busy']).toBe(false)
245
+ expect(basic['aria-disabled']).toBe(false)
246
+ expect(basic.noValidate).toBe(false)
247
+ expect(basic.autoComplete).toBe('on')
248
+
249
+ const loading = generateAriaAttributes(true, false, false, false, 'on')
250
+ expect(loading['aria-busy']).toBe(true)
251
+
252
+ const submitting = generateAriaAttributes(false, true, false, false, 'on')
253
+ expect(submitting['aria-busy']).toBe(true)
254
+
255
+ const disabled = generateAriaAttributes(false, false, true, false, 'on')
256
+ expect(disabled['aria-disabled']).toBe(true)
257
+
258
+ const noValidate = generateAriaAttributes(false, false, false, true, 'off')
259
+ expect(noValidate.noValidate).toBe(true)
260
+ expect(noValidate.autoComplete).toBe('off')
261
+ })
262
+
263
+ test('form submission logic works correctly', async () => {
264
+ const mockOnSubmit = jest.fn().mockResolvedValue()
265
+
266
+ const simulateSubmit = async (disabled, loading, isSubmitting, fields, noValidate, onSubmit) => {
267
+ const event = {
268
+ preventDefault: jest.fn(),
269
+ stopPropagation: jest.fn()
270
+ }
271
+
272
+ event.preventDefault()
273
+ event.stopPropagation()
274
+
275
+ if (disabled || loading || isSubmitting) return false
276
+
277
+ const valid = fields.every(({ valid }) => valid === true)
278
+ if (!valid && !noValidate) return false
279
+
280
+ const formData = fields.reduce((data, { id, value }) => {
281
+ if (value !== undefined && value !== null) {
282
+ data[id] = value
283
+ }
284
+ return data
285
+ }, {})
286
+
287
+ if (onSubmit) {
288
+ await onSubmit(formData, event)
289
+ }
290
+
291
+ return true
292
+ }
293
+
294
+ const validFields = [
295
+ { id: 'field1', value: 'test', valid: true }
296
+ ]
297
+
298
+ const invalidFields = [
299
+ { id: 'field1', value: '', valid: false }
300
+ ]
301
+
302
+ // Valid form submission
303
+ const result1 = await simulateSubmit(false, false, false, validFields, false, mockOnSubmit)
304
+ expect(result1).toBe(true)
305
+ expect(mockOnSubmit).toHaveBeenCalledWith({ field1: 'test' }, expect.any(Object))
306
+
307
+ mockOnSubmit.mockClear()
308
+
309
+ // Invalid form submission (should not submit)
310
+ const result2 = await simulateSubmit(false, false, false, invalidFields, false, mockOnSubmit)
311
+ expect(result2).toBe(false)
312
+ expect(mockOnSubmit).not.toHaveBeenCalled()
313
+
314
+ // Invalid form submission with noValidate (should submit)
315
+ const result3 = await simulateSubmit(false, false, false, invalidFields, true, mockOnSubmit)
316
+ expect(result3).toBe(true)
317
+ expect(mockOnSubmit).toHaveBeenCalled()
318
+
319
+ mockOnSubmit.mockClear()
320
+
321
+ // Disabled form (should not submit)
322
+ const result4 = await simulateSubmit(true, false, false, validFields, false, mockOnSubmit)
323
+ expect(result4).toBe(false)
324
+ expect(mockOnSubmit).not.toHaveBeenCalled()
325
+ })
326
+
327
+ test('form reset logic works correctly', () => {
328
+ const fields = [
329
+ { id: 'field1', value: 'test', required: true, valid: true },
330
+ { id: 'field2', value: 'data', required: false, valid: true }
331
+ ]
332
+
333
+ const resetFields = (fields) => {
334
+ return fields.map(field => ({
335
+ ...field,
336
+ value: '',
337
+ valid: !field.required
338
+ }))
339
+ }
340
+
341
+ const result = resetFields(fields)
342
+ expect(result[0]).toEqual({ id: 'field1', value: '', required: true, valid: false })
343
+ expect(result[1]).toEqual({ id: 'field2', value: '', required: false, valid: true })
344
+ })
345
+
346
+ test('child enhancement works correctly', () => {
347
+ const enhanceChild = (child, changeField, outlined, disabled, field) => {
348
+ const enhancedProps = {
349
+ onChange: changeField,
350
+ outlined: outlined,
351
+ disabled: disabled || child.props.disabled,
352
+ error: field && !field.valid ? (field.required ? 'This field is required' : false) : child.props.error
353
+ }
354
+ return enhancedProps
355
+ }
356
+
357
+ const mockChild = { props: { disabled: false, error: false } }
358
+ const mockField = { valid: false, required: true }
359
+
360
+ const result = enhanceChild(mockChild, () => {}, true, false, mockField)
361
+ expect(result.outlined).toBe(true)
362
+ expect(result.disabled).toBe(false)
363
+ expect(result.error).toBe('This field is required')
364
+
365
+ const validField = { valid: true, required: true }
366
+ const result2 = enhanceChild(mockChild, () => {}, false, false, validField)
367
+ expect(result2.error).toBe(false)
368
+ })
369
+ })