ywana-core8 0.1.74 → 0.1.76

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/ACCORDION_EVALUATION.md +583 -0
  2. package/CHECKBOX_EVALUATION.md +273 -0
  3. package/CHIP_EVALUATION.md +542 -0
  4. package/COLOR_EVALUATION.md +524 -0
  5. package/COMPONENTS_EVALUATION.md +477 -0
  6. package/FORM_EVALUATION.md +459 -0
  7. package/HEADER_EVALUATION.md +436 -0
  8. package/ICON_EVALUATION.md +254 -0
  9. package/LIST_EVALUATION.md +574 -0
  10. package/PROGRESS_EVALUATION.md +450 -0
  11. package/RADIO_EVALUATION.md +439 -0
  12. package/RADIO_VISUAL_FIX.md +183 -0
  13. package/SECTION_IMPROVEMENTS.md +153 -0
  14. package/SWITCH_EVALUATION.md +335 -0
  15. package/SWITCH_VISUAL_FIX.md +232 -0
  16. package/TAB_EVALUATION.md +626 -0
  17. package/TEXTFIELD_EVALUATION.md +747 -0
  18. package/TOOLTIP_FIX.md +157 -0
  19. package/TREE_EVALUATION.md +708 -0
  20. package/dist/index.cjs +7900 -1615
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.css +6094 -1122
  23. package/dist/index.css.map +1 -1
  24. package/dist/index.modern.js +7929 -1645
  25. package/dist/index.modern.js.map +1 -1
  26. package/dist/index.umd.js +7900 -1615
  27. package/dist/index.umd.js.map +1 -1
  28. package/jest.config.js +24 -0
  29. package/package.json +10 -1
  30. package/src/html/accordion.css +208 -4
  31. package/src/html/accordion.example.js +390 -0
  32. package/src/html/accordion.js +284 -28
  33. package/src/html/accordion.unit.test.js +334 -0
  34. package/src/html/button.css +157 -16
  35. package/src/html/button.example.js +374 -0
  36. package/src/html/button.js +240 -60
  37. package/src/html/button.test.js +422 -0
  38. package/src/html/checkbox.css +74 -2
  39. package/src/html/checkbox.example.js +316 -0
  40. package/src/html/checkbox.js +113 -26
  41. package/src/html/checkbox.test.js +285 -0
  42. package/src/html/chip.css +230 -19
  43. package/src/html/chip.example.js +355 -0
  44. package/src/html/chip.js +321 -25
  45. package/src/html/chip.test.js +425 -0
  46. package/src/html/color.css +435 -6
  47. package/src/html/color.example.js +527 -0
  48. package/src/html/color.js +458 -9
  49. package/src/html/color.test.js +362 -4
  50. package/src/html/components.example.js +492 -0
  51. package/src/html/components_enhanced.test.js +581 -0
  52. package/src/html/form.css +70 -3
  53. package/src/html/form.example.js +385 -0
  54. package/src/html/form.js +232 -34
  55. package/src/html/form.test.js +369 -0
  56. package/src/html/header2.css +264 -0
  57. package/src/html/header2.example.js +411 -0
  58. package/src/html/header2.js +203 -0
  59. package/src/html/header2.test.js +377 -0
  60. package/src/html/icon.css +20 -2
  61. package/src/html/icon.example.js +268 -0
  62. package/src/html/icon.js +86 -16
  63. package/src/html/icon.test.js +231 -0
  64. package/src/html/index.js +1 -1
  65. package/src/html/list.css +393 -1
  66. package/src/html/list.example.js +404 -0
  67. package/src/html/list.js +583 -40
  68. package/src/html/list.test.js +383 -0
  69. package/src/html/progress.css +707 -17
  70. package/src/html/progress.example.js +424 -0
  71. package/src/html/progress.js +906 -9
  72. package/src/html/progress.test.js +313 -0
  73. package/src/html/property.css +399 -0
  74. package/src/html/property.example.js +553 -0
  75. package/src/html/property.js +393 -15
  76. package/src/html/property.test.js +351 -2
  77. package/src/html/radio-visual-test.js +289 -0
  78. package/src/html/radio.css +137 -11
  79. package/src/html/radio.example.js +389 -0
  80. package/src/html/radio.js +234 -10
  81. package/src/html/radio.test.js +318 -0
  82. package/src/html/section.example.js +99 -0
  83. package/src/html/section.js +40 -3
  84. package/src/html/section.test.js +131 -0
  85. package/src/html/selector.css +329 -3
  86. package/src/html/selector.js +369 -23
  87. package/src/html/switch-debug.js +197 -0
  88. package/src/html/switch-test-visual.js +294 -0
  89. package/src/html/switch.css +200 -0
  90. package/src/html/switch.example.js +461 -0
  91. package/src/html/switch.js +283 -23
  92. package/src/html/switch.test.js +355 -0
  93. package/src/html/tab.css +288 -0
  94. package/src/html/tab.example.js +446 -0
  95. package/src/html/tab.js +387 -22
  96. package/src/html/tab_enhanced.js +378 -0
  97. package/src/html/tab_enhanced.test.js +504 -0
  98. package/src/html/table2.css +576 -0
  99. package/src/html/table2.example.js +703 -0
  100. package/src/html/table2.js +1252 -0
  101. package/src/html/table2.migration.md +328 -0
  102. package/src/html/table2.test.js +582 -0
  103. package/src/html/text.css +375 -0
  104. package/src/html/text.js +311 -20
  105. package/src/html/textfield.js +1 -1
  106. package/src/html/textfield2.css +842 -0
  107. package/src/html/textfield2.example.js +499 -0
  108. package/src/html/textfield2.js +1130 -0
  109. package/src/html/textfield2.test.js +950 -0
  110. package/src/html/thumbnail.css +289 -2
  111. package/src/html/thumbnail.js +214 -9
  112. package/src/html/tokenfield.css +449 -1
  113. package/src/html/tokenfield.example.js +503 -0
  114. package/src/html/tokenfield.js +561 -56
  115. package/src/html/tokenfield.test.js +423 -0
  116. package/src/html/tooltip-positioning-demo.js +187 -0
  117. package/src/html/tooltip.css +25 -2
  118. package/src/html/tree.css +228 -0
  119. package/src/html/tree.example.js +475 -0
  120. package/src/html/tree.js +712 -28
  121. package/src/html/tree_enhanced.test.js +495 -0
  122. package/table2.test.js +454 -0
  123. package/src/html/button.tsx +0 -38
