ywana-core8 0.1.82 → 0.1.84

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/src/html/tree.js CHANGED
@@ -1,10 +1,20 @@
1
- import React, { useState, useCallback, useEffect, useMemo, useRef } from 'react'
1
+ import React, { useState, useCallback, useEffect, useMemo, useRef, createContext, useContext } from 'react'
2
2
  import PropTypes from 'prop-types'
3
3
  import { Icon } from './icon'
4
4
  import { Text, TEXTFORMATS } from './text'
5
5
  import { TextField } from './textfield'
6
6
  import './tree.css'
7
7
 
8
+ // Context for tree state
9
+ const TreeContext = createContext({
10
+ multiSelect: false,
11
+ selectedItems: [],
12
+ onMultiSelect: null,
13
+ searchTerm: '',
14
+ forceExpandAll: false,
15
+ forceCollapseAll: false
16
+ })
17
+
8
18
  /**
9
19
  * Enhanced Tree component with improved functionality while maintaining 100% compatibility
10
20
  */
@@ -22,8 +32,7 @@ export const Tree = (props) => {
22
32
  sortDirection = 'asc',
23
33
  multiSelect = false,
24
34
  onMultiSelect,
25
- expandAll = false,
26
- collapseAll = false,
35
+ showExpandIcon = false,
27
36
  onExpandAll,
28
37
  onCollapseAll,
29
38
  disabled = false,
@@ -40,6 +49,9 @@ export const Tree = (props) => {
40
49
  const [searchTerm, setSearchTerm] = useState('')
41
50
  const [selectedItems, setSelectedItems] = useState([])
42
51
  const [expandedNodes, setExpandedNodes] = useState(new Set())
52
+ const [allExpanded, setAllExpanded] = useState(false)
53
+ const [forceExpandAll, setForceExpandAll] = useState(false)
54
+ const [forceCollapseAll, setForceCollapseAll] = useState(false)
43
55
  const treeRef = useRef(null)
44
56
 
45
57
  // Validate props
@@ -52,6 +64,73 @@ export const Tree = (props) => {
52
64
  setSearchTerm(value)
53
65
  }, [])
54
66
 
67
+ // Apply expansion state to nodes based on current tree state
68
+ const applyExpansionState = useCallback((nodeElements) => {
69
+ return React.Children.toArray(nodeElements).map(child => {
70
+ if (!React.isValidElement(child)) return child
71
+
72
+ // If it's a TreeNode, apply the current expansion state
73
+ if (child.type?.displayName === 'TreeNode' || child.props.hasOwnProperty('open')) {
74
+ const processedChildren = child.props.children ? applyExpansionState(child.props.children) : child.props.children
75
+
76
+ return React.cloneElement(child, {
77
+ ...child.props,
78
+ open: allExpanded, // Apply current expansion state
79
+ children: processedChildren
80
+ })
81
+ }
82
+
83
+ return child
84
+ })
85
+ }, [allExpanded])
86
+
87
+ // Filter nodes based on search term and auto-expand matching nodes
88
+ const filterNodes = useCallback((nodeElements, searchTerm) => {
89
+ if (!searchTerm.trim()) {
90
+ // When search is cleared, apply current expansion state
91
+ return applyExpansionState(nodeElements)
92
+ }
93
+
94
+ return React.Children.toArray(nodeElements).map(child => {
95
+ if (!React.isValidElement(child)) return child
96
+
97
+ // Check if current node matches search
98
+ const label = child.props.label || ''
99
+ const matches = searchBy.some(field => {
100
+ const value = child.props[field] || label
101
+ return typeof value === 'string' &&
102
+ value.toLowerCase().includes(searchTerm.toLowerCase())
103
+ })
104
+
105
+ // Check if any children match (recursive)
106
+ let hasMatchingChildren = false
107
+ let filteredChildren = child.props.children
108
+
109
+ if (child.props.children) {
110
+ filteredChildren = filterNodes(child.props.children, searchTerm)
111
+ hasMatchingChildren = React.Children.count(filteredChildren) > 0
112
+ }
113
+
114
+ // If current node or children match, include it and auto-expand
115
+ if (matches || hasMatchingChildren) {
116
+ // Clone the element and force it to be open if it's a TreeNode
117
+ if (child.type?.displayName === 'TreeNode' || child.props.hasOwnProperty('open')) {
118
+ return React.cloneElement(child, {
119
+ ...child.props,
120
+ open: true, // Auto-expand nodes with matches during search
121
+ children: filteredChildren
122
+ })
123
+ }
124
+ return React.cloneElement(child, {
125
+ ...child.props,
126
+ children: filteredChildren
127
+ })
128
+ }
129
+
130
+ return null
131
+ }).filter(Boolean)
132
+ }, [searchBy, applyExpansionState])
133
+
55
134
  // Handle multi-selection
56
135
  const handleMultiSelect = useCallback((id, selected) => {
57
136
  if (!multiSelect) return
@@ -68,16 +147,23 @@ export const Tree = (props) => {
68
147
  })
69
148
  }, [multiSelect, onMultiSelect])
