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
package/src/html/accordion.js
CHANGED
@@ -1,41 +1,116 @@
|
|
1
|
-
import React, {useState, useEffect} from 'react'
|
1
|
+
import React, { useState, useEffect, useCallback } from 'react'
|
2
|
+
import PropTypes from 'prop-types'
|
2
3
|
import { Icon } from './icon'
|
3
4
|
import './accordion.css'
|
4
5
|
|
5
6
|
/**
|
6
|
-
* Accordion
|
7
|
+
* Enhanced Accordion component with improved accessibility and robustness
|
7
8
|
*/
|
8
9
|
export const Accordion = (props) => {
|
10
|
+
const {
|
11
|
+
className,
|
12
|
+
sections = [],
|
13
|
+
disabled = false,
|
14
|
+
allowMultiple = true,
|
15
|
+
animated = true,
|
16
|
+
onCheck,
|
17
|
+
onToggle,
|
18
|
+
onSectionChange,
|
19
|
+
ariaLabel,
|
20
|
+
...restProps
|
21
|
+
} = props
|
9
22
|
|
10
|
-
const { className, sections = [], onCheck } = props
|
11
23
|
const [openSections, setOpenSections] = useState([])
|
12
24
|
const [checkedSections, setCheckedSections] = useState([])
|
13
25
|
|
26
|
+
// Validate props
|
27
|
+
if (!Array.isArray(sections)) {
|
28
|
+
console.warn('Accordion component: sections prop must be an array')
|
29
|
+
}
|
30
|
+
|
31
|
+
// Initialize sections state
|
14
32
|
useEffect(() => {
|
15
|
-
const openSections = sections.map(section => section.open)
|
16
|
-
const checkedSections = sections.map(section => section.checked)
|
33
|
+
const openSections = sections.map(section => section.open || false)
|
34
|
+
const checkedSections = sections.map(section => section.checked || false)
|
17
35
|
setOpenSections(openSections)
|
18
36
|
setCheckedSections(checkedSections)
|
19
37
|
}, [sections])
|
20
38
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
39
|
+
// Toggle section open/closed
|
40
|
+
const toggle = useCallback((index) => {
|
41
|
+
if (disabled) return
|
42
|
+
|
43
|
+
setOpenSections(prevOpen => {
|
44
|
+
const next = allowMultiple
|
45
|
+
? prevOpen.map((open, i) => i === index ? !open : open)
|
46
|
+
: prevOpen.map((open, i) => i === index ? !open : false)
|
47
|
+
|
48
|
+
if (onToggle) {
|
49
|
+
onToggle(index, next[index], sections[index])
|
50
|
+
}
|
51
|
+
|
52
|
+
if (onSectionChange) {
|
53
|
+
onSectionChange('toggle', index, next[index], sections[index])
|
54
|
+
}
|
55
|
+
|
56
|
+
return next
|
57
|
+
})
|
58
|
+
}, [disabled, allowMultiple, onToggle, onSectionChange, sections])
|
59
|
+
|
60
|
+
// Toggle section checked state
|
61
|
+
const check = useCallback((index) => {
|
62
|
+
if (disabled) return
|
63
|
+
|
64
|
+
setCheckedSections(prevChecked => {
|
65
|
+
const next = prevChecked.map((checked, i) => i === index ? !checked : checked)
|
66
|
+
|
67
|
+
if (onCheck) {
|
68
|
+
onCheck(index, next[index], sections[index]?.id, sections[index])
|
69
|
+
}
|
25
70
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
71
|
+
if (onSectionChange) {
|
72
|
+
onSectionChange('check', index, next[index], sections[index])
|
73
|
+
}
|
74
|
+
|
75
|
+
return next
|
76
|
+
})
|
77
|
+
}, [disabled, onCheck, onSectionChange, sections])
|
78
|
+
|
79
|
+
// Generate CSS classes
|
80
|
+
const safeClassName = className || ''
|
81
|
+
const cssClasses = [
|
82
|
+
'accordion',
|
83
|
+
disabled && 'disabled',
|
84
|
+
animated && 'animated',
|
85
|
+
safeClassName
|
86
|
+
].filter(Boolean).join(' ')
|
87
|
+
|
88
|
+
// Accessibility attributes
|
89
|
+
const ariaAttributes = {
|
90
|
+
'aria-label': ariaLabel || 'Accordion',
|
91
|
+
'aria-disabled': disabled,
|
92
|
+
role: 'region'
|
30
93
|
}
|
31
94
|
|
32
95
|
return (
|
33
|
-
<div className={
|
96
|
+
<div className={cssClasses} {...ariaAttributes} {...restProps}>
|
34
97
|
{sections.map((section, index) => {
|
35
98
|
const isOpen = openSections[index]
|
36
99
|
const isChecked = checkedSections[index]
|
100
|
+
const sectionId = section.id || `accordion-section-${index}`
|
101
|
+
|
37
102
|
return (
|
38
|
-
<AccordionSection
|
103
|
+
<AccordionSection
|
104
|
+
key={sectionId}
|
105
|
+
{...section}
|
106
|
+
open={isOpen}
|
107
|
+
checked={isChecked}
|
108
|
+
disabled={disabled || section.disabled}
|
109
|
+
animated={animated}
|
110
|
+
sectionIndex={index}
|
111
|
+
onToggle={() => toggle(index)}
|
112
|
+
onCheck={() => check(index)}
|
113
|
+
>
|
39
114
|
{section.children}
|
40
115
|
</AccordionSection>
|
41
116
|
)
|
@@ -48,23 +123,204 @@ export const Accordion = (props) => {
|
|
48
123
|
* AccordionSection
|
49
124
|
*/
|
50
125
|
const AccordionSection = (props) => {
|
126
|
+
const {
|
127
|
+
checked,
|
128
|
+
icon,
|
129
|
+
title,
|
130
|
+
subtitle,
|
131
|
+
open = false,
|
132
|
+
disabled = false,
|
133
|
+
animated = true,
|
134
|
+
sectionIndex,
|
135
|
+
onToggle,
|
136
|
+
onCheck,
|
137
|
+
toolbar,
|
138
|
+
info,
|
139
|
+
children,
|
140
|
+
ariaLabel,
|
141
|
+
...restProps
|
142
|
+
} = props
|
51
143
|
|
52
|
-
const { checked, icon, title, subtitle, open = false, onToggle, onCheck, toolbar, info, children } = props
|
53
144
|
const togglerIcon = open ? "expand_less" : "expand_more"
|
54
|
-
const checkedIcon = checked === undefined || checked === null ?
|
145
|
+
const checkedIcon = checked === undefined || checked === null ?
|
146
|
+
null :
|
147
|
+
checked === false ? "check_box_outline_blank" : "check_box"
|
148
|
+
|
149
|
+
// Handle keyboard navigation
|
150
|
+
const handleKeyDown = useCallback((event) => {
|
151
|
+
if (disabled) return
|
152
|
+
|
153
|
+
switch (event.key) {
|
154
|
+
case 'Enter':
|
155
|
+
case ' ':
|
156
|
+
event.preventDefault()
|
157
|
+
if (onToggle) onToggle()
|
158
|
+
break
|
159
|
+
case 'ArrowDown':
|
160
|
+
case 'ArrowUp':
|
161
|
+
// Allow parent to handle navigation between sections
|
162
|
+
break
|
163
|
+
default:
|
164
|
+
break
|
165
|
+
}
|
166
|
+
}, [disabled, onToggle])
|
167
|
+
|
168
|
+
// Handle header click
|
169
|
+
const handleHeaderClick = useCallback((event) => {
|
170
|
+
if (disabled) return
|
171
|
+
|
172
|
+
// Don't toggle if clicking on interactive elements
|
173
|
+
if (event.target.closest('.accordion-section-checker') ||
|
174
|
+
event.target.closest('.accordion-section--toolbar')) {
|
175
|
+
return
|
176
|
+
}
|
177
|
+
|
178
|
+
if (onToggle) onToggle()
|
179
|
+
}, [disabled, onToggle])
|
180
|
+
|
181
|
+
// Generate CSS classes
|
182
|
+
const cssClasses = [
|
183
|
+
'accordion-section',
|
184
|
+
open && 'open',
|
185
|
+
disabled && 'disabled',
|
186
|
+
animated && 'animated',
|
187
|
+
checked && 'checked'
|
188
|
+
].filter(Boolean).join(' ')
|
189
|
+
|
190
|
+
// Generate unique IDs for accessibility
|
191
|
+
const headerId = `accordion-header-${sectionIndex}`
|
192
|
+
const contentId = `accordion-content-${sectionIndex}`
|
193
|
+
|
194
|
+
// Accessibility attributes for header
|
195
|
+
const headerAriaAttributes = {
|
196
|
+
'aria-expanded': open,
|
197
|
+
'aria-controls': contentId,
|
198
|
+
'aria-disabled': disabled,
|
199
|
+
'aria-label': ariaLabel || (typeof title === 'string' ? title : 'Accordion section'),
|
200
|
+
role: 'button',
|
201
|
+
tabIndex: disabled ? -1 : 0,
|
202
|
+
id: headerId
|
203
|
+
}
|
204
|
+
|
205
|
+
// Accessibility attributes for content
|
206
|
+
const contentAriaAttributes = {
|
207
|
+
'aria-labelledby': headerId,
|
208
|
+
'aria-hidden': !open,
|
209
|
+
role: 'region',
|
210
|
+
id: contentId
|
211
|
+
}
|
55
212
|
|
56
213
|
return (
|
57
|
-
<section
|
58
|
-
<header
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
{
|
63
|
-
|
64
|
-
{
|
65
|
-
|
66
|
-
|
67
|
-
|
214
|
+
<section className={cssClasses} {...restProps}>
|
215
|
+
<header
|
216
|
+
className="accordion-section-header"
|
217
|
+
onClick={handleHeaderClick}
|
218
|
+
onKeyDown={handleKeyDown}
|
219
|
+
{...headerAriaAttributes}
|
220
|
+
>
|
221
|
+
{checkedIcon && (
|
222
|
+
<Icon
|
223
|
+
className="accordion-section-checker"
|
224
|
+
icon={checkedIcon}
|
225
|
+
clickable={!disabled}
|
226
|
+
action={disabled ? undefined : onCheck}
|
227
|
+
disabled={disabled}
|
228
|
+
ariaLabel={checked ? 'Uncheck section' : 'Check section'}
|
229
|
+
/>
|
230
|
+
)}
|
231
|
+
|
232
|
+
{icon && (
|
233
|
+
<Icon
|
234
|
+
className="accordion-section-icon"
|
235
|
+
icon={icon}
|
236
|
+
ariaLabel={`${icon} icon`}
|
237
|
+
/>
|
238
|
+
)}
|
239
|
+
|
240
|
+
{title && (
|
241
|
+
<div className="accordion-section--title">
|
242
|
+
{title}
|
243
|
+
</div>
|
244
|
+
)}
|
245
|
+
|
246
|
+
{subtitle && (
|
247
|
+
<div className="accordion-section--subtitle">
|
248
|
+
{subtitle}
|
249
|
+
</div>
|
250
|
+
)}
|
251
|
+
|
252
|
+
{info && (
|
253
|
+
<div className="accordion-section--info">
|
254
|
+
{info}
|
255
|
+
</div>
|
256
|
+
)}
|
257
|
+
|
258
|
+
{toolbar && (
|
259
|
+
<div className="accordion-section--toolbar" role="toolbar">
|
260
|
+
{toolbar}
|
261
|
+
</div>
|
262
|
+
)}
|
263
|
+
|
264
|
+
<Icon
|
265
|
+
className="accordion-section-toggler"
|
266
|
+
icon={togglerIcon}
|
267
|
+
clickable={!disabled}
|
268
|
+
action={disabled ? undefined : onToggle}
|
269
|
+
disabled={disabled}
|
270
|
+
ariaLabel={open ? 'Collapse section' : 'Expand section'}
|
271
|
+
/>
|
272
|
+
</header>
|
273
|
+
|
274
|
+
{open && (
|
275
|
+
<main
|
276
|
+
className="accordion-section-content"
|
277
|
+
{...contentAriaAttributes}
|
278
|
+
>
|
279
|
+
{children}
|
280
|
+
</main>
|
281
|
+
)}
|
68
282
|
</section>
|
69
283
|
)
|
70
284
|
}
|
285
|
+
|
286
|
+
// PropTypes for Accordion
|
287
|
+
Accordion.propTypes = {
|
288
|
+
/** Array of section objects */
|
289
|
+
sections: PropTypes.arrayOf(PropTypes.shape({
|
290
|
+
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
291
|
+
title: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
292
|
+
subtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
293
|
+
icon: PropTypes.string,
|
294
|
+
info: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
295
|
+
toolbar: PropTypes.node,
|
296
|
+
children: PropTypes.node,
|
297
|
+
open: PropTypes.bool,
|
298
|
+
checked: PropTypes.bool,
|
299
|
+
disabled: PropTypes.bool
|
300
|
+
})).isRequired,
|
301
|
+
/** Additional CSS classes */
|
302
|
+
className: PropTypes.string,
|
303
|
+
/** Whether accordion is disabled */
|
304
|
+
disabled: PropTypes.bool,
|
305
|
+
/** Whether multiple sections can be open */
|
306
|
+
allowMultiple: PropTypes.bool,
|
307
|
+
/** Whether to animate transitions */
|
308
|
+
animated: PropTypes.bool,
|
309
|
+
/** ARIA label for the accordion */
|
310
|
+
ariaLabel: PropTypes.string,
|
311
|
+
/** Callback when section is checked/unchecked */
|
312
|
+
onCheck: PropTypes.func,
|
313
|
+
/** Callback when section is toggled */
|
314
|
+
onToggle: PropTypes.func,
|
315
|
+
/** Callback when any section changes */
|
316
|
+
onSectionChange: PropTypes.func
|
317
|
+
}
|
318
|
+
|
319
|
+
Accordion.defaultProps = {
|
320
|
+
className: '',
|
321
|
+
disabled: false,
|
322
|
+
allowMultiple: true,
|
323
|
+
animated: true
|
324
|
+
}
|
325
|
+
|
326
|
+
export default Accordion
|
@@ -0,0 +1,334 @@
|
|
1
|
+
import React from 'react'
|
2
|
+
import { Accordion } from './accordion'
|
3
|
+
|
4
|
+
// Pruebas unitarias para el componente Accordion mejorado
|
5
|
+
describe('Accordion Component', () => {
|
6
|
+
// Mock de los componentes dependientes
|
7
|
+
const mockIcon = jest.fn()
|
8
|
+
|
9
|
+
beforeEach(() => {
|
10
|
+
jest.clearAllMocks()
|
11
|
+
|
12
|
+
// Mock del componente Icon
|
13
|
+
jest.doMock('./icon', () => ({
|
14
|
+
Icon: mockIcon
|
15
|
+
}))
|
16
|
+
|
17
|
+
// Mock de console.warn para las pruebas
|
18
|
+
jest.spyOn(console, 'warn').mockImplementation(() => {})
|
19
|
+
})
|
20
|
+
|
21
|
+
afterEach(() => {
|
22
|
+
console.warn.mockRestore()
|
23
|
+
})
|
24
|
+
|
25
|
+
test('component exports correctly', () => {
|
26
|
+
expect(Accordion).toBeDefined()
|
27
|
+
expect(typeof Accordion).toBe('function')
|
28
|
+
})
|
29
|
+
|
30
|
+
test('component has correct PropTypes', () => {
|
31
|
+
expect(Accordion.propTypes).toBeDefined()
|
32
|
+
expect(Accordion.propTypes.sections).toBeDefined()
|
33
|
+
expect(Accordion.propTypes.className).toBeDefined()
|
34
|
+
expect(Accordion.propTypes.disabled).toBeDefined()
|
35
|
+
expect(Accordion.propTypes.allowMultiple).toBeDefined()
|
36
|
+
expect(Accordion.propTypes.animated).toBeDefined()
|
37
|
+
expect(Accordion.propTypes.ariaLabel).toBeDefined()
|
38
|
+
expect(Accordion.propTypes.onCheck).toBeDefined()
|
39
|
+
expect(Accordion.propTypes.onToggle).toBeDefined()
|
40
|
+
expect(Accordion.propTypes.onSectionChange).toBeDefined()
|
41
|
+
})
|
42
|
+
|
43
|
+
test('component has correct defaultProps', () => {
|
44
|
+
expect(Accordion.defaultProps).toBeDefined()
|
45
|
+
expect(Accordion.defaultProps.className).toBe('')
|
46
|
+
expect(Accordion.defaultProps.disabled).toBe(false)
|
47
|
+
expect(Accordion.defaultProps.allowMultiple).toBe(true)
|
48
|
+
expect(Accordion.defaultProps.animated).toBe(true)
|
49
|
+
})
|
50
|
+
|
51
|
+
test('warns when sections is not an array', () => {
|
52
|
+
const validateSections = (sections) => {
|
53
|
+
if (!Array.isArray(sections)) {
|
54
|
+
console.warn('Accordion component: sections prop must be an array')
|
55
|
+
}
|
56
|
+
}
|
57
|
+
|
58
|
+
validateSections('not an array')
|
59
|
+
expect(console.warn).toHaveBeenCalledWith('Accordion component: sections prop must be an array')
|
60
|
+
|
61
|
+
console.warn.mockClear()
|
62
|
+
validateSections([])
|
63
|
+
expect(console.warn).not.toHaveBeenCalled()
|
64
|
+
})
|
65
|
+
|
66
|
+
test('sections state initialization works correctly', () => {
|
67
|
+
const sections = [
|
68
|
+
{ id: 1, open: true, checked: false },
|
69
|
+
{ id: 2, open: false, checked: true },
|
70
|
+
{ id: 3, open: true, checked: true }
|
71
|
+
]
|
72
|
+
|
73
|
+
const initializeState = (sections) => {
|
74
|
+
const openSections = sections.map(section => section.open || false)
|
75
|
+
const checkedSections = sections.map(section => section.checked || false)
|
76
|
+
return { openSections, checkedSections }
|
77
|
+
}
|
78
|
+
|
79
|
+
const result = initializeState(sections)
|
80
|
+
expect(result.openSections).toEqual([true, false, true])
|
81
|
+
expect(result.checkedSections).toEqual([false, true, true])
|
82
|
+
})
|
83
|
+
|
84
|
+
test('toggle logic works correctly with allowMultiple=true', () => {
|
85
|
+
const openSections = [true, false, true]
|
86
|
+
const allowMultiple = true
|
87
|
+
const index = 1
|
88
|
+
|
89
|
+
const toggle = (openSections, index, allowMultiple) => {
|
90
|
+
return allowMultiple
|
91
|
+
? openSections.map((open, i) => i === index ? !open : open)
|
92
|
+
: openSections.map((open, i) => i === index ? !open : false)
|
93
|
+
}
|
94
|
+
|
95
|
+
const result = toggle(openSections, index, allowMultiple)
|
96
|
+
expect(result).toEqual([true, true, true])
|
97
|
+
})
|
98
|
+
|
99
|
+
test('toggle logic works correctly with allowMultiple=false', () => {
|
100
|
+
const openSections = [true, false, true]
|
101
|
+
const allowMultiple = false
|
102
|
+
const index = 1
|
103
|
+
|
104
|
+
const toggle = (openSections, index, allowMultiple) => {
|
105
|
+
return allowMultiple
|
106
|
+
? openSections.map((open, i) => i === index ? !open : open)
|
107
|
+
: openSections.map((open, i) => i === index ? !open : false)
|
108
|
+
}
|
109
|
+
|
110
|
+
const result = toggle(openSections, index, allowMultiple)
|
111
|
+
expect(result).toEqual([false, true, false])
|
112
|
+
})
|
113
|
+
|
114
|
+
test('check logic works correctly', () => {
|
115
|
+
const checkedSections = [false, true, false]
|
116
|
+
const index = 0
|
117
|
+
|
118
|
+
const check = (checkedSections, index) => {
|
119
|
+
return checkedSections.map((checked, i) => i === index ? !checked : checked)
|
120
|
+
}
|
121
|
+
|
122
|
+
const result = check(checkedSections, index)
|
123
|
+
expect(result).toEqual([true, true, false])
|
124
|
+
})
|
125
|
+
|
126
|
+
test('CSS classes generation works correctly', () => {
|
127
|
+
const generateClasses = (disabled, animated, className) => {
|
128
|
+
return [
|
129
|
+
'accordion',
|
130
|
+
disabled && 'disabled',
|
131
|
+
animated && 'animated',
|
132
|
+
className || ''
|
133
|
+
].filter(Boolean).join(' ')
|
134
|
+
}
|
135
|
+
|
136
|
+
expect(generateClasses(false, false, '')).toBe('accordion')
|
137
|
+
expect(generateClasses(true, false, '')).toBe('accordion disabled')
|
138
|
+
expect(generateClasses(false, true, '')).toBe('accordion animated')
|
139
|
+
expect(generateClasses(true, true, 'custom')).toBe('accordion disabled animated custom')
|
140
|
+
})
|
141
|
+
|
142
|
+
test('accessibility attributes generation works correctly', () => {
|
143
|
+
const generateAriaAttributes = (ariaLabel, disabled) => {
|
144
|
+
return {
|
145
|
+
'aria-label': ariaLabel || 'Accordion',
|
146
|
+
'aria-disabled': disabled,
|
147
|
+
role: 'region'
|
148
|
+
}
|
149
|
+
}
|
150
|
+
|
151
|
+
const result1 = generateAriaAttributes(null, false)
|
152
|
+
expect(result1['aria-label']).toBe('Accordion')
|
153
|
+
expect(result1['aria-disabled']).toBe(false)
|
154
|
+
expect(result1.role).toBe('region')
|
155
|
+
|
156
|
+
const result2 = generateAriaAttributes('Custom Accordion', true)
|
157
|
+
expect(result2['aria-label']).toBe('Custom Accordion')
|
158
|
+
expect(result2['aria-disabled']).toBe(true)
|
159
|
+
})
|
160
|
+
|
161
|
+
test('section ID generation works correctly', () => {
|
162
|
+
const generateSectionId = (section, index) => {
|
163
|
+
return section.id || `accordion-section-${index}`
|
164
|
+
}
|
165
|
+
|
166
|
+
expect(generateSectionId({ id: 'custom-id' }, 0)).toBe('custom-id')
|
167
|
+
expect(generateSectionId({}, 0)).toBe('accordion-section-0')
|
168
|
+
expect(generateSectionId({ id: null }, 1)).toBe('accordion-section-1')
|
169
|
+
})
|
170
|
+
|
171
|
+
test('callback handling works correctly', () => {
|
172
|
+
const mockOnCheck = jest.fn()
|
173
|
+
const mockOnToggle = jest.fn()
|
174
|
+
const mockOnSectionChange = jest.fn()
|
175
|
+
|
176
|
+
const simulateCheck = (index, newValue, section, onCheck, onSectionChange) => {
|
177
|
+
if (onCheck) {
|
178
|
+
onCheck(index, newValue, section?.id, section)
|
179
|
+
}
|
180
|
+
|
181
|
+
if (onSectionChange) {
|
182
|
+
onSectionChange('check', index, newValue, section)
|
183
|
+
}
|
184
|
+
}
|
185
|
+
|
186
|
+
const simulateToggle = (index, newValue, section, onToggle, onSectionChange) => {
|
187
|
+
if (onToggle) {
|
188
|
+
onToggle(index, newValue, section)
|
189
|
+
}
|
190
|
+
|
191
|
+
if (onSectionChange) {
|
192
|
+
onSectionChange('toggle', index, newValue, section)
|
193
|
+
}
|
194
|
+
}
|
195
|
+
|
196
|
+
const section = { id: 'test-section', title: 'Test' }
|
197
|
+
|
198
|
+
simulateCheck(0, true, section, mockOnCheck, mockOnSectionChange)
|
199
|
+
expect(mockOnCheck).toHaveBeenCalledWith(0, true, 'test-section', section)
|
200
|
+
expect(mockOnSectionChange).toHaveBeenCalledWith('check', 0, true, section)
|
201
|
+
|
202
|
+
simulateToggle(0, true, section, mockOnToggle, mockOnSectionChange)
|
203
|
+
expect(mockOnToggle).toHaveBeenCalledWith(0, true, section)
|
204
|
+
expect(mockOnSectionChange).toHaveBeenCalledWith('toggle', 0, true, section)
|
205
|
+
})
|
206
|
+
|
207
|
+
test('disabled state prevents interactions', () => {
|
208
|
+
const simulateInteraction = (disabled) => {
|
209
|
+
if (disabled) return false
|
210
|
+
return true
|
211
|
+
}
|
212
|
+
|
213
|
+
expect(simulateInteraction(true)).toBe(false)
|
214
|
+
expect(simulateInteraction(false)).toBe(true)
|
215
|
+
})
|
216
|
+
})
|
217
|
+
|
218
|
+
// AccordionSection tests
|
219
|
+
describe('AccordionSection Component', () => {
|
220
|
+
test('keyboard navigation logic works correctly', () => {
|
221
|
+
const mockOnToggle = jest.fn()
|
222
|
+
|
223
|
+
const handleKeyDown = (event, disabled, onToggle) => {
|
224
|
+
if (disabled) return false
|
225
|
+
|
226
|
+
switch (event.key) {
|
227
|
+
case 'Enter':
|
228
|
+
case ' ':
|
229
|
+
if (onToggle) onToggle()
|
230
|
+
return true
|
231
|
+
default:
|
232
|
+
return false
|
233
|
+
}
|
234
|
+
}
|
235
|
+
|
236
|
+
// Test Enter key
|
237
|
+
expect(handleKeyDown({ key: 'Enter' }, false, mockOnToggle)).toBe(true)
|
238
|
+
expect(mockOnToggle).toHaveBeenCalled()
|
239
|
+
|
240
|
+
mockOnToggle.mockClear()
|
241
|
+
|
242
|
+
// Test Space key
|
243
|
+
expect(handleKeyDown({ key: ' ' }, false, mockOnToggle)).toBe(true)
|
244
|
+
expect(mockOnToggle).toHaveBeenCalled()
|
245
|
+
|
246
|
+
mockOnToggle.mockClear()
|
247
|
+
|
248
|
+
// Test disabled state
|
249
|
+
expect(handleKeyDown({ key: 'Enter' }, true, mockOnToggle)).toBe(false)
|
250
|
+
expect(mockOnToggle).not.toHaveBeenCalled()
|
251
|
+
|
252
|
+
// Test other keys
|
253
|
+
expect(handleKeyDown({ key: 'Tab' }, false, mockOnToggle)).toBe(false)
|
254
|
+
})
|
255
|
+
|
256
|
+
test('header click logic works correctly', () => {
|
257
|
+
const mockOnToggle = jest.fn()
|
258
|
+
|
259
|
+
const handleHeaderClick = (event, disabled, onToggle) => {
|
260
|
+
if (disabled) return false
|
261
|
+
|
262
|
+
// Don't toggle if clicking on interactive elements
|
263
|
+
if (event.target.closest('.accordion-section-checker') ||
|
264
|
+
event.target.closest('.accordion-section--toolbar')) {
|
265
|
+
return false
|
266
|
+
}
|
267
|
+
|
268
|
+
if (onToggle) onToggle()
|
269
|
+
return true
|
270
|
+
}
|
271
|
+
|
272
|
+
// Normal click
|
273
|
+
const normalEvent = { target: { closest: () => null } }
|
274
|
+
expect(handleHeaderClick(normalEvent, false, mockOnToggle)).toBe(true)
|
275
|
+
expect(mockOnToggle).toHaveBeenCalled()
|
276
|
+
|
277
|
+
mockOnToggle.mockClear()
|
278
|
+
|
279
|
+
// Click on checker
|
280
|
+
const checkerEvent = { target: { closest: (selector) => selector === '.accordion-section-checker' ? true : null } }
|
281
|
+
expect(handleHeaderClick(checkerEvent, false, mockOnToggle)).toBe(false)
|
282
|
+
expect(mockOnToggle).not.toHaveBeenCalled()
|
283
|
+
|
284
|
+
// Click on toolbar
|
285
|
+
const toolbarEvent = { target: { closest: (selector) => selector === '.accordion-section--toolbar' ? true : null } }
|
286
|
+
expect(handleHeaderClick(toolbarEvent, false, mockOnToggle)).toBe(false)
|
287
|
+
expect(mockOnToggle).not.toHaveBeenCalled()
|
288
|
+
|
289
|
+
// Disabled state
|
290
|
+
expect(handleHeaderClick(normalEvent, true, mockOnToggle)).toBe(false)
|
291
|
+
expect(mockOnToggle).not.toHaveBeenCalled()
|
292
|
+
})
|
293
|
+
|
294
|
+
test('section CSS classes generation works correctly', () => {
|
295
|
+
const generateSectionClasses = (open, disabled, animated, checked) => {
|
296
|
+
return [
|
297
|
+
'accordion-section',
|
298
|
+
open && 'open',
|
299
|
+
disabled && 'disabled',
|
300
|
+
animated && 'animated',
|
301
|
+
checked && 'checked'
|
302
|
+
].filter(Boolean).join(' ')
|
303
|
+
}
|
304
|
+
|
305
|
+
expect(generateSectionClasses(false, false, false, false)).toBe('accordion-section')
|
306
|
+
expect(generateSectionClasses(true, false, false, false)).toBe('accordion-section open')
|
307
|
+
expect(generateSectionClasses(false, true, false, false)).toBe('accordion-section disabled')
|
308
|
+
expect(generateSectionClasses(false, false, true, false)).toBe('accordion-section animated')
|
309
|
+
expect(generateSectionClasses(false, false, false, true)).toBe('accordion-section checked')
|
310
|
+
expect(generateSectionClasses(true, true, true, true)).toBe('accordion-section open disabled animated checked')
|
311
|
+
})
|
312
|
+
|
313
|
+
test('checked icon logic works correctly', () => {
|
314
|
+
const getCheckedIcon = (checked) => {
|
315
|
+
return checked === undefined || checked === null ?
|
316
|
+
null :
|
317
|
+
checked === false ? "check_box_outline_blank" : "check_box"
|
318
|
+
}
|
319
|
+
|
320
|
+
expect(getCheckedIcon(undefined)).toBeNull()
|
321
|
+
expect(getCheckedIcon(null)).toBeNull()
|
322
|
+
expect(getCheckedIcon(false)).toBe("check_box_outline_blank")
|
323
|
+
expect(getCheckedIcon(true)).toBe("check_box")
|
324
|
+
})
|
325
|
+
|
326
|
+
test('toggler icon logic works correctly', () => {
|
327
|
+
const getTogglerIcon = (open) => {
|
328
|
+
return open ? "expand_less" : "expand_more"
|
329
|
+
}
|
330
|
+
|
331
|
+
expect(getTogglerIcon(true)).toBe("expand_less")
|
332
|
+
expect(getTogglerIcon(false)).toBe("expand_more")
|
333
|
+
})
|
334
|
+
})
|