@@ -0,0 +1,203 @@
1
+ import React, { useCallback } from 'react'
2
+ import PropTypes from 'prop-types'
3
+ import { Icon } from './icon'
4
+ import { Text } from './text'
5
+ import './header2.css'
6
+
7
+ /**
8
+ * Enhanced Header component with improved accessibility and robustness
9
+ * Maintains 100% visual compatibility with original Header
10
+ */
11
+ export const Header2 = (props) => {
12
+ const {
13
+ title,
14
+ icon,
15
+ iconSrc,
16
+ img,
17
+ caption = false,
18
+ prominent = false,
19
+ dense = false,
20
+ primary = false,
21
+ secondary = false,
22
+ clickable = false,
23
+ disabled = false,
24
+ loading = false,
25
+ className,
26
+ children,
27
+ action,
28
+ ariaLabel,
29
+ role = 'banner',
30
+ onClick,
31
+ onIconClick,
32
+ ...restProps
33
+ } = props
34
+
35
+ // Validate props
36
+ if (!title && !icon && !iconSrc && !ariaLabel) {
37
+ console.warn('Header2 component: title, icon, iconSrc, or ariaLabel should be provided for accessibility')
38
+ }
39
+
40
+ // Handle icon click
41
+ const handleIconClick = useCallback((event) => {
42
+ if (disabled || loading) return
43
+
44
+ if (onIconClick) {
45
+ onIconClick(event)
46
+ } else if (action) {
47
+ action(event)
48
+ }
49
+ }, [disabled, loading, onIconClick, action])
50
+
51
+ // Handle header click
52
+ const handleClick = useCallback((event) => {
53
+ if (disabled || loading) return
54
+
55
+ if (onClick) {
56
+ onClick(event)
57
+ }
58
+ }, [disabled, loading, onClick])
59
+
60
+ // Generate CSS classes (maintaining exact same logic as original)
61
+ const captionClass = caption ? 'caption' : ''
62
+ const prominentClass = prominent ? 'prominent' : ''
63
+ const denseClass = dense ? 'dense' : ''
64
+ let themeClass = primary ? 'primary' : ''
65
+ themeClass = secondary ? 'secondary' : themeClass
66
+
67
+ const safeClassName = className || ''
68
+ const cssClasses = [
69
+ 'header2',
70
+ captionClass,
71
+ prominentClass,
72
+ denseClass,
73
+ themeClass,
74
+ disabled && 'disabled',
75
+ loading && 'loading',
76
+ safeClassName
77
+ ].filter(Boolean).join(' ')
78
+
79
+ // Generate icon element (maintaining exact same logic as original)
80
+ let iconElement = null
81
+ if (icon) {
82
+ iconElement = (
83
+ <Icon
84
+ icon={icon}
85
+ clickable={clickable && !disabled && !loading}
86
+ action={handleIconClick}
87
+ disabled={disabled}
88
+ ariaLabel={`${icon} icon`}
89
+ />
90
+ )
91
+ } else if (iconSrc) {
92
+ iconElement = (
93
+ <img
94
+ className="header2-icon"
95
+ src={iconSrc}
96
+ alt={ariaLabel || title || 'Header icon'}
97
+ onClick={clickable && !disabled && !loading ? handleIconClick : undefined}
98
+ style={{
99
+ cursor: clickable && !disabled && !loading ? 'pointer' : 'default',
100
+ opacity: disabled ? 0.6 : 1
101
+ }}
102
+ />
103
+ )
104
+ }
105
+
106
+ // Generate background style (maintaining exact same logic as original)
107
+ const backgroundStyle = img ? { backgroundImage: `url(${img})` } : {}
108
+
109
+ // Generate title element (maintaining exact same logic as original)
110
+ const titleElement = title ? (
111
+ <label className="header2-title">
112
+ <Text>{title}</Text>
113
+ </label>
114
+ ) : null
115
+
116
+ // Accessibility attributes
117
+ const ariaAttributes = {
118
+ 'aria-label': ariaLabel || (typeof title === 'string' ? title : 'Header'),
119
+ 'aria-disabled': disabled,
120
+ 'aria-busy': loading,
121
+ role: role
122
+ }
123
+
124
+ return (
125
+ <header
126
+ className={cssClasses}
127
+ style={backgroundStyle}
128
+ onClick={onClick ? handleClick : undefined}
129
+ {...ariaAttributes}
130
+ {...restProps}
131
+ >
132
+ {iconElement}
133
+ {titleElement}
134
+ <span className="header2-actions" role="toolbar" aria-label="Header actions">
135
+ {loading && (
136
+ <Icon
137
+ icon="hourglass_empty"
138
+ className="header2-loading"
139
+ ariaLabel="Loading"
140
+ />
141
+ )}
142
+ {children}
143
+ </span>
144
+ </header>
145
+ )
146
+ }
147
+
148
+ // PropTypes for Header2
149
+ Header2.propTypes = {
150
+ /** Header title text */
151
+ title: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
152
+ /** Material icon name */
153
+ icon: PropTypes.string,
154
+ /** Icon image source URL */
155
+ iconSrc: PropTypes.string,
156
+ /** Background image URL */
157
+ img: PropTypes.string,
158
+ /** Whether header has caption style (4rem height) */
159
+ caption: PropTypes.bool,
160
+ /** Whether header has prominent style (12.8rem height) */
161
+ prominent: PropTypes.bool,
162
+ /** Whether header has dense style (2.4rem height) */
163
+ dense: PropTypes.bool,
164
+ /** Whether header has primary theme */
165
+ primary: PropTypes.bool,
166
+ /** Whether header has secondary theme */
167
+ secondary: PropTypes.bool,
168
+ /** Whether icon is clickable */
169
+ clickable: PropTypes.bool,
170
+ /** Whether header is disabled */
171
+ disabled: PropTypes.bool,
172
+ /** Whether header is in loading state */
173
+ loading: PropTypes.bool,
174
+ /** Additional CSS classes */
175
+ className: PropTypes.string,
176
+ /** Action elements (buttons, etc.) */
177
+ children: PropTypes.node,
178
+ /** Icon click handler (legacy support) */
179
+ action: PropTypes.func,
180
+ /** ARIA label for accessibility */
181
+ ariaLabel: PropTypes.string,
182
+ /** ARIA role */
183
+ role: PropTypes.string,
184
+ /** Header click handler */
185
+ onClick: PropTypes.func,
186
+ /** Icon click handler */
187
+ onIconClick: PropTypes.func
188
+ }
189
+
190
+ Header2.defaultProps = {
191
+ caption: false,
192
+ prominent: false,
193
+ dense: false,
194
+ primary: false,
195
+ secondary: false,
196
+ clickable: false,
197
+ disabled: false,
198
+ loading: false,
199
+ role: 'banner',
200
+ className: ''
201
+ }
202
+
203
+ export default Header2
@@ -0,0 +1,377 @@
1
+ import React from 'react'
2
+ import { Header2 } from './header2'
3
+
4
+ // Pruebas unitarias para el componente Header2
5
+ describe('Header2 Component', () => {
6
+ // Mock de los componentes dependientes
7
+ const mockText = jest.fn()
8
+ const mockIcon = jest.fn()
9
+
10
+ beforeEach(() => {
11
+ jest.clearAllMocks()
12
+
13
+ // Mock del componente Text
14
+ jest.doMock('./text', () => ({
15
+ Text: mockText
16
+ }))
17
+
18
+ // Mock del componente Icon
19
+ jest.doMock('./icon', () => ({
20
+ Icon: mockIcon
21
+ }))
22
+
23
+ // Mock de console.warn para las pruebas
24
+ jest.spyOn(console, 'warn').mockImplementation(() => {})
25
+ })
26
+
27
+ afterEach(() => {
28
+ console.warn.mockRestore()
29
+ })
30
+
31
+ test('component exports correctly', () => {
32
+ expect(Header2).toBeDefined()
33
+ expect(typeof Header2).toBe('function')
34
+ })
35
+
36
+ test('component has correct PropTypes', () => {
37
+ expect(Header2.propTypes).toBeDefined()
38
+ expect(Header2.propTypes.title).toBeDefined()
39
+ expect(Header2.propTypes.icon).toBeDefined()
40
+ expect(Header2.propTypes.iconSrc).toBeDefined()
41
+ expect(Header2.propTypes.img).toBeDefined()
42
+ expect(Header2.propTypes.caption).toBeDefined()
43
+ expect(Header2.propTypes.prominent).toBeDefined()
44
+ expect(Header2.propTypes.dense).toBeDefined()
45
+ expect(Header2.propTypes.primary).toBeDefined()
46
+ expect(Header2.propTypes.secondary).toBeDefined()
47
+ expect(Header2.propTypes.clickable).toBeDefined()
48
+ expect(Header2.propTypes.disabled).toBeDefined()
49
+ expect(Header2.propTypes.loading).toBeDefined()
50
+ expect(Header2.propTypes.className).toBeDefined()
51
+ expect(Header2.propTypes.children).toBeDefined()
52
+ expect(Header2.propTypes.action).toBeDefined()
53
+ expect(Header2.propTypes.ariaLabel).toBeDefined()
54
+ expect(Header2.propTypes.role).toBeDefined()
55
+ expect(Header2.propTypes.onClick).toBeDefined()
56
+ expect(Header2.propTypes.onIconClick).toBeDefined()
57
+ })
58
+
59
+ test('component has correct defaultProps', () => {
60
+ expect(Header2.defaultProps).toBeDefined()
61
+ expect(Header2.defaultProps.caption).toBe(false)
62
+ expect(Header2.defaultProps.prominent).toBe(false)
63
+ expect(Header2.defaultProps.dense).toBe(false)
64
+ expect(Header2.defaultProps.primary).toBe(false)
65
+ expect(Header2.defaultProps.secondary).toBe(false)
66
+ expect(Header2.defaultProps.clickable).toBe(false)
67
+ expect(Header2.defaultProps.disabled).toBe(false)
68
+ expect(Header2.defaultProps.loading).toBe(false)
69
+ expect(Header2.defaultProps.role).toBe('banner')
70
+ expect(Header2.defaultProps.className).toBe('')
71
+ })
72
+
73
+ test('warns when no accessibility props are provided', () => {
74
+ const validateAccessibility = (title, icon, iconSrc, ariaLabel) => {
75
+ if (!title && !icon && !iconSrc && !ariaLabel) {
76
+ console.warn('Header2 component: title, icon, iconSrc, or ariaLabel should be provided for accessibility')
77
+ }
78
+ }
79
+
80
+ validateAccessibility(null, null, null, null)
81
+ expect(console.warn).toHaveBeenCalledWith('Header2 component: title, icon, iconSrc, or ariaLabel should be provided for accessibility')
82
+
83
+ console.warn.mockClear()
84
+ validateAccessibility('Test Title', null, null, null)
85
+ expect(console.warn).not.toHaveBeenCalled()
86
+ })
87
+
88
+ test('CSS classes generation maintains original logic', () => {
89
+ const generateClasses = (caption, prominent, dense, primary, secondary, disabled, loading, className) => {
90
+ const captionClass = caption ? 'caption' : ''
91
+ const prominentClass = prominent ? 'prominent' : ''
92
+ const denseClass = dense ? 'dense' : ''
93
+ let themeClass = primary ? 'primary' : ''
94
+ themeClass = secondary ? 'secondary' : themeClass
95
+
96
+ return [
97
+ 'header2',
98
+ captionClass,
99
+ prominentClass,
100
+ denseClass,
101
+ themeClass,
102
+ disabled && 'disabled',
103
+ loading && 'loading',
104
+ className || ''
105
+ ].filter(Boolean).join(' ')
106
+ }
107
+
108
+ // Test basic header
109
+ expect(generateClasses(false, false, false, false, false, false, false, ''))
110
+ .toBe('header2')
111
+
112
+ // Test caption variant
113
+ expect(generateClasses(true, false, false, false, false, false, false, ''))
114
+ .toBe('header2 caption')
115
+
116
+ // Test prominent variant
117
+ expect(generateClasses(false, true, false, false, false, false, false, ''))
118
+ .toBe('header2 prominent')
119
+
120
+ // Test dense variant
121
+ expect(generateClasses(false, false, true, false, false, false, false, ''))
122
+ .toBe('header2 dense')
123
+
124
+ // Test primary theme
125
+ expect(generateClasses(false, false, false, true, false, false, false, ''))
126
+ .toBe('header2 primary')
127
+
128
+ // Test secondary theme (overrides primary)
129
+ expect(generateClasses(false, false, false, true, true, false, false, ''))
130
+ .toBe('header2 secondary')
131
+
132
+ // Test disabled state
133
+ expect(generateClasses(false, false, false, false, false, true, false, ''))
134
+ .toBe('header2 disabled')
135
+
136
+ // Test loading state
137
+ expect(generateClasses(false, false, false, false, false, false, true, ''))
138
+ .toBe('header2 loading')
139
+
140
+ // Test custom className
141
+ expect(generateClasses(false, false, false, false, false, false, false, 'custom-class'))
142
+ .toBe('header2 custom-class')
143
+
144
+ // Test combination
145
+ expect(generateClasses(true, false, false, true, false, false, false, 'custom'))
146
+ .toBe('header2 caption primary custom')
147
+ })
148
+
149
+ test('icon element generation maintains original logic', () => {
150
+ const generateIconElement = (icon, iconSrc, clickable, disabled, loading, handleIconClick, ariaLabel, title) => {
151
+ if (icon) {
152
+ return {
153
+ type: 'Icon',
154
+ props: {
155
+ icon: icon,
156
+ clickable: clickable && !disabled && !loading,
157
+ action: handleIconClick,
158
+ disabled: disabled,
159
+ ariaLabel: `${icon} icon`
160
+ }
161
+ }
162
+ } else if (iconSrc) {
163
+ return {
164
+ type: 'img',
165
+ props: {
166
+ className: 'header2-icon',
167
+ src: iconSrc,
168
+ alt: ariaLabel || title || 'Header icon',
169
+ onClick: clickable && !disabled && !loading ? handleIconClick : undefined,
170
+ style: {
171
+ cursor: clickable && !disabled && !loading ? 'pointer' : 'default',
172
+ opacity: disabled ? 0.6 : 1
173
+ }
174
+ }
175
+ }
176
+ }
177
+ return null
178
+ }
179
+
180
+ const mockHandler = jest.fn()
181
+
182
+ // Test with icon
183
+ const iconResult = generateIconElement('home', null, true, false, false, mockHandler, null, null)
184
+ expect(iconResult.type).toBe('Icon')
185
+ expect(iconResult.props.icon).toBe('home')
186
+ expect(iconResult.props.clickable).toBe(true)
187
+ expect(iconResult.props.ariaLabel).toBe('home icon')
188
+
189
+ // Test with iconSrc
190
+ const imgResult = generateIconElement(null, '/icon.png', true, false, false, mockHandler, 'Custom label', 'Title')
191
+ expect(imgResult.type).toBe('img')
192
+ expect(imgResult.props.src).toBe('/icon.png')
193
+ expect(imgResult.props.alt).toBe('Custom label')
194
+ expect(imgResult.props.style.cursor).toBe('pointer')
195
+
196
+ // Test disabled state
197
+ const disabledResult = generateIconElement('home', null, true, true, false, mockHandler, null, null)
198
+ expect(disabledResult.props.clickable).toBe(false)
199
+
200
+ // Test loading state
201
+ const loadingResult = generateIconElement('home', null, true, false, true, mockHandler, null, null)
202
+ expect(loadingResult.props.clickable).toBe(false)
203
+
204
+ // Test no icon
205
+ const noIconResult = generateIconElement(null, null, true, false, false, mockHandler, null, null)
206
+ expect(noIconResult).toBeNull()
207
+ })
208
+
209
+ test('background style generation maintains original logic', () => {
210
+ const generateBackgroundStyle = (img) => {
211
+ return img ? { backgroundImage: `url(${img})` } : {}
212
+ }
213
+
214
+ expect(generateBackgroundStyle(null)).toEqual({})
215
+ expect(generateBackgroundStyle('')).toEqual({})
216
+ expect(generateBackgroundStyle('/bg.jpg')).toEqual({ backgroundImage: 'url(/bg.jpg)' })
217
+ expect(generateBackgroundStyle('https://example.com/bg.png')).toEqual({ backgroundImage: 'url(https://example.com/bg.png)' })
218
+ })
219
+
220
+ test('accessibility attributes generation works correctly', () => {
221
+ const generateAriaAttributes = (ariaLabel, title, disabled, loading, role) => {
222
+ return {
223
+ 'aria-label': ariaLabel || (typeof title === 'string' ? title : 'Header'),
224
+ 'aria-disabled': disabled,
225
+ 'aria-busy': loading,
226
+ role: role
227
+ }
228
+ }
229
+
230
+ // Test with string title
231
+ const result1 = generateAriaAttributes(null, 'Test Title', false, false, 'banner')
232
+ expect(result1['aria-label']).toBe('Test Title')
233
+ expect(result1['aria-disabled']).toBe(false)
234
+ expect(result1['aria-busy']).toBe(false)
235
+ expect(result1.role).toBe('banner')
236
+
237
+ // Test with custom aria-label
238
+ const result2 = generateAriaAttributes('Custom Label', 'Test Title', false, false, 'banner')
239
+ expect(result2['aria-label']).toBe('Custom Label')
240
+
241
+ // Test with non-string title
242
+ const result3 = generateAriaAttributes(null, { type: 'span', children: 'Complex Title' }, false, false, 'banner')
243
+ expect(result3['aria-label']).toBe('Header')
244
+
245
+ // Test disabled state
246
+ const result4 = generateAriaAttributes(null, 'Test Title', true, false, 'banner')
247
+ expect(result4['aria-disabled']).toBe(true)
248
+
249
+ // Test loading state
250
+ const result5 = generateAriaAttributes(null, 'Test Title', false, true, 'banner')
251
+ expect(result5['aria-busy']).toBe(true)
252
+
253
+ // Test custom role
254
+ const result6 = generateAriaAttributes(null, 'Test Title', false, false, 'heading')
255
+ expect(result6.role).toBe('heading')
256
+ })
257
+
258
+ test('icon click handling works correctly', () => {
259
+ const mockOnIconClick = jest.fn()
260
+ const mockAction = jest.fn()
261
+
262
+ const simulateIconClick = (disabled, loading, onIconClick, action) => {
263
+ if (disabled || loading) return false
264
+
265
+ if (onIconClick) {
266
+ onIconClick({})
267
+ return true
268
+ } else if (action) {
269
+ action({})
270
+ return true
271
+ }
272
+ return false
273
+ }
274
+
275
+ // Normal click with onIconClick
276
+ expect(simulateIconClick(false, false, mockOnIconClick, null)).toBe(true)
277
+ expect(mockOnIconClick).toHaveBeenCalled()
278
+
279
+ mockOnIconClick.mockClear()
280
+
281
+ // Normal click with action (fallback)
282
+ expect(simulateIconClick(false, false, null, mockAction)).toBe(true)
283
+ expect(mockAction).toHaveBeenCalled()
284
+
285
+ mockAction.mockClear()
286
+
287
+ // Disabled click
288
+ expect(simulateIconClick(true, false, mockOnIconClick, null)).toBe(false)
289
+ expect(mockOnIconClick).not.toHaveBeenCalled()
290
+
291
+ // Loading click
292
+ expect(simulateIconClick(false, true, mockOnIconClick, null)).toBe(false)
293
+ expect(mockOnIconClick).not.toHaveBeenCalled()
294
+
295
+ // No handlers
296
+ expect(simulateIconClick(false, false, null, null)).toBe(false)
297
+ })
298
+
299
+ test('header click handling works correctly', () => {
300
+ const mockOnClick = jest.fn()
301
+
302
+ const simulateHeaderClick = (disabled, loading, onClick) => {
303
+ if (disabled || loading) return false
304
+
305
+ if (onClick) {
306
+ onClick({})
307
+ return true
308
+ }
309
+ return false
310
+ }
311
+
312
+ // Normal click
313
+ expect(simulateHeaderClick(false, false, mockOnClick)).toBe(true)
314
+ expect(mockOnClick).toHaveBeenCalled()
315
+
316
+ mockOnClick.mockClear()
317
+
318
+ // Disabled click
319
+ expect(simulateHeaderClick(true, false, mockOnClick)).toBe(false)
320
+ expect(mockOnClick).not.toHaveBeenCalled()
321
+
322
+ // Loading click
323
+ expect(simulateHeaderClick(false, true, mockOnClick)).toBe(false)
324
+ expect(mockOnClick).not.toHaveBeenCalled()
325
+
326
+ // No handler
327
+ expect(simulateHeaderClick(false, false, null)).toBe(false)
328
+ })
329
+
330
+ test('title element generation maintains original logic', () => {
331
+ const generateTitleElement = (title) => {
332
+ return title ? {
333
+ type: 'label',
334
+ className: 'header2-title',
335
+ children: { type: 'Text', children: title }
336
+ } : null
337
+ }
338
+
339
+ expect(generateTitleElement(null)).toBeNull()
340
+ expect(generateTitleElement('')).toBeNull()
341
+
342
+ const result = generateTitleElement('Test Title')
343
+ expect(result.type).toBe('label')
344
+ expect(result.className).toBe('header2-title')
345
+ expect(result.children.children).toBe('Test Title')
346
+ })
347
+
348
+ test('theme class logic maintains original behavior', () => {
349
+ const generateThemeClass = (primary, secondary) => {
350
+ let themeClass = primary ? 'primary' : ''
351
+ themeClass = secondary ? 'secondary' : themeClass
352
+ return themeClass
353
+ }
354
+
355
+ expect(generateThemeClass(false, false)).toBe('')
356
+ expect(generateThemeClass(true, false)).toBe('primary')
357
+ expect(generateThemeClass(false, true)).toBe('secondary')
358
+ expect(generateThemeClass(true, true)).toBe('secondary') // secondary overrides primary
359
+ })
360
+
361
+ test('maintains exact same height calculations as original', () => {
362
+ const getHeaderHeight = (caption, prominent, dense) => {
363
+ if (prominent) return '12.8rem'
364
+ if (caption) return '4rem'
365
+ if (dense) return '2.4rem'
366
+ return '3rem' // min-height for normal
367
+ }
368
+
369
+ expect(getHeaderHeight(false, false, false)).toBe('3rem')
370
+ expect(getHeaderHeight(true, false, false)).toBe('4rem')
371
+ expect(getHeaderHeight(false, true, false)).toBe('12.8rem')
372
+ expect(getHeaderHeight(false, false, true)).toBe('2.4rem')
373
+
374
+ // Prominent takes precedence
375
+ expect(getHeaderHeight(true, true, true)).toBe('12.8rem')
376
+ })
377
+ })
package/src/html/icon.css CHANGED
@@ -29,15 +29,33 @@
29
29
  }
30
30
 
31
31
  .icon.disabled {
32
- color: var(--text-color-lighter)
32
+ color: var(--text-color-lighter);
33
+ cursor: not-allowed;
34
+ }
35
+
36
+ .icon.clickable {
37
+ cursor: pointer;
33
38
  }
34
39
 
35
40
  .icon.clickable:hover {
36
41
  background-color: rgba(100,100,100,.1);
37
- cursor: pointer;
38
42
  border-radius: 100%;
39
43
  }
40
44
 
45
+ .icon.clickable:focus {
46
+ outline: 2px solid var(--primary-color);
47
+ outline-offset: 2px;
48
+ border-radius: 100%;
49
+ }
50
+
51
+ .icon.clickable.disabled {
52
+ cursor: not-allowed;
53
+ }
54
+
55
+ .icon.clickable.disabled:hover {
56
+ background-color: transparent;
57
+ }
58
+
41
59
  .icon.small {
42
60
  width: 2rem;
43
61
  height: 2rem;