70
149
 
71
- // Handle expand/collapse all
72
- const handleExpandAll = useCallback(() => {
73
- if (onExpandAll) onExpandAll()
74
- // Implementation would depend on tree structure
75
- }, [onExpandAll])
76
-
77
- const handleCollapseAll = useCallback(() => {
78
- if (onCollapseAll) onCollapseAll()
79
- // Implementation would depend on tree structure
80
- }, [onCollapseAll])
150
+ // Handle expand/collapse toggle - using context to propagate to all TreeNodes
151
+ const handleExpandToggle = useCallback(() => {
152
+ const newState = !allExpanded
153
+ setAllExpanded(newState)
154
+
155
+ if (newState) {
156
+ if (onExpandAll) onExpandAll()
157
+ // Trigger expand all via context
158
+ setForceExpandAll(true)
159
+ setTimeout(() => setForceExpandAll(false), 100)
160
+ } else {
161
+ if (onCollapseAll) onCollapseAll()
162
+ // Trigger collapse all via context
163
+ setForceCollapseAll(true)
164
+ setTimeout(() => setForceCollapseAll(false), 100)
165
+ }
166
+ }, [allExpanded, onExpandAll, onCollapseAll])
81
167
 
82
168
  // Generate CSS classes
83
169
  const cssClasses = [
@@ -134,58 +220,61 @@ export const Tree = (props) => {
134
220
  )
135
221
  }
136
222
 
223
+ const contextValue = {
224
+ multiSelect,
225
+ selectedItems,
226
+ onMultiSelect: handleMultiSelect,
227
+ searchTerm,
228
+ forceExpandAll,
229
+ forceCollapseAll
230
+ }
231
+
137
232
  return (
138
- <div
139
- className={cssClasses}
140
- style={style}
141
- ref={treeRef}
142
- {...ariaAttributes}
143
- {...restProps}
144
- >
145
- {searchable && (
146
- <div className="tree__search">
147
- <TextField
148
- id="tree-search"
149
- placeholder={searchPlaceholder}
150
- value={searchTerm}
151
- onChange={handleSearch}
152
- icon="search"
153
- outlined={true}
154
- size="small"
155
- />
156
- </div>
157
- )}
233
+ <TreeContext.Provider value={contextValue}>
234
+ <div
235
+ className={cssClasses}
236
+ style={style}
237
+ ref={treeRef}
238
+ {...ariaAttributes}
239
+ {...restProps}
240
+ >
241
+ {/* Header with search and expand controls */}
242
+ {(searchable || showExpandIcon) && (
243
+ <div className={`tree__header ${!searchable && showExpandIcon ? 'tree__header--expand-only' : ''}`}>
244
+ {searchable && (
245
+ <div className="tree__search">
246
+ <TextField
247
+ id="tree-search"
248
+ placeholder={searchPlaceholder}
249
+ value={searchTerm}
250
+ onChange={handleSearch}
251
+ icon="search"
252
+ outlined={true}
253
+ size="small"
254
+ />
255
+ </div>
256
+ )}
257
+
258
+ {showExpandIcon && (
259
+ <div className="tree__expand-control">
260
+ <Icon
261
+ icon={allExpanded ? "unfold_less" : "unfold_more"}
262
+ size="small"
263
+ clickable
264
+ action={handleExpandToggle}
265
+ tooltip={allExpanded ? "Collapse all" : "Expand all"}
266
+ />
267
+ </div>
268
+ )}
269
+ </div>
270
+ )}
158
271
 
159
- {(expandAll || collapseAll) && (
160
- <div className="tree__controls">
161
- {expandAll && (
162
- <button
163
- className="tree__control-button"
164
- onClick={handleExpandAll}
165
- aria-label="Expand all nodes"
166
- >
167
- <Icon icon="unfold_more" size="small" />
168
- <Text size="sm">Expand All</Text>
169
- </button>
170
- )}
171
- {collapseAll && (
172
- <button
173
- className="tree__control-button"
174
- onClick={handleCollapseAll}
175
- aria-label="Collapse all nodes"
176
- >
177
- <Icon icon="unfold_less" size="small" />
178
- <Text size="sm">Collapse All</Text>
179
- </button>
180
- )}
272
+ <div className="tree__content">
273
+ {nodes}
274
+ {searchable ? filterNodes(children, searchTerm) : applyExpansionState(children)}
181
275
  </div>
182
- )}
183
-
184
- <div className="tree__content">
185
- {nodes}
186
- {children}
187
276
  </div>
188
- </div>
277
+ </TreeContext.Provider>
189
278
  )
