react-core-ts 1.0.22 → 1.0.24

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