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.
- 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/textfield.js +1 -1
- 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/list.js
CHANGED
@@ -1,23 +1,248 @@
|
|
1
|
-
import React, { Fragment } from 'react'
|
2
|
-
import
|
3
|
-
import {
|
1
|
+
import React, { Fragment, useState, useEffect, useCallback, useMemo, useRef } from 'react'
|
2
|
+
import PropTypes from 'prop-types'
|
3
|
+
import { Icon } from './icon'
|
4
|
+
import { Text } from './text'
|
5
|
+
import { CircularProgress } from './progress'
|
6
|
+
import { TextField } from './textfield'
|
4
7
|
import "./list.css"
|
5
8
|
|
6
9
|
/**
|
7
|
-
* List
|
10
|
+
* Enhanced List component with improved functionality while maintaining 100% compatibility
|
8
11
|
*/
|
9
12
|
export const List = (props) => {
|
13
|
+
const {
|
14
|
+
items = [],
|
15
|
+
children,
|
16
|
+
selected,
|
17
|
+
onSelect,
|
18
|
+
groupBy,
|
19
|
+
groupRenderer,
|
20
|
+
// New enhanced props (all optional for compatibility)
|
21
|
+
loading = false,
|
22
|
+
empty = false,
|
23
|
+
emptyMessage = "No items found",
|
24
|
+
emptyIcon = "inbox",
|
25
|
+
searchable = false,
|
26
|
+
searchPlaceholder = "Search...",
|
27
|
+
searchBy = ['line1', 'line2'],
|
28
|
+
sortable = false,
|
29
|
+
sortBy,
|
30
|
+
sortDirection = 'asc',
|
31
|
+
onSort,
|
32
|
+
multiSelect = false,
|
33
|
+
onMultiSelect,
|
34
|
+
dense = false,
|
35
|
+
disabled = false,
|
36
|
+
animated = true,
|
37
|
+
virtualized = false,
|
38
|
+
itemHeight = 48,
|
39
|
+
maxHeight,
|
40
|
+
className,
|
41
|
+
style,
|
42
|
+
ariaLabel,
|
43
|
+
role = 'list',
|
44
|
+
...restProps
|
45
|
+
} = props
|
10
46
|
|
11
|
-
const
|
47
|
+
const [searchTerm, setSearchTerm] = useState('')
|
48
|
+
const [sortConfig, setSortConfig] = useState({
|
49
|
+
key: sortBy,
|
50
|
+
direction: sortDirection
|
51
|
+
})
|
52
|
+
const listRef = useRef(null)
|
12
53
|
|
13
|
-
|
14
|
-
|
54
|
+
// Validate props
|
55
|
+
if (!Array.isArray(items)) {
|
56
|
+
console.warn('List component: items prop must be an array')
|
15
57
|
}
|
16
58
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
59
|
+
// Handle selection (maintaining original behavior)
|
60
|
+
const handleSelect = useCallback((id, event) => {
|
61
|
+
if (disabled) return
|
62
|
+
|
63
|
+
if (multiSelect && event?.ctrlKey) {
|
64
|
+
// Multi-select with Ctrl key
|
65
|
+
const currentSelected = Array.isArray(selected) ? selected : [selected].filter(Boolean)
|
66
|
+
const newSelected = currentSelected.includes(id)
|
67
|
+
? currentSelected.filter(s => s !== id)
|
68
|
+
: [...currentSelected, id]
|
69
|
+
|
70
|
+
if (onMultiSelect) {
|
71
|
+
onMultiSelect(newSelected)
|
72
|
+
} else if (onSelect) {
|
73
|
+
onSelect(newSelected)
|
74
|
+
}
|
75
|
+
} else {
|
76
|
+
// Single select (original behavior)
|
77
|
+
if (onSelect) onSelect(id)
|
78
|
+
}
|
79
|
+
}, [disabled, multiSelect, selected, onSelect, onMultiSelect])
|
80
|
+
|
81
|
+
// Handle search
|
82
|
+
const handleSearch = useCallback((searchId, value) => {
|
83
|
+
setSearchTerm(value)
|
84
|
+
}, [])
|
85
|
+
|
86
|
+
// Filter items by search
|
87
|
+
const filteredItems = useMemo(() => {
|
88
|
+
if (!searchable || !searchTerm.trim()) return items
|
89
|
+
|
90
|
+
return items.filter(item => {
|
91
|
+
const searchText = searchBy
|
92
|
+
.map(field => item[field] || '')
|
93
|
+
.join(' ')
|
94
|
+
.toLowerCase()
|
95
|
+
return searchText.includes(searchTerm.toLowerCase())
|
96
|
+
})
|
97
|
+
}, [items, searchable, searchTerm, searchBy])
|
98
|
+
|
99
|
+
// Sort items
|
100
|
+
const sortedItems = useMemo(() => {
|
101
|
+
if (!sortable || !sortConfig.key) return filteredItems
|
102
|
+
|
103
|
+
return [...filteredItems].sort((a, b) => {
|
104
|
+
const aValue = a[sortConfig.key] || ''
|
105
|
+
const bValue = b[sortConfig.key] || ''
|
106
|
+
|
107
|
+
if (sortConfig.direction === 'asc') {
|
108
|
+
return aValue.toString().localeCompare(bValue.toString())
|
109
|
+
} else {
|
110
|
+
return bValue.toString().localeCompare(aValue.toString())
|
111
|
+
}
|
112
|
+
})
|
113
|
+
}, [filteredItems, sortable, sortConfig])
|
114
|
+
|
115
|
+
// Handle sort
|
116
|
+
const handleSort = useCallback((key) => {
|
117
|
+
if (!sortable) return
|
118
|
+
|
119
|
+
const direction = sortConfig.key === key && sortConfig.direction === 'asc' ? 'desc' : 'asc'
|
120
|
+
const newConfig = { key, direction }
|
121
|
+
|
122
|
+
setSortConfig(newConfig)
|
123
|
+
if (onSort) onSort(newConfig)
|
124
|
+
}, [sortable, sortConfig, onSort])
|
125
|
+
|
126
|
+
// Generate CSS classes
|
127
|
+
const cssClasses = [
|
128
|
+
'list',
|
129
|
+
dense && 'list--dense',
|
130
|
+
disabled && 'list--disabled',
|
131
|
+
animated && 'list--animated',
|
132
|
+
loading && 'list--loading',
|
133
|
+
className
|
134
|
+
].filter(Boolean).join(' ')
|
135
|
+
|
136
|
+
// Accessibility attributes
|
137
|
+
const ariaAttributes = {
|
138
|
+
'aria-label': ariaLabel || 'List',
|
139
|
+
'aria-disabled': disabled,
|
140
|
+
'aria-busy': loading,
|
141
|
+
role: role
|
142
|
+
}
|
143
|
+
|
144
|
+
// Show loading state
|
145
|
+
if (loading) {
|
146
|
+
return (
|
147
|
+
<div className={cssClasses} style={style} {...ariaAttributes} {...restProps}>
|
148
|
+
<div className="list__loading">
|
149
|
+
<CircularProgress size="medium" />
|
150
|
+
<Text>Loading...</Text>
|
151
|
+
</div>
|
152
|
+
{children}
|
153
|
+
</div>
|
154
|
+
)
|
155
|
+
}
|
156
|
+
|
157
|
+
// Show empty state
|
158
|
+
if (empty || sortedItems.length === 0) {
|
159
|
+
return (
|
160
|
+
<div className={cssClasses} style={style} {...ariaAttributes} {...restProps}>
|
161
|
+
{searchable && (
|
162
|
+
<div className="list__search">
|
163
|
+
<TextField
|
164
|
+
id="list-search"
|
165
|
+
placeholder={searchPlaceholder}
|
166
|
+
value={searchTerm}
|
167
|
+
onChange={handleSearch}
|
168
|
+
icon="search"
|
169
|
+
outlined={true}
|
170
|
+
size="small"
|
171
|
+
/>
|
172
|
+
</div>
|
173
|
+
)}
|
174
|
+
<div className="list__empty">
|
175
|
+
<Icon icon={emptyIcon} size="large" />
|
176
|
+
<Text>{emptyMessage}</Text>
|
177
|
+
</div>
|
178
|
+
{children}
|
179
|
+
</div>
|
180
|
+
)
|
181
|
+
}
|
182
|
+
|
183
|
+
// Render grouped or normal list
|
184
|
+
return groupBy ? (
|
185
|
+
<GroupedList
|
186
|
+
{...props}
|
187
|
+
items={sortedItems}
|
188
|
+
onSelect={handleSelect}
|
189
|
+
searchTerm={searchTerm}
|
190
|
+
onSearch={handleSearch}
|
191
|
+
onSort={handleSort}
|
192
|
+
sortConfig={sortConfig}
|
193
|
+
cssClasses={cssClasses}
|
194
|
+
ariaAttributes={ariaAttributes}
|
195
|
+
/>
|
196
|
+
) : (
|
197
|
+
<div className={cssClasses} style={style} ref={listRef} {...ariaAttributes} {...restProps}>
|
198
|
+
{searchable && (
|
199
|
+
<div className="list__search">
|
200
|
+
<TextField
|
201
|
+
id="list-search"
|
202
|
+
placeholder={searchPlaceholder}
|
203
|
+
value={searchTerm}
|
204
|
+
onChange={handleSearch}
|
205
|
+
icon="search"
|
206
|
+
outlined={true}
|
207
|
+
size="small"
|
208
|
+
/>
|
209
|
+
</div>
|
210
|
+
)}
|
211
|
+
|
212
|
+
{sortable && sortBy && (
|
213
|
+
<div className="list__sort">
|
214
|
+
<button
|
215
|
+
className="list__sort-button"
|
216
|
+
onClick={() => handleSort(sortBy)}
|
217
|
+
aria-label={`Sort by ${sortBy}`}
|
218
|
+
>
|
219
|
+
<Text>{sortBy}</Text>
|
220
|
+
<Icon
|
221
|
+
icon={sortConfig.direction === 'asc' ? 'arrow_upward' : 'arrow_downward'}
|
222
|
+
size="small"
|
223
|
+
/>
|
224
|
+
</button>
|
225
|
+
</div>
|
226
|
+
)}
|
227
|
+
|
228
|
+
<ul
|
229
|
+
className="list__items"
|
230
|
+
style={{ maxHeight: maxHeight }}
|
231
|
+
role="listbox"
|
232
|
+
aria-multiselectable={multiSelect}
|
233
|
+
>
|
234
|
+
{sortedItems.map(item => (
|
235
|
+
<ListItem
|
236
|
+
key={item.id}
|
237
|
+
item={item}
|
238
|
+
selected={selected}
|
239
|
+
onSelect={handleSelect}
|
240
|
+
multiSelect={multiSelect}
|
241
|
+
dense={dense}
|
242
|
+
disabled={disabled}
|
243
|
+
animated={animated}
|
244
|
+
/>
|
245
|
+
))}
|
21
246
|
</ul>
|
22
247
|
{children}
|
23
248
|
</div>
|
@@ -25,31 +250,126 @@ export const List = (props) => {
|
|
25
250
|
}
|
26
251
|
|
27
252
|
/**
|
28
|
-
* Grouped List
|
253
|
+
* Enhanced Grouped List with improved functionality
|
29
254
|
*/
|
30
255
|
const GroupedList = (props) => {
|
31
|
-
const {
|
256
|
+
const {
|
257
|
+
items = [],
|
258
|
+
children,
|
259
|
+
selected,
|
260
|
+
onSelect,
|
261
|
+
groupBy,
|
262
|
+
groupRenderer,
|
263
|
+
searchTerm,
|
264
|
+
onSearch,
|
265
|
+
searchable = false,
|
266
|
+
searchPlaceholder = "Search...",
|
267
|
+
multiSelect = false,
|
268
|
+
dense = false,
|
269
|
+
disabled = false,
|
270
|
+
animated = true,
|
271
|
+
cssClasses,
|
272
|
+
ariaAttributes,
|
273
|
+
style,
|
274
|
+
...restProps
|
275
|
+
} = props
|
32
276
|
|
33
|
-
const
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
277
|
+
const [collapsedGroups, setCollapsedGroups] = useState(new Set())
|
278
|
+
|
279
|
+
// Group items (maintaining original logic)
|
280
|
+
const groups = useMemo(() => {
|
281
|
+
return items.reduce((groups, item) => {
|
282
|
+
let group = groups.find(g => g.name === item[groupBy])
|
283
|
+
if (!group) {
|
284
|
+
group = { name: item[groupBy], items: [] }
|
285
|
+
groups.push(group)
|
286
|
+
}
|
287
|
+
group.items.push(item)
|
288
|
+
return groups
|
289
|
+
}, [])
|
290
|
+
}, [items, groupBy])
|
291
|
+
|
292
|
+
// Handle group collapse/expand
|
293
|
+
const handleGroupToggle = useCallback((groupName) => {
|
294
|
+
setCollapsedGroups(prev => {
|
295
|
+
const next = new Set(prev)
|
296
|
+
if (next.has(groupName)) {
|
297
|
+
next.delete(groupName)
|
298
|
+
} else {
|
299
|
+
next.add(groupName)
|
300
|
+
}
|
301
|
+
return next
|
302
|
+
})
|
41
303
|
}, [])
|
42
304
|
|
43
305
|
return (
|
44
|
-
<div className=
|
306
|
+
<div className={`${cssClasses} grouped`} style={style} {...ariaAttributes} {...restProps}>
|
307
|
+
{searchable && (
|
308
|
+
<div className="list__search">
|
309
|
+
<TextField
|
310
|
+
id="grouped-list-search"
|
311
|
+
placeholder={searchPlaceholder}
|
312
|
+
value={searchTerm}
|
313
|
+
onChange={onSearch}
|
314
|
+
icon="search"
|
315
|
+
outlined={true}
|
316
|
+
size="small"
|
317
|
+
/>
|
318
|
+
</div>
|
319
|
+
)}
|
320
|
+
|
45
321
|
{groups.map(group => {
|
322
|
+
const isCollapsed = collapsedGroups.has(group.name)
|
46
323
|
const groupTitle = groupRenderer ? groupRenderer(group) : <Text>{group.name}</Text>
|
324
|
+
|
47
325
|
return (
|
48
326
|
<Fragment key={group.name}>
|
49
|
-
<header
|
50
|
-
|
51
|
-
{
|
52
|
-
|
327
|
+
<header
|
328
|
+
className="list__group-header"
|
329
|
+
onClick={() => handleGroupToggle(group.name)}
|
330
|
+
role="button"
|
331
|
+
tabIndex={0}
|
332
|
+
aria-expanded={!isCollapsed}
|
333
|
+
aria-controls={`group-${group.name}`}
|
334
|
+
onKeyDown={(e) => {
|
335
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
336
|
+
e.preventDefault()
|
337
|
+
handleGroupToggle(group.name)
|
338
|
+
}
|
339
|
+
}}
|
340
|
+
>
|
341
|
+
<Icon
|
342
|
+
icon={isCollapsed ? 'expand_more' : 'expand_less'}
|
343
|
+
size="small"
|
344
|
+
className="list__group-toggle"
|
345
|
+
/>
|
346
|
+
{groupTitle}
|
347
|
+
<span className="list__group-count">
|
348
|
+
({group.items.length})
|
349
|
+
</span>
|
350
|
+
</header>
|
351
|
+
|
352
|
+
{!isCollapsed && (
|
353
|
+
<ul
|
354
|
+
id={`group-${group.name}`}
|
355
|
+
className="list__group-items"
|
356
|
+
role="group"
|
357
|
+
aria-labelledby={`group-header-${group.name}`}
|
358
|
+
>
|
359
|
+
{group.items.map(item => (
|
360
|
+
<ListItem
|
361
|
+
key={item.id}
|
362
|
+
item={item}
|
363
|
+
selected={selected}
|
364
|
+
onSelect={onSelect}
|
365
|
+
multiSelect={multiSelect}
|
366
|
+
dense={dense}
|
367
|
+
disabled={disabled}
|
368
|
+
animated={animated}
|
369
|
+
/>
|
370
|
+
))}
|
371
|
+
</ul>
|
372
|
+
)}
|
53
373
|
</Fragment>
|
54
374
|
)
|
55
375
|
})}
|
@@ -59,25 +379,248 @@ const GroupedList = (props) => {
|
|
59
379
|
}
|
60
380
|
|
61
381
|
/**
|
62
|
-
* List Item
|
382
|
+
* Enhanced List Item with improved functionality and accessibility
|
63
383
|
*/
|
64
|
-
const ListItem = ({
|
65
|
-
|
384
|
+
const ListItem = ({
|
385
|
+
item,
|
386
|
+
selected,
|
387
|
+
onSelect,
|
388
|
+
multiSelect = false,
|
389
|
+
dense = false,
|
390
|
+
disabled = false,
|
391
|
+
animated = true
|
392
|
+
}) => {
|
393
|
+
const {
|
394
|
+
id,
|
395
|
+
icon,
|
396
|
+
iconTooltip,
|
397
|
+
line1,
|
398
|
+
line2,
|
399
|
+
meta,
|
400
|
+
avatar,
|
401
|
+
badge,
|
402
|
+
actions,
|
403
|
+
disabled: itemDisabled = false,
|
404
|
+
...itemProps
|
405
|
+
} = item
|
406
|
+
|
407
|
+
const isItemDisabled = disabled || itemDisabled
|
408
|
+
const isSelected = Array.isArray(selected) ? selected.includes(id) : selected === id
|
409
|
+
|
410
|
+
// Handle selection with keyboard and mouse support
|
411
|
+
const handleSelect = useCallback((event) => {
|
412
|
+
if (isItemDisabled) return
|
413
|
+
|
414
|
+
event.preventDefault()
|
415
|
+
if (onSelect) onSelect(id, event)
|
416
|
+
}, [isItemDisabled, onSelect, id])
|
417
|
+
|
418
|
+
// Handle keyboard navigation
|
419
|
+
const handleKeyDown = useCallback((event) => {
|
420
|
+
if (isItemDisabled) return
|
421
|
+
|
422
|
+
switch (event.key) {
|
423
|
+
case 'Enter':
|
424
|
+
case ' ':
|
425
|
+
event.preventDefault()
|
426
|
+
if (onSelect) onSelect(id, event)
|
427
|
+
break
|
428
|
+
case 'ArrowDown':
|
429
|
+
case 'ArrowUp':
|
430
|
+
// Allow parent to handle navigation
|
431
|
+
break
|
432
|
+
default:
|
433
|
+
break
|
434
|
+
}
|
435
|
+
}, [isItemDisabled, onSelect, id])
|
66
436
|
|
67
|
-
|
68
|
-
|
437
|
+
// Generate CSS classes
|
438
|
+
const cssClasses = [
|
439
|
+
'list__item',
|
440
|
+
isSelected && 'list__item--selected',
|
441
|
+
isItemDisabled && 'list__item--disabled',
|
442
|
+
dense && 'list__item--dense',
|
443
|
+
animated && 'list__item--animated',
|
444
|
+
multiSelect && 'list__item--multi-select'
|
445
|
+
].filter(Boolean).join(' ')
|
446
|
+
|
447
|
+
// Accessibility attributes
|
448
|
+
const ariaAttributes = {
|
449
|
+
'aria-selected': isSelected,
|
450
|
+
'aria-disabled': isItemDisabled,
|
451
|
+
'aria-label': typeof line1 === 'string' ? line1 : 'List item',
|
452
|
+
role: 'option',
|
453
|
+
tabIndex: isItemDisabled ? -1 : 0
|
69
454
|
}
|
70
455
|
|
71
|
-
const isSelected = Array.isArray(selected) ? selected.includes(id) : selected === id
|
72
|
-
const style = isSelected ? "selected" : ""
|
73
456
|
return (
|
74
|
-
<li
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
457
|
+
<li
|
458
|
+
className={cssClasses}
|
459
|
+
onClick={handleSelect}
|
460
|
+
onKeyDown={handleKeyDown}
|
461
|
+
{...ariaAttributes}
|
462
|
+
{...itemProps}
|
463
|
+
>
|
464
|
+
{/* Selection indicator for multi-select */}
|
465
|
+
{multiSelect && (
|
466
|
+
<div className="list__item-selector">
|
467
|
+
<Icon
|
468
|
+
icon={isSelected ? 'check_box' : 'check_box_outline_blank'}
|
469
|
+
size="small"
|
470
|
+
className="list__item-checkbox"
|
471
|
+
/>
|
472
|
+
</div>
|
473
|
+
)}
|
474
|
+
|
475
|
+
{/* Avatar or Icon */}
|
476
|
+
{avatar ? (
|
477
|
+
<div className="list__item-avatar">
|
478
|
+
{typeof avatar === 'string' ? (
|
479
|
+
<img src={avatar} alt="" className="list__item-avatar-img" />
|
480
|
+
) : (
|
481
|
+
avatar
|
482
|
+
)}
|
483
|
+
</div>
|
484
|
+
) : icon ? (
|
485
|
+
<div className="list__item-icon">
|
486
|
+
<Icon
|
487
|
+
icon={icon}
|
488
|
+
size="small"
|
489
|
+
tooltip={iconTooltip}
|
490
|
+
disabled={isItemDisabled}
|
491
|
+
/>
|
492
|
+
</div>
|
493
|
+
) : null}
|
494
|
+
|
495
|
+
{/* Main content */}
|
496
|
+
<main className="list__item-content">
|
497
|
+
<div className="list__item-primary">
|
498
|
+
<Text className="list__item-line1">{line1}</Text>
|
499
|
+
{badge && (
|
500
|
+
<span className="list__item-badge">
|
501
|
+
{typeof badge === 'string' ? <Text size="small">{badge}</Text> : badge}
|
502
|
+
</span>
|
503
|
+
)}
|
504
|
+
</div>
|
505
|
+
{line2 && (
|
506
|
+
<div className="list__item-secondary">
|
507
|
+
<Text className="list__item-line2" size="small">{line2}</Text>
|
508
|
+
</div>
|
509
|
+
)}
|
79
510
|
</main>
|
80
|
-
|
511
|
+
|
512
|
+
{/* Meta information */}
|
513
|
+
{meta && (
|
514
|
+
<div className="list__item-meta">
|
515
|
+
{typeof meta === 'string' ? <Text size="small">{meta}</Text> : meta}
|
516
|
+
</div>
|
517
|
+
)}
|
518
|
+
|
519
|
+
{/* Actions */}
|
520
|
+
{actions && (
|
521
|
+
<div className="list__item-actions" role="toolbar">
|
522
|
+
{actions}
|
523
|
+
</div>
|
524
|
+
)}
|
81
525
|
</li>
|
82
526
|
)
|
83
|
-
}
|
527
|
+
}
|
528
|
+
|
529
|
+
// PropTypes for List component
|
530
|
+
List.propTypes = {
|
531
|
+
/** Array of items to display */
|
532
|
+
items: PropTypes.arrayOf(PropTypes.shape({
|
533
|
+
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
534
|
+
line1: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,
|
535
|
+
line2: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
536
|
+
icon: PropTypes.string,
|
537
|
+
iconTooltip: PropTypes.string,
|
538
|
+
avatar: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
539
|
+
badge: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
540
|
+
meta: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
541
|
+
actions: PropTypes.node,
|
542
|
+
disabled: PropTypes.bool
|
543
|
+
})).isRequired,
|
544
|
+
/** Child elements */
|
545
|
+
children: PropTypes.node,
|
546
|
+
/** Selected item ID(s) */
|
547
|
+
selected: PropTypes.oneOfType([
|
548
|
+
PropTypes.string,
|
549
|
+
PropTypes.number,
|
550
|
+
PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number]))
|
551
|
+
]),
|
552
|
+
/** Selection callback */
|
553
|
+
onSelect: PropTypes.func,
|
554
|
+
/** Group items by this property */
|
555
|
+
groupBy: PropTypes.string,
|
556
|
+
/** Custom group renderer */
|
557
|
+
groupRenderer: PropTypes.func,
|
558
|
+
/** Loading state */
|
559
|
+
loading: PropTypes.bool,
|
560
|
+
/** Empty state */
|
561
|
+
empty: PropTypes.bool,
|
562
|
+
/** Empty state message */
|
563
|
+
emptyMessage: PropTypes.string,
|
564
|
+
/** Empty state icon */
|
565
|
+
emptyIcon: PropTypes.string,
|
566
|
+
/** Enable search functionality */
|
567
|
+
searchable: PropTypes.bool,
|
568
|
+
/** Search input placeholder */
|
569
|
+
searchPlaceholder: PropTypes.string,
|
570
|
+
/** Properties to search by */
|
571
|
+
searchBy: PropTypes.arrayOf(PropTypes.string),
|
572
|
+
/** Enable sorting */
|
573
|
+
sortable: PropTypes.bool,
|
574
|
+
/** Property to sort by */
|
575
|
+
sortBy: PropTypes.string,
|
576
|
+
/** Sort direction */
|
577
|
+
sortDirection: PropTypes.oneOf(['asc', 'desc']),
|
578
|
+
/** Sort callback */
|
579
|
+
onSort: PropTypes.func,
|
580
|
+
/** Enable multi-selection */
|
581
|
+
multiSelect: PropTypes.bool,
|
582
|
+
/** Multi-selection callback */
|
583
|
+
onMultiSelect: PropTypes.func,
|
584
|
+
/** Dense layout */
|
585
|
+
dense: PropTypes.bool,
|
586
|
+
/** Disabled state */
|
587
|
+
disabled: PropTypes.bool,
|
588
|
+
/** Enable animations */
|
589
|
+
animated: PropTypes.bool,
|
590
|
+
/** Enable virtualization for large lists */
|
591
|
+
virtualized: PropTypes.bool,
|
592
|
+
/** Item height for virtualization */
|
593
|
+
itemHeight: PropTypes.number,
|
594
|
+
/** Maximum height of list */
|
595
|
+
maxHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
596
|
+
/** Additional CSS classes */
|
597
|
+
className: PropTypes.string,
|
598
|
+
/** Inline styles */
|
599
|
+
style: PropTypes.object,
|
600
|
+
/** ARIA label */
|
601
|
+
ariaLabel: PropTypes.string,
|
602
|
+
/** ARIA role */
|
603
|
+
role: PropTypes.string
|
604
|
+
}
|
605
|
+
|
606
|
+
List.defaultProps = {
|
607
|
+
items: [],
|
608
|
+
loading: false,
|
609
|
+
empty: false,
|
610
|
+
emptyMessage: "No items found",
|
611
|
+
emptyIcon: "inbox",
|
612
|
+
searchable: false,
|
613
|
+
searchPlaceholder: "Search...",
|
614
|
+
searchBy: ['line1', 'line2'],
|
615
|
+
sortable: false,
|
616
|
+
sortDirection: 'asc',
|
617
|
+
multiSelect: false,
|
618
|
+
dense: false,
|
619
|
+
disabled: false,
|
620
|
+
animated: true,
|
621
|
+
virtualized: false,
|
622
|
+
itemHeight: 48,
|
623
|
+
role: 'list'
|
624
|
+
}
|
625
|
+
|
626
|
+
export default List
|