190
279
  }
191
280
 
@@ -222,11 +311,36 @@ export const TreeNode = (props) => {
222
311
  const [isDragging, setIsDragging] = useState(false)
223
312
  const nodeRef = useRef(null)
224
313
 
314
+ // Get tree context for forced expand/collapse
315
+ const treeContext = useContext(TreeContext)
316
+ const { forceExpandAll, forceCollapseAll } = treeContext
317
+
225
318
  // Sync with open prop
226
319
  useEffect(() => {
227
320
  setIsOpen(open)
228
321
  }, [open])
229
322
 
323
+ // Handle forced expand/collapse from tree context
324
+ useEffect(() => {
325
+ if (forceExpandAll && expandable) {
326
+ setIsOpen(true)
327
+ // Also update the DOM element
328
+ if (nodeRef.current) {
329
+ nodeRef.current.open = true
330
+ }
331
+ }
332
+ }, [forceExpandAll, expandable])
333
+
334
+ useEffect(() => {
335
+ if (forceCollapseAll && expandable) {
336
+ setIsOpen(false)
337
+ // Also update the DOM element
338
+ if (nodeRef.current) {
339
+ nodeRef.current.open = false
340
+ }
341
+ }
342
+ }, [forceCollapseAll, expandable])
343
+
230
344
  // Handle selection (maintaining original behavior)
231
345
  const handleSelect = useCallback((event) => {
232
346
  if (disabled) return
@@ -235,13 +349,13 @@ export const TreeNode = (props) => {
235
349
  if (onSelect) onSelect(id)
236
350
  }, [disabled, onSelect, id])
237
351
 
238
- // Handle toggle
352
+ // Handle toggle - sync with details element
239
353
  const handleToggle = useCallback((event) => {
240
354
  if (disabled || !expandable) return
241
355
 
242
- event.preventDefault()
243
- event.stopPropagation()
244
- setIsOpen(prev => !prev)
356
+ // Sync our state with the details element
357
+ const detailsElement = event.currentTarget
358
+ setIsOpen(detailsElement.open)
245
359
  }, [disabled, expandable])
246
360
 
247
361
  // Handle keyboard interaction
@@ -334,16 +448,24 @@ export const TreeNode = (props) => {
334
448
  onDragEnd={handleDragEnd}
335
449
  onDrop={handleDrop}
336
450
  onDragOver={handleDragOver}
451
+ onToggle={handleToggle}
337
452
  {...restProps}
338
453
  >
339
454
  <summary
340
455
  className="tree-item"
341
456
  onClick={(event) => {
342
- // Si se hace click en el área general del nodo (no en el label), toggle
343
- if (event.target === event.currentTarget ||
344
- event.target.closest('.tree-node__toggle')) {
345
- handleToggle(event)
457
+ // Si se hace click en el toggle, permitir el comportamiento natural
458
+ if (event.target.closest('.tree-node__toggle')) {
459
+ // No hacer nada, dejar que el details maneje el toggle
460
+ return
346
461
  }
462
+
463
+ // Si hay onSelect y se hace click fuera del toggle, seleccionar
464
+ if (onSelect) {
465
+ event.preventDefault() // Prevenir el toggle del details
466
+ handleSelect(event)
467
+ }
468
+ // Si no hay onSelect, permitir el comportamiento natural del details
347
469
  }}
348
470
  onKeyDown={handleKeyDown}
349
471
  {...ariaAttributes}
@@ -351,13 +473,7 @@ export const TreeNode = (props) => {
351
473
  >
352
474
  {/* Expand/collapse indicator */}
353
475
  {hasChildren && expandable && (
354
- <div
355
- className="tree-node__toggle"
356
- onClick={(event) => {
357
- event.stopPropagation()
358
- handleToggle(event)
359
- }}
360
- >
476
+ <div className="tree-node__toggle">
361
477
  <Icon
362
478
  icon={isOpen ? 'expand_less' : 'expand_more'}
363
479
  size="small"
@@ -385,13 +501,7 @@ export const TreeNode = (props) => {
385
501
  )}
386
502
 
387
503
  {/* Label with badge */}
388
- <div
389
- className={`label ${clickable}`}
390
- onClick={(event) => {
391
- event.stopPropagation()
392
- handleSelect(event)
393
- }}
394
- >
504
+ <div className={`label ${clickable}`}>
395
505
  <span className="tree-node__label-text">
396
506
  {labelTxt}
397
507
  </span>
@@ -450,6 +560,13 @@ export const TreeItem = (props) => {
450
560
  const [isDragging, setIsDragging] = useState(false)
451
561
  const itemRef = useRef(null)
452
562
 
563
+ // Get tree context
564
+ const treeContext = useContext(TreeContext)
565
+ const { multiSelect, selectedItems, onMultiSelect: handleTreeMultiSelect } = treeContext
566
+
567
+ // Check if this item is selected in multi-select mode
568
+ const isMultiSelected = multiSelect && selectedItems.includes(id)
569
+
453
570
  // Handle selection (maintaining original behavior)
454
571
  const handleSelect = useCallback((event) => {
455
572
  if (disabled) return
@@ -458,13 +575,21 @@ export const TreeItem = (props) => {
458
575
  if (onSelect) onSelect(id)
459
576
  }, [disabled, onSelect, id])
460
577
 
461
- // Handle checkbox (maintaining original behavior)
578
+ // Handle checkbox (maintaining original behavior + multi-select)
462
579
  const handleCheck = useCallback((event) => {
463
580
  if (disabled) return
464
581
 
465
582
  event.stopPropagation()
466
- if (onCheck) onCheck(id, event.target.checked)
467
- }, [disabled, onCheck, id])
583
+ const isChecked = event.target.checked
584
+
585
+ // Handle original onCheck callback
586
+ if (onCheck) onCheck(id, isChecked)
587
+
588
+ // Handle multi-select if enabled
589
+ if (multiSelect && handleTreeMultiSelect) {
590
+ handleTreeMultiSelect(id, isChecked)
591
+ }
592
+ }, [disabled, onCheck, id, multiSelect, handleTreeMultiSelect])
468
593
 
469
594
  // Handle keyboard interaction
470
595
  const handleKeyDown = useCallback((event) => {
@@ -552,12 +677,12 @@ export const TreeItem = (props) => {
552
677
  {...ariaAttributes}
553
678
  {...restProps}
554
679
  >
555
- {/* Checkbox (maintaining original structure) */}
556
- {onCheck && (
680
+ {/* Checkbox (maintaining original structure + multi-select) */}
681
+ {(onCheck || multiSelect) && (
557
682
  <div className="tree-item__checkbox">
558
683
  <input
559
684
  type="checkbox"
560
- checked={checked}
685
+ checked={multiSelect ? isMultiSelected : checked}
561
686
  onChange={handleCheck}
562
687
  disabled={disabled}
563
688
  aria-label={`Select ${typeof label === 'string' ? label : 'item'}`}
@@ -618,10 +743,8 @@ Tree.propTypes = {
618
743
  multiSelect: PropTypes.bool,
619
744
  /** Multi-selection callback */
620
745
  onMultiSelect: PropTypes.func,
621
- /** Show expand all button */
622
- expandAll: PropTypes.bool,
623
- /** Show collapse all button */
624
- collapseAll: PropTypes.bool,
746
+ /** Show expand/collapse icon */
747
+ showExpandIcon: PropTypes.bool,
625
748
  /** Expand all callback */
626
749
  onExpandAll: PropTypes.func,
627
750
  /** Collapse all callback */