react-core-ts 1.0.25 → 1.0.26

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.
@@ -1,611 +0,0 @@
1
- import React, {
2
- forwardRef,
3
- useCallback,
4
- useEffect,
5
- useImperativeHandle,
6
- useRef,
7
- useState,
8
- } from 'react';
9
- import ReactDOM from 'react-dom';
10
-
11
- import { AutoSuggestionInputProps } from './commontypes';
12
- import InputActions from './components/InputActions';
13
- import TreeNode from './components/TreeNode';
14
- import { useSuggestions } from './utilities/autosuggestions';
15
- import { buildTree } from './utilities/convertFlatArraytoTreeStructure';
16
- import { debounce } from './utilities/debounce';
17
- import { deepEqual } from './utilities/deepEqual';
18
- import { default as Tooltip } from './utilities/expandableTootltip';
19
- import { Search, Spinner } from './utilities/icons';
20
-
21
- type ValueProps = {
22
- [key: string]: string;
23
- };
24
- const AutoCompleteWithTreeStructure = forwardRef<
25
- HTMLInputElement,
26
- AutoSuggestionInputProps
27
- >(
28
- (
29
- {
30
- label,
31
- onChange,
32
- getData = async () => [],
33
- data = [],
34
- errors,
35
- required = false,
36
- name,
37
- fullWidth = false,
38
- placeholder,
39
- id,
40
- type = 'custom_select',
41
- selectedItems: propsSeelctedItems = [],
42
- readOnly = false,
43
- disabled = false,
44
- value,
45
- isMultiple = false,
46
- desc = 'name',
47
- descId = 'id',
48
- singleSelect,
49
- className,
50
- async = false,
51
- paginationEnabled,
52
- initialLoad,
53
- actionLabel,
54
- handleAction,
55
- nextBlock,
56
- notDataMessage,
57
- onFocus,
58
- expandable = false,
59
- textCount = 10,
60
- itemCount = 1,
61
- scrollRef,
62
- isTreeDropdown = true,
63
- flatArray = false,
64
- parentField = 'parentId',
65
- },
66
- ref
67
- ) => {
68
- const dropdownRef = useRef<HTMLDivElement>(null);
69
- // State Hooks Section
70
- const [isInitialRender, setIsInitialRender] = useState(true);
71
-
72
- const [inputValue, setInputValue] = useState<string>(value);
73
- const [searchValue, setSearchValue] = useState<string>('');
74
- const [nextPage, setNextPage] = useState<number | undefined>(1);
75
- const [dropOpen, setDropOpen] = useState<boolean>(false);
76
- const [selectedItems, setSelectedItems] = useState<ValueProps[]>([]);
77
- // API call for suggestions through a custom hook
78
- const [dropDownData, setDropDownData] = useState<ValueProps[]>(data);
79
- const inputRef = useRef(null);
80
- const dropRef = useRef(null);
81
-
82
- useEffect(() => {
83
- if (data) {
84
- if (flatArray) {
85
- setDropDownData(buildTree(data, descId, parentField));
86
- } else {
87
- setDropDownData(data);
88
- }
89
- }
90
- }, [data]);
91
- useImperativeHandle(ref, () => inputRef.current);
92
- const [dropdownStyle, setDropdownStyle] = useState({
93
- top: 0,
94
- left: 0,
95
- width: 200,
96
- });
97
-
98
- const adjustDropdownPosition = () => {
99
- if (dropdownRef.current) {
100
- const inputRect = dropdownRef.current.getBoundingClientRect();
101
- const dropdownPosition = {
102
- left: inputRect.left + window.scrollX,
103
- width: inputRect.width,
104
- top: 0,
105
- };
106
- // Check if there's enough space below the input for the dropdown
107
- const spaceBelow = window.innerHeight - inputRect.bottom;
108
- const spaceAbove = inputRect.top + window.scrollY;
109
-
110
- const dropdownHeight = 275; // Assume a fixed height or calculate based on content
111
- if (spaceBelow >= dropdownHeight) {
112
- dropdownPosition.top =
113
- inputRect.top + window.scrollY + inputRect.height;
114
- } else {
115
- dropdownPosition.top =
116
- inputRect.top + window.scrollY - dropdownHeight + 73;
117
- }
118
-
119
- setDropdownStyle({
120
- ...dropdownPosition,
121
- });
122
- }
123
- };
124
-
125
- useEffect(() => {
126
- window.addEventListener('resize', adjustDropdownPosition);
127
- adjustDropdownPosition();
128
-
129
- return () => {
130
- window.removeEventListener('resize', adjustDropdownPosition);
131
- };
132
- }, [dropOpen, selectedItems]);
133
-
134
- const { suggestions, isLoading, handlePickSuggestions } = useSuggestions(
135
- getData,
136
- data,
137
- dropOpen,
138
- async,
139
- paginationEnabled,
140
- initialLoad,
141
- inputValue,
142
- isMultiple,
143
- setNextPage,
144
- selectedItems
145
- // nextBlock
146
- );
147
-
148
- // Adding debounce to avoid making API calls on every keystroke
149
- const handleChangeWithDebounce = debounce((value) => {
150
- if ((type === 'auto_complete' || type === 'auto_suggestion') && async) {
151
- handlePickSuggestions(value, 1);
152
- }
153
- }, 1000);
154
-
155
- useEffect(() => {
156
- if (!deepEqual(selectedItems, propsSeelctedItems))
157
- setSelectedItems(propsSeelctedItems);
158
- }, [propsSeelctedItems]);
159
- // Effect to set the input value whenever `value` prop changes
160
- useEffect(() => {
161
- setInputValue(value ?? '');
162
- }, [value]);
163
-
164
- const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
165
- const { value } = e.target;
166
- setDropOpen(true);
167
- setSearchValue(value);
168
- handleChangeWithDebounce(value);
169
- if (!value) {
170
- setInputValue('');
171
- // onChange({ [descId]: '', [desc]: '' });
172
- }
173
- };
174
- const handleSuggestionChange = (e: React.ChangeEvent<HTMLInputElement>) => {
175
- const { value } = e.target;
176
- setDropOpen(true);
177
- setSearchValue(value);
178
- handleChangeWithDebounce(value);
179
- };
180
-
181
- const handleBlur = () => {
182
- setTimeout(() => {
183
- setDropOpen(false);
184
- }, 200);
185
- };
186
-
187
- const handleClear = () => {
188
- if (searchValue) {
189
- setSearchValue('');
190
- setDropOpen(false);
191
- } else {
192
- setInputValue('');
193
- onChange({ [descId]: '', [desc]: '' });
194
- setDropOpen(false);
195
- }
196
- };
197
-
198
- const generateClassName = useCallback(() => {
199
- return `qbs-textfield-default ${className} ${
200
- errors && errors?.message ? 'textfield-error' : 'textfield'
201
- } ${expandable ? 'expandable' : ''}`;
202
- }, [errors, name]);
203
- const handleRemoveSelectedItem = (index: number) => {
204
- setSelectedItems((prev) => {
205
- return prev.filter((_, i) => i !== index);
206
- });
207
- };
208
-
209
- useEffect(() => {
210
- if (isInitialRender) {
211
- setIsInitialRender(false);
212
- } else {
213
- onChange(selectedItems);
214
- }
215
- }, [selectedItems]);
216
-
217
- useEffect(() => {
218
- const handleClickOutside = (event: any) => {
219
- if (
220
- dropRef.current &&
221
- event.target instanceof Node &&
222
- !dropRef.current.contains(event.target) &&
223
- event.target.nodeName !== 'svg'
224
- ) {
225
- setDropOpen(false);
226
- setSearchValue('');
227
- }
228
- };
229
- document.addEventListener('mousedown', handleClickOutside as any);
230
- window.addEventListener('scroll', handleClickOutside as any);
231
-
232
- const scrollableDivs = document.querySelectorAll(
233
- 'div[style*="overflow"]'
234
- );
235
- scrollableDivs.forEach((div) =>
236
- div.addEventListener('scroll', handleClickOutside as any)
237
- );
238
- if (scrollRef && scrollRef.current && scrollRef.current !== null) {
239
- scrollRef.current.addEventListener('scroll', handleClickOutside as any);
240
- }
241
- return () => {
242
- document.removeEventListener('mousedown', handleClickOutside as any);
243
- window.removeEventListener('scroll', handleClickOutside as any);
244
- scrollableDivs.forEach((div) =>
245
- div.removeEventListener('scroll', handleClickOutside as any)
246
- );
247
- if (scrollRef && scrollRef.current && scrollRef.current !== null) {
248
- scrollRef.current.removeEventListener(
249
- 'scroll',
250
- handleClickOutside as any
251
- );
252
- }
253
- };
254
- }, []);
255
-
256
- // Filtering suggestions based on type and search value
257
- const selected: any = isMultiple ? selectedItems : inputValue;
258
-
259
- const handleOnClick = () => {
260
- !disabled && !readOnly ? setDropOpen(true) : '';
261
- };
262
- const onInputFocus = () => {
263
- if (inputRef.current) inputRef.current.focus();
264
- handleOnClick();
265
- };
266
-
267
- const tooltipContent =
268
- selectedItems?.length > itemCount
269
- ? selectedItems
270
- ?.slice(itemCount)
271
- .map((item) => item[desc])
272
- .join(', ')
273
- : '';
274
-
275
- const handleDropOpen = (e: any) => {
276
- if (!dropOpen) setDropOpen(true);
277
- };
278
-
279
- const handleDropClose = (e: any) => {
280
- if (dropOpen) setDropOpen(false);
281
- };
282
- const toggleExpand = (id: string) => {
283
- const newTreeData = updateTreeNode(dropDownData, id, (node) => {
284
- node.expanded = !node.expanded;
285
- });
286
- setDropDownData(newTreeData);
287
- };
288
-
289
- // Function to handle checkbox change (single or multi-select)
290
- const updateNode = (id: string, checked: boolean) => {
291
- let newTreeData;
292
- let checkedNodes: any[] = []; // Array to store the entire checked node objects
293
-
294
- if (isMultiple) {
295
- // Multi-select logic: update only the clicked node
296
- newTreeData = updateTreeNode(dropDownData, id, (node) => {
297
- node.checked = checked;
298
-
299
- // Update the checked nodes array
300
- if (checked) {
301
- checkedNodes.push(node); // Add the entire node if checked
302
- } else {
303
- // Remove the node from the array if unchecked
304
- checkedNodes = checkedNodes.filter(
305
- (checkedNode) => checkedNode[descId] !== node[descId]
306
- );
307
- }
308
- });
309
-
310
- // Find all checked nodes in the newTreeData (in case you need all checked nodes at any point)
311
- checkedNodes = [];
312
- newTreeData.forEach((node) => {
313
- getCheckedNodes(node, checkedNodes); // Recursively collect checked nodes
314
- });
315
- setSelectedItems(checkedNodes);
316
- onChange(checkedNodes);
317
- } else {
318
- // Single-select logic: uncheck all other nodes and check only the clicked one
319
- newTreeData = dropDownData.map((node) =>
320
- updateAllNodes(node, id, checked)
321
- );
322
- }
323
- setDropDownData(newTreeData);
324
- };
325
- const getCheckedNodes = (node: any, checkedNodes: any[]) => {
326
- if (node.checked) {
327
- checkedNodes.push(node); // Push the entire node object
328
- }
329
- if (node.children && node.children.length > 0) {
330
- node.children.forEach((child: any) =>
331
- getCheckedNodes(child, checkedNodes)
332
- );
333
- }
334
- };
335
-
336
- // Recursive function to update a specific node by ID
337
- const updateTreeNode = (
338
- nodes: any[],
339
- id: string,
340
- updateFn: (node: any) => void
341
- ): any[] => {
342
- return nodes.map((node) => {
343
- if (node[descId] === id) {
344
- updateFn(node);
345
- }
346
-
347
- if (node.children) {
348
- node.children = updateTreeNode(node.children, id, updateFn);
349
- }
350
-
351
- return node;
352
- });
353
- };
354
-
355
- // Recursive function to uncheck all other nodes in single-select mode
356
- const updateAllNodes = (node: any, id: string, checked: boolean): any => {
357
- // Reset all checked states to false, except for the selected node
358
- node.checked = node[descId] === id ? checked : false;
359
- if (node.checked) {
360
- setInputValue(node[desc]);
361
- onChange(node);
362
- }
363
- if (node.children) {
364
- node.children = node.children.map((child: any) =>
365
- updateAllNodes(child, id, checked)
366
- );
367
- }
368
-
369
- return node;
370
- };
371
- const renderTree = (node: any, index: number) => {
372
- return (
373
- <div className=" ml-4 mt-4 flex flex-col gap-4">
374
- <TreeNode
375
- key={node[descId]}
376
- node={node}
377
- // handleMultiSelect={handleMultiSelect}
378
- selected={selected}
379
- isMultiple={isMultiple}
380
- singleSelect={singleSelect}
381
- idx={node[descId]}
382
- updateNode={updateNode}
383
- desc={desc}
384
- descId={descId}
385
- toggleExpand={toggleExpand}
386
- >
387
- {node.expanded &&
388
- node.children?.map((childNode: any, index: number) =>
389
- renderTree(childNode, index)
390
- )}
391
- </TreeNode>
392
- </div>
393
- );
394
- };
395
-
396
- return (
397
- <div className={fullWidth ? 'fullWidth' : 'autoWidth'} ref={dropdownRef}>
398
- {label && (
399
- <div
400
- style={{
401
- marginBottom: 5,
402
- display: 'flex',
403
- justifyContent: 'space-between',
404
- }}
405
- >
406
- <label className={`labels label-text`}>
407
- {label}
408
- {required && <span className="text-error"> *</span>}
409
- </label>
410
- <span onClick={() => handleAction?.()} className={`action_label`}>
411
- {actionLabel}
412
- </span>
413
- </div>
414
- )}
415
- {/* Displaying selected items for multi-select */}
416
-
417
- <div
418
- className={`${
419
- expandable ? 'qbs-expandable-container' : 'qbs-container'
420
- }`}
421
- style={{ position: 'relative' }}
422
- >
423
- {selectedItems?.length > 0 && (
424
- <>
425
- {selectedItems
426
- .slice(
427
- 0,
428
- selectedItems?.length > itemCount
429
- ? itemCount
430
- : selectedItems?.length
431
- )
432
- .map((item, index) => (
433
- <div key={index} className="selected-items-container">
434
- <Tooltip
435
- title={item?.[desc]}
436
- enabled={item?.[desc]?.length > textCount}
437
- >
438
- <div key={item.id} className="selected-item">
439
- {item?.[desc]?.length > textCount
440
- ? `${item?.[desc].substring(0, textCount)}...`
441
- : item?.[desc]}
442
-
443
- <button
444
- onClick={() => handleRemoveSelectedItem(index)}
445
- className="remove-item-btn"
446
- aria-label={`Remove ${item?.[desc]}`}
447
- >
448
- X
449
- </button>
450
- </div>
451
- </Tooltip>
452
- </div>
453
- ))}
454
- {selectedItems?.length > itemCount && (
455
- <div className="selected-items-container">
456
- <Tooltip title={tooltipContent} enabled={true}>
457
- <div className="selected-item-more">
458
- +{selectedItems?.length - itemCount} more
459
- </div>
460
- </Tooltip>
461
- </div>
462
- )}
463
- </>
464
- )}
465
-
466
- <div
467
- className={`qbs-textfield-expandable ${
468
- !expandable ? 'qbs-normal' : ''
469
- }`}
470
- data-value={
471
- selectedItems?.length > 0 || searchValue
472
- ? searchValue
473
- : placeholder ?? ''
474
- }
475
- onClick={() => onInputFocus()}
476
- >
477
- <input
478
- id={id}
479
- ref={inputRef}
480
- type="text"
481
- value={
482
- type === 'auto_suggestion' && !expandable
483
- ? inputValue
484
- : searchValue || inputValue
485
- }
486
- onChange={handleChange}
487
- // onBlur={handleBlur}
488
- onFocus={onFocus}
489
- onClick={() => handleOnClick()}
490
- className={generateClassName()}
491
- placeholder={selectedItems?.length > 0 ? '' : placeholder ?? ''}
492
- readOnly={
493
- readOnly ||
494
- type === 'custom_select' ||
495
- (type == 'auto_suggestion' && !expandable)
496
- }
497
- disabled={disabled}
498
- data-testid="custom-autocomplete"
499
- />
500
- </div>
501
-
502
- {/* Icons for Clearing Input or Toggling Dropdown */}
503
-
504
- <InputActions
505
- inputValue={inputValue}
506
- searchValue={searchValue}
507
- dropOpen={dropOpen}
508
- handleDropOpen={handleDropOpen}
509
- handleDropClose={handleDropClose}
510
- disabled={disabled}
511
- readOnly={readOnly}
512
- expandable={expandable}
513
- handleClear={handleClear}
514
- />
515
- {/* Displaying Loading Spinner */}
516
-
517
- {/* Suggestions Dropdown */}
518
-
519
- {dropOpen &&
520
- ReactDOM.createPortal(
521
- <ul
522
- ref={dropRef}
523
- style={{ ...dropdownStyle, minHeight: 192 }}
524
- className={`qbs-autocomplete-suggestions`}
525
- >
526
- {type == 'auto_suggestion' && !expandable && (
527
- <div
528
- style={{ position: 'relative' }}
529
- className="react-core-ts-search-container"
530
- >
531
- <span className="dropdown-search-icon">
532
- <Search />
533
- </span>
534
- <input
535
- className="dropdown-search-input"
536
- onChange={handleSuggestionChange}
537
- value={searchValue}
538
- placeholder="Search"
539
- />
540
- </div>
541
- )}
542
-
543
- <div
544
- className={`qbs-autocomplete-suggestions-sub ${
545
- isTreeDropdown ? ' gap-4 min-h-[184px]' : ''
546
- }`}
547
- >
548
- {dropDownData?.length > 0 ? (
549
- dropDownData.map((suggestion: any, idx: number) => (
550
- <TreeNode
551
- key={suggestion[descId]}
552
- node={suggestion}
553
- // isSelected={isSelected}
554
- // handleMultiSelect={handleMultiSelect}
555
- selected={selected}
556
- isMultiple={isMultiple}
557
- singleSelect={singleSelect}
558
- idx={suggestion[descId]}
559
- updateNode={updateNode}
560
- desc={desc}
561
- descId={descId}
562
- toggleExpand={toggleExpand}
563
- >
564
- {suggestion.expanded &&
565
- suggestion.children?.map(
566
- (childNode: any, index: number) =>
567
- renderTree(childNode, index)
568
- )}
569
- </TreeNode>
570
- ))
571
- ) : (
572
- <>
573
- {isLoading ? (
574
- <div
575
- style={{ display: 'flex', justifyContent: 'center' }}
576
- >
577
- <span>
578
- <Spinner />
579
- </span>
580
- </div>
581
- ) : (
582
- <li
583
- className="qbs-autocomplete-notfound"
584
- onClick={handleBlur}
585
- >
586
- {notDataMessage ?? 'No Results Found'}
587
- </li>
588
- )}
589
- </>
590
- )}
591
- </div>
592
- </ul>,
593
- document.body
594
- )}
595
- </div>
596
-
597
- {/* Displaying Validation Error */}
598
- {errors && (
599
- <div
600
- className="text-error text-error-label mt-[1px]"
601
- data-testid="autocomplete-error"
602
- >
603
- {errors.message}
604
- </div>
605
- )}
606
- </div>
607
- );
608
- }
609
- );
610
-
611
- export default React.memo(AutoCompleteWithTreeStructure);