react-core-ts 1.0.7 → 1.0.9

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.
@@ -0,0 +1,545 @@
1
+ import React, {
2
+ forwardRef,
3
+ useCallback,
4
+ useEffect,
5
+ useImperativeHandle,
6
+ useRef,
7
+ useState,
8
+ } from 'react';
9
+
10
+ import { AutoSuggestionInputProps } from './commontypes';
11
+ import { useSuggestions } from './utilities/autosuggestions';
12
+ import { debounce } from './utilities/debounce';
13
+ import { deepEqual } from './utilities/deepEqual';
14
+ import { default as Tooltip } from './utilities/expandableTootltip';
15
+ import { filterSuggestions } from './utilities/filterSuggestions';
16
+ import { Close, DropArrow, Search, Spinner } from './utilities/icons';
17
+
18
+ type ValueProps = {
19
+ [key: string]: string;
20
+ };
21
+ const ExpandableAutoComplete = forwardRef<
22
+ HTMLInputElement,
23
+ AutoSuggestionInputProps
24
+ >(
25
+ (
26
+ {
27
+ label,
28
+ onChange,
29
+ getData = async () => [],
30
+ data = [],
31
+ errors,
32
+ required = false,
33
+ name,
34
+ fullWidth = false,
35
+ placeholder,
36
+ id,
37
+ type = 'custom_select',
38
+ selectedItems: propsSeelctedItems = [],
39
+ readOnly = false,
40
+ disabled = false,
41
+ value,
42
+ isMultiple = false,
43
+ desc = 'name',
44
+ descId = 'id',
45
+ singleSelect,
46
+ className,
47
+ async = false,
48
+ paginationEnabled,
49
+ initialLoad,
50
+ actionLabel,
51
+ handleAction,
52
+ nextBlock,
53
+ notDataMessage,
54
+ onFocus,
55
+ expandable = false,
56
+ textCount = 10,
57
+ itemCount = 1,
58
+ },
59
+ ref
60
+ ) => {
61
+ const dropdownRef = useRef<HTMLDivElement>(null);
62
+ // State Hooks Section
63
+ const [isInitialRender, setIsInitialRender] = useState(true);
64
+
65
+ const [inputValue, setInputValue] = useState<string>(value);
66
+ const [searchValue, setSearchValue] = useState<string>('');
67
+ const [nextPage, setNextPage] = useState<number | undefined>(1);
68
+ const [dropOpen, setDropOpen] = useState<boolean>(false);
69
+ const [selectedItems, setSelectedItems] = useState<ValueProps[]>([]);
70
+ // API call for suggestions through a custom hook
71
+ const [dropdownPosition, setDropdownPosition] = useState('bottom');
72
+ const inputRef = useRef(null);
73
+ const dropRef = useRef(null);
74
+ useImperativeHandle(ref, () => inputRef.current);
75
+
76
+ const adjustDropdownPosition = () => {
77
+ if (inputRef.current && dropRef.current) {
78
+ const inputBoxRect = inputRef.current.getBoundingClientRect();
79
+ const viewportHeight = window.innerHeight;
80
+
81
+ const spaceAbove = inputBoxRect.top;
82
+ const spaceBelow = viewportHeight - inputBoxRect.bottom;
83
+ if (spaceAbove > spaceBelow) {
84
+ setDropdownPosition('top');
85
+ } else {
86
+ setDropdownPosition('bottom');
87
+ }
88
+ }
89
+ };
90
+
91
+ useEffect(() => {
92
+ window.addEventListener('resize', adjustDropdownPosition);
93
+ adjustDropdownPosition();
94
+
95
+ return () => {
96
+ window.removeEventListener('resize', adjustDropdownPosition);
97
+ };
98
+ }, [dropOpen]);
99
+
100
+ const { suggestions, isLoading, handlePickSuggestions } = useSuggestions(
101
+ getData,
102
+ data,
103
+ dropOpen,
104
+ async,
105
+ paginationEnabled,
106
+ initialLoad,
107
+ inputValue,
108
+ isMultiple,
109
+ setNextPage,
110
+ selectedItems
111
+ // nextBlock
112
+ );
113
+
114
+ // Handling the selection of a suggestion
115
+ const handleSuggestionClick = useCallback((suggestion: ValueProps) => {
116
+ if (isMultiple) {
117
+ setSelectedItems((prev) => [...prev, suggestion]);
118
+ } else {
119
+ setInputValue(suggestion[desc]);
120
+ }
121
+ setSearchValue('');
122
+ setInputValue(suggestion[desc]);
123
+ onChange(suggestion);
124
+ setDropOpen(false);
125
+ }, []);
126
+
127
+ // Adding debounce to avoid making API calls on every keystroke
128
+ const handleChangeWithDebounce = debounce((value) => {
129
+ if ((type === 'auto_complete' || type === 'auto_suggestion') && async) {
130
+ handlePickSuggestions(value, 1);
131
+ }
132
+ }, 1000);
133
+
134
+ const handleMultiSelect = (e: any, suggestion: ValueProps) => {
135
+ const { checked } = e.target;
136
+ if (isMultiple) {
137
+ if (checked) {
138
+ setSelectedItems((prev) => [...prev, suggestion]);
139
+ } else {
140
+ setSelectedItems((prev) => {
141
+ return prev.filter(
142
+ (item, i) => item[descId] !== suggestion[descId]
143
+ );
144
+ });
145
+ }
146
+ } else {
147
+ if (checked) {
148
+ setInputValue(suggestion[desc]);
149
+ onChange(suggestion);
150
+ } else {
151
+ setInputValue('');
152
+ }
153
+ }
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: React.MouseEvent) => {
219
+ if (
220
+ dropdownRef.current &&
221
+ event.target instanceof Node &&
222
+ !dropdownRef.current.contains(event.target)
223
+ ) {
224
+ setDropOpen(false);
225
+ setSearchValue('');
226
+ }
227
+ };
228
+ document.addEventListener('mousedown', handleClickOutside as any);
229
+ return () => {
230
+ document.removeEventListener('mousedown', handleClickOutside as any);
231
+ };
232
+ }, []);
233
+
234
+ // Filtering suggestions based on type and search value
235
+ const selected: any = isMultiple ? selectedItems : inputValue;
236
+ const filteredData: ValueProps[] = filterSuggestions(
237
+ suggestions,
238
+ searchValue,
239
+ type,
240
+ desc,
241
+ selected,
242
+ async
243
+ );
244
+ const isSelected = (
245
+ item: ValueProps,
246
+ selectedItems: ValueProps[] | string
247
+ ): boolean => {
248
+ if (Array.isArray(selectedItems)) {
249
+ return selectedItems.some(
250
+ (selectedItem) => selectedItem[desc] === item[desc]
251
+ );
252
+ } else {
253
+ return item[desc] === selectedItems;
254
+ }
255
+ };
256
+
257
+ const handleLoadMore = () => {
258
+ if (paginationEnabled) {
259
+ handlePickSuggestions(searchValue, nextPage + 1, true);
260
+ setNextPage(nextPage + 1);
261
+ }
262
+ };
263
+
264
+ const handleOnClick = () => {
265
+ !disabled && !readOnly ? setDropOpen(true) : '';
266
+ };
267
+ const onInputFocus = () => {
268
+ if (inputRef.current) inputRef.current.focus();
269
+ handleOnClick();
270
+ };
271
+ const tooltipContent =
272
+ selectedItems?.length > itemCount
273
+ ? selectedItems
274
+ ?.slice(itemCount)
275
+ .map((item) => item[desc])
276
+ .join(', ')
277
+ : '';
278
+
279
+ return (
280
+ <div className={fullWidth ? 'fullWidth' : 'autoWidth'} ref={dropdownRef}>
281
+ {label && (
282
+ <div
283
+ style={{
284
+ marginBottom: 5,
285
+ display: 'flex',
286
+ justifyContent: 'space-between',
287
+ }}
288
+ >
289
+ <label className={`labels label-text`}>
290
+ {label}
291
+ {required && <span className="text-error"> *</span>}
292
+ </label>
293
+ <span onClick={() => handleAction?.()} className={`action_label`}>
294
+ {actionLabel}
295
+ </span>
296
+ </div>
297
+ )}
298
+ {/* Displaying selected items for multi-select */}
299
+
300
+ <div
301
+ className={`${
302
+ expandable ? 'qbs-expandable-container' : 'qbs-container'
303
+ }`}
304
+ style={{ position: 'relative' }}
305
+ >
306
+ {selectedItems?.length > 0 && (
307
+ <>
308
+ {selectedItems
309
+ .slice(
310
+ 0,
311
+ selectedItems?.length > itemCount
312
+ ? itemCount
313
+ : selectedItems?.length
314
+ )
315
+ .map((item, index) => (
316
+ <>
317
+ <div className="selected-items-container">
318
+ <Tooltip
319
+ title={item?.[desc]}
320
+ enabled={item?.[desc]?.length > textCount}
321
+ >
322
+ <div key={item.id} className="selected-item">
323
+ {item?.[desc]?.length > textCount
324
+ ? `${item?.[desc].substring(0, textCount)}...`
325
+ : item?.[desc]}
326
+
327
+ <button
328
+ onClick={() => handleRemoveSelectedItem(index)}
329
+ className="remove-item-btn"
330
+ aria-label={`Remove ${item?.[desc]}`}
331
+ >
332
+ X
333
+ </button>
334
+ </div>
335
+ </Tooltip>
336
+ </div>
337
+ </>
338
+ ))}
339
+ {selectedItems?.length > itemCount && (
340
+ <div className="selected-items-container">
341
+ <Tooltip title={tooltipContent} enabled={true}>
342
+ <div className="selected-item-more">
343
+ +{selectedItems?.length - itemCount} more
344
+ </div>
345
+ </Tooltip>
346
+ </div>
347
+ )}
348
+ </>
349
+ )}
350
+
351
+ <div
352
+ className={`qbs-textfield-expandable ${
353
+ !expandable ? 'qbs-normal' : ''
354
+ }`}
355
+ data-value={
356
+ selectedItems?.length > 0 || searchValue
357
+ ? searchValue
358
+ : placeholder ?? ''
359
+ }
360
+ onClick={() => onInputFocus()}
361
+ >
362
+ <input
363
+ id={id}
364
+ ref={inputRef}
365
+ type="text"
366
+ value={
367
+ type === 'auto_suggestion' && !expandable
368
+ ? inputValue
369
+ : searchValue || inputValue
370
+ }
371
+ onChange={handleChange}
372
+ // onBlur={handleBlur}
373
+ onFocus={onFocus}
374
+ onClick={() => handleOnClick()}
375
+ className={generateClassName()}
376
+ placeholder={selectedItems?.length > 0 ? '' : placeholder ?? ''}
377
+ readOnly={
378
+ readOnly ||
379
+ type === 'custom_select' ||
380
+ (type == 'auto_suggestion' && !expandable)
381
+ }
382
+ disabled={disabled}
383
+ data-testid="custom-autocomplete"
384
+ />
385
+ </div>
386
+
387
+ {/* Icons for Clearing Input or Toggling Dropdown */}
388
+ {!expandable && (
389
+ <div className="qbs-autocomplete-close-icon">
390
+ {(inputValue || searchValue) && !disabled && !readOnly && (
391
+ <button
392
+ onClick={handleClear}
393
+ className="icon-button"
394
+ aria-label="clear"
395
+ >
396
+ <Close />
397
+ </button>
398
+ )}
399
+
400
+ <button
401
+ disabled={disabled || readOnly}
402
+ type="button"
403
+ onClick={() => setDropOpen(!dropOpen)}
404
+ className="icon-button"
405
+ aria-label="toggle"
406
+ >
407
+ <DropArrow />
408
+ </button>
409
+ </div>
410
+ )}
411
+
412
+ {/* Displaying Loading Spinner */}
413
+
414
+ {/* Suggestions Dropdown */}
415
+ {dropOpen && (
416
+ <ul
417
+ ref={dropRef}
418
+ className={`qbs-autocomplete-suggestions ${dropdownPosition}`}
419
+ >
420
+ {type == 'auto_suggestion' && !expandable && (
421
+ <div
422
+ style={{ position: 'relative' }}
423
+ className="react-core-ts-search-container"
424
+ >
425
+ <span className="dropdown-search-icon">
426
+ <Search />
427
+ </span>
428
+ <input
429
+ className="dropdown-search-input"
430
+ onChange={handleSuggestionChange}
431
+ value={searchValue}
432
+ placeholder="Search"
433
+ />
434
+ </div>
435
+ )}
436
+
437
+ <div className="qbs-autocomplete-suggestions-sub">
438
+ {/* Displaying Suggestions or Not Found Message */}
439
+ {/* {isLoading && (
440
+ <div style={{ display: 'flex', justifyContent: 'center' }}>
441
+ <span>
442
+ <Spinner />
443
+ </span>
444
+ </div>
445
+
446
+ {/* )} */}
447
+
448
+ {filteredData?.length > 0 ? (
449
+ filteredData.map((suggestion: ValueProps, idx: number) => (
450
+ <div
451
+ key={idx.toString()}
452
+ className={`qbs-autocomplete-listitem-container ${
453
+ (isMultiple || singleSelect) &&
454
+ 'qbs-autocomplete-checkbox-container'
455
+ } ${
456
+ isSelected(suggestion, selected) ? 'is-selected' : ''
457
+ }`}
458
+ >
459
+ {(isMultiple || singleSelect) && (
460
+ <div className="qbs-autocomplete-checkbox">
461
+ <input
462
+ onChange={(e) => handleMultiSelect(e, suggestion)}
463
+ type="checkbox"
464
+ checked={isSelected(suggestion, selected)}
465
+ id={`qbs-checkbox-${idx.toString()}`}
466
+ />
467
+ <label htmlFor={`qbs-checkbox-${idx.toString()}`}>
468
+ <svg
469
+ width="8"
470
+ height="6"
471
+ viewBox="0 0 8 6"
472
+ fill="none"
473
+ xmlns="http://www.w3.org/2000/svg"
474
+ >
475
+ <path
476
+ d="M0 3.21739L2.89883 6L8 1.06994L6.89494 0L2.89883 3.86768L1.09728 2.14745L0 3.21739Z"
477
+ fill="white"
478
+ />
479
+ </svg>
480
+ </label>
481
+ </div>
482
+ )}
483
+ <li
484
+ key={idx}
485
+ className={`qbs-autocomplete-suggestions-item ${
486
+ isSelected(suggestion, selected) ? 'is-selected' : ''
487
+ }`}
488
+ onClick={() => handleSuggestionClick(suggestion)}
489
+ data-testid={suggestion[desc]}
490
+ >
491
+ {suggestion[desc]}
492
+ </li>
493
+ </div>
494
+ ))
495
+ ) : (
496
+ <>
497
+ {isLoading ? (
498
+ <div
499
+ style={{ display: 'flex', justifyContent: 'center' }}
500
+ >
501
+ <span>
502
+ <Spinner />
503
+ </span>
504
+ </div>
505
+ ) : (
506
+ <li
507
+ className="qbs-autocomplete-notfound"
508
+ onClick={handleBlur}
509
+ >
510
+ {notDataMessage ?? 'No Results Found'}
511
+ </li>
512
+ )}
513
+ </>
514
+ )}
515
+ {paginationEnabled &&
516
+ nextBlock !== 0 &&
517
+ nextBlock !== undefined &&
518
+ filteredData?.length > 0 && (
519
+ <div
520
+ className="loadMoreSection"
521
+ onClick={() => handleLoadMore()}
522
+ >
523
+ <p style={{ margin: 2 }}>Load More</p>
524
+ </div>
525
+ )}
526
+ </div>
527
+ </ul>
528
+ )}
529
+ </div>
530
+
531
+ {/* Displaying Validation Error */}
532
+ {errors && (
533
+ <div
534
+ className="text-error text-error-label mt-[1px]"
535
+ data-testid="autocomplete-error"
536
+ >
537
+ {errors.message}
538
+ </div>
539
+ )}
540
+ </div>
541
+ );
542
+ }
543
+ );
544
+
545
+ export default React.memo(ExpandableAutoComplete);
@@ -21,8 +21,8 @@ export interface AutoSuggestionInputProps {
21
21
  readOnly?: boolean;
22
22
  disabled?: boolean;
23
23
  isMultiple?: boolean;
24
- desc: string;
25
- descId: string;
24
+ desc?: string;
25
+ descId?: string;
26
26
  singleSelect?: boolean;
27
27
  className?: string;
28
28
  async?: boolean;
@@ -35,4 +35,7 @@ export interface AutoSuggestionInputProps {
35
35
  selectedItems?: any[];
36
36
  onFocus?: (e: React.FocusEvent<HTMLInputElement, Element>) => void;
37
37
  dopDownRef?: HTMLInputElement;
38
+ expandable?: boolean
39
+ textCount?: number
40
+ itemCount?: number
38
41
  }
package/src/index.tsx CHANGED
@@ -2,6 +2,8 @@ import './styles/global.css';
2
2
 
3
3
  export { default as AutoComplete } from './AutoComplete';
4
4
  export { default as CustomAutoComplete } from './AutoComplete';
5
+ export { default as ExpandableAutoComplete } from './ExpandableAutoComplete';
6
+
5
7
  export { default as ToolTip } from './utilities/tooltip';
6
8
  export { deepEqual } from './utilities/deepEqual';
7
9
  export type { AutoSuggestionInputProps } from './commontypes';
@@ -181,6 +181,55 @@ body * {
181
181
  color: #313131;
182
182
  }
183
183
 
184
+ .qbs-expandable-container {
185
+ border: 1px solid #d6d8dc;
186
+ background: #fff;
187
+ box-sizing: border-box;
188
+ padding: 5px;
189
+ display: flex;
190
+ flex-wrap: wrap;
191
+ flex: 1 1 0%;
192
+ gap: 5px;
193
+ }
194
+
195
+ .qbs-textfield-default.expandable {
196
+ height: auto;
197
+ border: none;
198
+ }
199
+
200
+ .qbs-expandable-container .qbs-textfield-expandable {
201
+ flex: 1 1 auto;
202
+ display: inline-grid;
203
+ grid-template-columns: 0px min-content;
204
+ grid-area: 1 / 1 / 2 / 3;
205
+ overflow: hidden;
206
+ }
207
+ .qbs-expandable-container .qbs-textfield-expandable::after {
208
+ content: attr(data-value) " ";
209
+ visibility: hidden;
210
+ white-space: pre;
211
+ grid-area: 1 / 2;
212
+ font: inherit;
213
+ min-width: 2px;
214
+ border: 0px;
215
+ margin: 0px;
216
+ outline: 0px;
217
+ padding: 0px;
218
+ }
219
+ .qbs-expandable-container .qbs-textfield-expandable input {
220
+ opacity: 1;
221
+ width: 100%;
222
+ grid-area: 1 / 2;
223
+ min-width: 10px;
224
+ border: 0px;
225
+ margin: 0px;
226
+ outline: 0px;
227
+ padding: 0px;
228
+ height: auto;
229
+ min-height: auto;
230
+ border-radius: 0;
231
+ }
232
+
184
233
  .qbs-textfield-default::placeholder {
185
234
  font-size: 14px;
186
235
  font-style: normal;
@@ -290,6 +339,12 @@ ul {
290
339
  flex-wrap: wrap;
291
340
  }
292
341
 
342
+ .qbs-expandable-container .selected-items-container {
343
+ position: relative;
344
+ top: 0;
345
+ left: 0;
346
+ }
347
+
293
348
  .selected-item-more {
294
349
  display: flex;
295
350
  align-items: center;
@@ -467,7 +522,7 @@ span.dropdown-search-icon {
467
522
  cursor: pointer;
468
523
  }
469
524
 
470
- .qbs-table-tooltip .tooltiptext {
525
+ .qbs-table-tooltip .tooltiptext, .tooltiptext {
471
526
  visibility: hidden;
472
527
  background-color: #000;
473
528
  color: #fff;
@@ -501,21 +556,21 @@ span.dropdown-search-icon {
501
556
  opacity: 1;
502
557
  }
503
558
 
504
- .qbs-table-tooltip .tooltiptext::after {
505
- content: '';
559
+ .qbs-table-tooltip .tooltiptext::after, .tooltiptext::after {
560
+ content: "";
506
561
  position: absolute;
507
562
  border-width: 5px;
508
563
  border-style: solid;
509
564
  }
510
565
 
511
- .qbs-table-tooltip.up .tooltiptext::after {
566
+ .qbs-table-tooltip.up .tooltiptext::after, .up.tooltiptext::after {
512
567
  top: 100%;
513
568
  left: 50%;
514
569
  margin-left: -5px;
515
570
  border-color: #000 transparent transparent transparent;
516
571
  }
517
572
 
518
- .qbs-table-tooltip.down .tooltiptext::after {
573
+ .qbs-table-tooltip.down .tooltiptext::after, .down.tooltiptext::after {
519
574
  bottom: 100%;
520
575
  left: 50%;
521
576
  margin-left: -5px;