react-core-ts 1.0.2 → 1.0.3
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/.babel.config.js +7 -0
- package/.npmingnore +3 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/styles/components/autocomplete.css +1 -1
- package/dist/utilities/tooltip.js +0 -10
- package/dist/utilities/tooltip.js.map +1 -1
- package/docs/App.tsx +214 -0
- package/docs/index.html +40 -0
- package/docs/index.tsx +12 -0
- package/docs/style.css +4 -0
- package/package.json +1 -5
- package/postcss.config.js +7 -0
- package/src/AutoComplete.tsx +489 -0
- package/src/commontypes.ts +38 -0
- package/src/index.html +1 -0
- package/src/index.tsx +7 -0
- package/src/utilities/autosuggestions.ts +83 -0
- package/src/utilities/debounce.ts +15 -0
- package/src/utilities/deepEqual.ts +54 -0
- package/src/utilities/filterSuggestions.ts +46 -0
- package/src/utilities/icons.tsx +84 -0
- package/src/utilities/tooltip.tsx +52 -0
- package/tsconfig.json +14 -0
- package/webpack.config.js +66 -0
- package/dist/bundle.js +0 -2
- package/dist/bundle.js.LICENSE.txt +0 -14
- package/dist/utilities/tootltip.d.ts +0 -3
- package/dist/utilities/tootltip.js +0 -74
- package/dist/utilities/tootltip.js.map +0 -1
- /package/{dist/styles → src}/styles/components/autocomplete.css +0 -0
- /package/{dist/styles → src}/styles/global.css +0 -0
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
import Tooltip from './utilities/tooltip';
|
|
2
|
+
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
|
3
|
+
|
|
4
|
+
import { AutoSuggestionInputProps } from './commontypes';
|
|
5
|
+
import { useSuggestions } from './utilities/autosuggestions';
|
|
6
|
+
import { debounce } from './utilities/debounce';
|
|
7
|
+
import { deepEqual } from './utilities/deepEqual';
|
|
8
|
+
import { filterSuggestions } from './utilities/filterSuggestions';
|
|
9
|
+
import { Close, DropArrow, Search, Spinner } from './utilities/icons';
|
|
10
|
+
|
|
11
|
+
type ValueProps = {
|
|
12
|
+
[key: string]: string;
|
|
13
|
+
};
|
|
14
|
+
const AutoComplete = forwardRef<HTMLInputElement, AutoSuggestionInputProps>(
|
|
15
|
+
(
|
|
16
|
+
{
|
|
17
|
+
label,
|
|
18
|
+
onChange,
|
|
19
|
+
getData = async () => [],
|
|
20
|
+
data = [],
|
|
21
|
+
errors,
|
|
22
|
+
required = false,
|
|
23
|
+
name,
|
|
24
|
+
fullWidth = false,
|
|
25
|
+
placeholder,
|
|
26
|
+
id,
|
|
27
|
+
type = 'custom_select',
|
|
28
|
+
selectedItems: propsSeelctedItems = [],
|
|
29
|
+
readOnly = false,
|
|
30
|
+
disabled = false,
|
|
31
|
+
value,
|
|
32
|
+
isMultiple = false,
|
|
33
|
+
desc = 'name',
|
|
34
|
+
descId = 'id',
|
|
35
|
+
singleSelect,
|
|
36
|
+
className,
|
|
37
|
+
async = false,
|
|
38
|
+
paginationEnabled,
|
|
39
|
+
initialLoad,
|
|
40
|
+
actionLabel,
|
|
41
|
+
handleAction,
|
|
42
|
+
nextBlock,
|
|
43
|
+
notDataMessage,
|
|
44
|
+
onFocus,
|
|
45
|
+
},
|
|
46
|
+
ref
|
|
47
|
+
) => {
|
|
48
|
+
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
49
|
+
// State Hooks Section
|
|
50
|
+
const [isInitialRender, setIsInitialRender] = useState(true);
|
|
51
|
+
|
|
52
|
+
const [inputValue, setInputValue] = useState<string>(value);
|
|
53
|
+
const [searchValue, setSearchValue] = useState<string>('');
|
|
54
|
+
const [nextPage, setNextPage] = useState<number | undefined>(1);
|
|
55
|
+
const [dropOpen, setDropOpen] = useState<boolean>(false);
|
|
56
|
+
const [selectedItems, setSelectedItems] = useState<ValueProps[]>([]);
|
|
57
|
+
// API call for suggestions through a custom hook
|
|
58
|
+
const [dropdownPosition, setDropdownPosition] = useState('bottom');
|
|
59
|
+
const inputRef = useRef(null);
|
|
60
|
+
const dropRef = useRef(null);
|
|
61
|
+
useImperativeHandle(ref, () => inputRef.current);
|
|
62
|
+
|
|
63
|
+
const adjustDropdownPosition = () => {
|
|
64
|
+
if (inputRef.current && dropRef.current) {
|
|
65
|
+
const inputBoxRect = inputRef.current.getBoundingClientRect();
|
|
66
|
+
const viewportHeight = window.innerHeight;
|
|
67
|
+
|
|
68
|
+
const spaceAbove = inputBoxRect.top;
|
|
69
|
+
const spaceBelow = viewportHeight - inputBoxRect.bottom;
|
|
70
|
+
if (spaceAbove > spaceBelow) {
|
|
71
|
+
setDropdownPosition('top');
|
|
72
|
+
} else {
|
|
73
|
+
setDropdownPosition('bottom');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
window.addEventListener('resize', adjustDropdownPosition);
|
|
80
|
+
adjustDropdownPosition();
|
|
81
|
+
|
|
82
|
+
return () => {
|
|
83
|
+
window.removeEventListener('resize', adjustDropdownPosition);
|
|
84
|
+
};
|
|
85
|
+
}, [dropOpen]);
|
|
86
|
+
|
|
87
|
+
const { suggestions, isLoading, handlePickSuggestions } = useSuggestions(
|
|
88
|
+
getData,
|
|
89
|
+
data,
|
|
90
|
+
dropOpen,
|
|
91
|
+
async,
|
|
92
|
+
paginationEnabled,
|
|
93
|
+
initialLoad,
|
|
94
|
+
inputValue,
|
|
95
|
+
isMultiple,
|
|
96
|
+
setNextPage,
|
|
97
|
+
selectedItems
|
|
98
|
+
// nextBlock
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// Handling the selection of a suggestion
|
|
102
|
+
const handleSuggestionClick = useCallback((suggestion: ValueProps) => {
|
|
103
|
+
if (isMultiple) {
|
|
104
|
+
setSelectedItems((prev) => [...prev, suggestion]);
|
|
105
|
+
} else {
|
|
106
|
+
setInputValue(suggestion[desc]);
|
|
107
|
+
}
|
|
108
|
+
setSearchValue('');
|
|
109
|
+
setInputValue(suggestion[desc]);
|
|
110
|
+
onChange(suggestion);
|
|
111
|
+
setDropOpen(false);
|
|
112
|
+
}, []);
|
|
113
|
+
|
|
114
|
+
// Adding debounce to avoid making API calls on every keystroke
|
|
115
|
+
const handleChangeWithDebounce = debounce((value) => {
|
|
116
|
+
if ((type === 'auto_complete' || type === 'auto_suggestion') && async) {
|
|
117
|
+
handlePickSuggestions(value, 1);
|
|
118
|
+
}
|
|
119
|
+
}, 1000);
|
|
120
|
+
|
|
121
|
+
const handleMultiSelect = (e: any, suggestion: ValueProps) => {
|
|
122
|
+
const { checked } = e.target;
|
|
123
|
+
if (isMultiple) {
|
|
124
|
+
if (checked) {
|
|
125
|
+
setSelectedItems((prev) => [...prev, suggestion]);
|
|
126
|
+
} else {
|
|
127
|
+
setSelectedItems((prev) => {
|
|
128
|
+
return prev.filter(
|
|
129
|
+
(item, i) => item[descId] !== suggestion[descId]
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
if (checked) {
|
|
135
|
+
setInputValue(suggestion[desc]);
|
|
136
|
+
onChange(suggestion);
|
|
137
|
+
} else {
|
|
138
|
+
setInputValue('');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
if (!deepEqual(selectedItems, propsSeelctedItems))
|
|
144
|
+
setSelectedItems(propsSeelctedItems);
|
|
145
|
+
}, [propsSeelctedItems]);
|
|
146
|
+
// Effect to set the input value whenever `value` prop changes
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
setInputValue(value ?? '');
|
|
149
|
+
}, [value]);
|
|
150
|
+
|
|
151
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
152
|
+
const { value } = e.target;
|
|
153
|
+
setDropOpen(true);
|
|
154
|
+
setSearchValue(value);
|
|
155
|
+
handleChangeWithDebounce(value);
|
|
156
|
+
if (!value) {
|
|
157
|
+
setInputValue('');
|
|
158
|
+
onChange({ [descId]: '', [desc]: '' });
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
const handleSuggestionChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
162
|
+
const { value } = e.target;
|
|
163
|
+
setDropOpen(true);
|
|
164
|
+
setSearchValue(value);
|
|
165
|
+
handleChangeWithDebounce(value);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const handleBlur = () => {
|
|
169
|
+
setTimeout(() => {
|
|
170
|
+
setDropOpen(false);
|
|
171
|
+
}, 200);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const handleClear = () => {
|
|
175
|
+
if (searchValue) {
|
|
176
|
+
setSearchValue('');
|
|
177
|
+
setDropOpen(false);
|
|
178
|
+
} else {
|
|
179
|
+
setInputValue('');
|
|
180
|
+
onChange({ [descId]: '', [desc]: '' });
|
|
181
|
+
setDropOpen(false);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const generateClassName = useCallback(() => {
|
|
186
|
+
return `qbs-textfield-default ${className} ${
|
|
187
|
+
errors && errors?.message ? 'textfield-error' : 'textfield'
|
|
188
|
+
}`;
|
|
189
|
+
}, [errors, name]);
|
|
190
|
+
const handleRemoveSelectedItem = (index: number) => {
|
|
191
|
+
setSelectedItems((prev) => {
|
|
192
|
+
return prev.filter((_, i) => i !== index);
|
|
193
|
+
});
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
useEffect(() => {
|
|
197
|
+
if (isInitialRender) {
|
|
198
|
+
setIsInitialRender(false);
|
|
199
|
+
} else {
|
|
200
|
+
onChange(selectedItems);
|
|
201
|
+
}
|
|
202
|
+
}, [selectedItems]);
|
|
203
|
+
|
|
204
|
+
useEffect(() => {
|
|
205
|
+
const handleClickOutside = (event: React.MouseEvent) => {
|
|
206
|
+
if (
|
|
207
|
+
dropdownRef.current &&
|
|
208
|
+
event.target instanceof Node &&
|
|
209
|
+
!dropdownRef.current.contains(event.target)
|
|
210
|
+
) {
|
|
211
|
+
setDropOpen(false);
|
|
212
|
+
setSearchValue('');
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
document.addEventListener('mousedown', handleClickOutside as any);
|
|
216
|
+
return () => {
|
|
217
|
+
document.removeEventListener('mousedown', handleClickOutside as any);
|
|
218
|
+
};
|
|
219
|
+
}, []);
|
|
220
|
+
|
|
221
|
+
// Filtering suggestions based on type and search value
|
|
222
|
+
const selected: any = isMultiple ? selectedItems : inputValue;
|
|
223
|
+
const filteredData: ValueProps[] = filterSuggestions(
|
|
224
|
+
suggestions,
|
|
225
|
+
searchValue,
|
|
226
|
+
type,
|
|
227
|
+
desc,
|
|
228
|
+
selected,
|
|
229
|
+
async
|
|
230
|
+
);
|
|
231
|
+
const isSelected = (
|
|
232
|
+
item: ValueProps,
|
|
233
|
+
selectedItems: ValueProps[] | string
|
|
234
|
+
): boolean => {
|
|
235
|
+
if (Array.isArray(selectedItems)) {
|
|
236
|
+
return selectedItems.some(
|
|
237
|
+
(selectedItem) => selectedItem[desc] === item[desc]
|
|
238
|
+
);
|
|
239
|
+
} else {
|
|
240
|
+
return item[desc] === selectedItems;
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const handleLoadMore = () => {
|
|
245
|
+
if (paginationEnabled) {
|
|
246
|
+
handlePickSuggestions(searchValue, nextPage + 1, true);
|
|
247
|
+
setNextPage(nextPage + 1);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const handleOnClick = () => {
|
|
252
|
+
!disabled && !readOnly ? setDropOpen(true) : '';
|
|
253
|
+
};
|
|
254
|
+
const tooltipContent =
|
|
255
|
+
selectedItems?.length > 1
|
|
256
|
+
? selectedItems
|
|
257
|
+
?.slice(1)
|
|
258
|
+
.map((item) => item[desc])
|
|
259
|
+
.join(', ')
|
|
260
|
+
: '';
|
|
261
|
+
|
|
262
|
+
return (
|
|
263
|
+
<div className={fullWidth ? 'fullWidth' : 'autoWidth'} ref={dropdownRef}>
|
|
264
|
+
{label && (
|
|
265
|
+
<div
|
|
266
|
+
style={{
|
|
267
|
+
marginBottom: 5,
|
|
268
|
+
display: 'flex',
|
|
269
|
+
justifyContent: 'space-between',
|
|
270
|
+
}}
|
|
271
|
+
>
|
|
272
|
+
<label className={`labels label-text`}>
|
|
273
|
+
{label}
|
|
274
|
+
{required && <span className="text-error"> *</span>}
|
|
275
|
+
</label>
|
|
276
|
+
<span onClick={() => handleAction?.()} className={`action_label`}>
|
|
277
|
+
{actionLabel}
|
|
278
|
+
</span>
|
|
279
|
+
</div>
|
|
280
|
+
)}
|
|
281
|
+
{/* Displaying selected items for multi-select */}
|
|
282
|
+
|
|
283
|
+
<div style={{ position: 'relative' }}>
|
|
284
|
+
<div className="selected-items-container ">
|
|
285
|
+
{selectedItems?.length > 0 && (
|
|
286
|
+
<>
|
|
287
|
+
<div key={selectedItems[0].id} className="selected-item">
|
|
288
|
+
<Tooltip title={selectedItems[0]?.[desc]}>
|
|
289
|
+
{selectedItems[0]?.[desc]?.length > 8
|
|
290
|
+
? `${selectedItems[0]?.[desc].substring(0, 8)}...`
|
|
291
|
+
: selectedItems[0]?.[desc]}
|
|
292
|
+
</Tooltip>
|
|
293
|
+
<button
|
|
294
|
+
onClick={() => handleRemoveSelectedItem(0)}
|
|
295
|
+
className="remove-item-btn"
|
|
296
|
+
aria-label={`Remove ${selectedItems[0]?.[desc]}`}
|
|
297
|
+
>
|
|
298
|
+
X
|
|
299
|
+
</button>
|
|
300
|
+
</div>
|
|
301
|
+
<Tooltip title={tooltipContent}>
|
|
302
|
+
{selectedItems?.length > 1 && (
|
|
303
|
+
<div className="selected-item-more">
|
|
304
|
+
+{selectedItems?.length - 1} more
|
|
305
|
+
</div>
|
|
306
|
+
)}
|
|
307
|
+
</Tooltip>
|
|
308
|
+
</>
|
|
309
|
+
)}
|
|
310
|
+
</div>
|
|
311
|
+
<input
|
|
312
|
+
id={id}
|
|
313
|
+
ref={inputRef}
|
|
314
|
+
type="text"
|
|
315
|
+
value={
|
|
316
|
+
type === 'auto_suggestion'
|
|
317
|
+
? inputValue
|
|
318
|
+
: searchValue || inputValue
|
|
319
|
+
}
|
|
320
|
+
onChange={handleChange}
|
|
321
|
+
// onBlur={handleBlur}
|
|
322
|
+
onFocus={onFocus}
|
|
323
|
+
onClick={() => handleOnClick()}
|
|
324
|
+
className={generateClassName()}
|
|
325
|
+
placeholder={selectedItems?.length > 0 ? '' : placeholder ?? ''}
|
|
326
|
+
readOnly={
|
|
327
|
+
readOnly || type === 'custom_select' || type == 'auto_suggestion'
|
|
328
|
+
}
|
|
329
|
+
disabled={disabled}
|
|
330
|
+
data-testid="custom-autocomplete"
|
|
331
|
+
/>
|
|
332
|
+
|
|
333
|
+
{/* Icons for Clearing Input or Toggling Dropdown */}
|
|
334
|
+
<div className="qbs-autocomplete-close-icon">
|
|
335
|
+
{(inputValue || searchValue) && !disabled && !readOnly && (
|
|
336
|
+
<button
|
|
337
|
+
onClick={handleClear}
|
|
338
|
+
className="icon-button"
|
|
339
|
+
aria-label="clear"
|
|
340
|
+
>
|
|
341
|
+
<Close />
|
|
342
|
+
</button>
|
|
343
|
+
)}
|
|
344
|
+
|
|
345
|
+
<button
|
|
346
|
+
disabled={disabled || readOnly}
|
|
347
|
+
type="button"
|
|
348
|
+
onClick={() => setDropOpen(!dropOpen)}
|
|
349
|
+
className="icon-button"
|
|
350
|
+
aria-label="toggle"
|
|
351
|
+
>
|
|
352
|
+
<DropArrow />
|
|
353
|
+
</button>
|
|
354
|
+
</div>
|
|
355
|
+
|
|
356
|
+
{/* Displaying Loading Spinner */}
|
|
357
|
+
|
|
358
|
+
{/* Suggestions Dropdown */}
|
|
359
|
+
{dropOpen && (
|
|
360
|
+
<ul
|
|
361
|
+
ref={dropRef}
|
|
362
|
+
className={`qbs-autocomplete-suggestions ${dropdownPosition}`}
|
|
363
|
+
>
|
|
364
|
+
{type == 'auto_suggestion' && (
|
|
365
|
+
<div
|
|
366
|
+
style={{ position: 'relative' }}
|
|
367
|
+
className="react-core-ts-search-container"
|
|
368
|
+
>
|
|
369
|
+
<span className="dropdown-search-icon">
|
|
370
|
+
<Search />
|
|
371
|
+
</span>
|
|
372
|
+
<input
|
|
373
|
+
className="dropdown-search-input"
|
|
374
|
+
onChange={handleSuggestionChange}
|
|
375
|
+
value={searchValue}
|
|
376
|
+
placeholder="Search"
|
|
377
|
+
/>
|
|
378
|
+
</div>
|
|
379
|
+
)}
|
|
380
|
+
|
|
381
|
+
<div className="qbs-autocomplete-suggestions-sub">
|
|
382
|
+
{/* Displaying Suggestions or Not Found Message */}
|
|
383
|
+
{/* {isLoading && (
|
|
384
|
+
<div style={{ display: 'flex', justifyContent: 'center' }}>
|
|
385
|
+
<span>
|
|
386
|
+
<Spinner />
|
|
387
|
+
</span>
|
|
388
|
+
</div>
|
|
389
|
+
|
|
390
|
+
{/* )} */}
|
|
391
|
+
|
|
392
|
+
{filteredData?.length > 0 ? (
|
|
393
|
+
filteredData.map((suggestion: ValueProps, idx: number) => (
|
|
394
|
+
<div
|
|
395
|
+
key={idx.toString()}
|
|
396
|
+
className={`qbs-autocomplete-listitem-container ${
|
|
397
|
+
(isMultiple || singleSelect) &&
|
|
398
|
+
'qbs-autocomplete-checkbox-container'
|
|
399
|
+
} ${
|
|
400
|
+
isSelected(suggestion, selected) ? 'is-selected' : ''
|
|
401
|
+
}`}
|
|
402
|
+
>
|
|
403
|
+
{(isMultiple || singleSelect) && (
|
|
404
|
+
<div className="qbs-autocomplete-checkbox">
|
|
405
|
+
<input
|
|
406
|
+
onChange={(e) => handleMultiSelect(e, suggestion)}
|
|
407
|
+
type="checkbox"
|
|
408
|
+
checked={isSelected(suggestion, selected)}
|
|
409
|
+
id={`qbs-checkbox-${idx.toString()}`}
|
|
410
|
+
/>
|
|
411
|
+
<label htmlFor={`qbs-checkbox-${idx.toString()}`}>
|
|
412
|
+
<svg
|
|
413
|
+
width="8"
|
|
414
|
+
height="6"
|
|
415
|
+
viewBox="0 0 8 6"
|
|
416
|
+
fill="none"
|
|
417
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
418
|
+
>
|
|
419
|
+
<path
|
|
420
|
+
d="M0 3.21739L2.89883 6L8 1.06994L6.89494 0L2.89883 3.86768L1.09728 2.14745L0 3.21739Z"
|
|
421
|
+
fill="white"
|
|
422
|
+
/>
|
|
423
|
+
</svg>
|
|
424
|
+
</label>
|
|
425
|
+
</div>
|
|
426
|
+
)}
|
|
427
|
+
<li
|
|
428
|
+
key={idx}
|
|
429
|
+
className={`qbs-autocomplete-suggestions-item ${
|
|
430
|
+
isSelected(suggestion, selected) ? 'is-selected' : ''
|
|
431
|
+
}`}
|
|
432
|
+
onClick={() => handleSuggestionClick(suggestion)}
|
|
433
|
+
data-testid={suggestion[desc]}
|
|
434
|
+
>
|
|
435
|
+
{suggestion[desc]}
|
|
436
|
+
</li>
|
|
437
|
+
</div>
|
|
438
|
+
))
|
|
439
|
+
) : (
|
|
440
|
+
<>
|
|
441
|
+
{isLoading ? (
|
|
442
|
+
<div
|
|
443
|
+
style={{ display: 'flex', justifyContent: 'center' }}
|
|
444
|
+
>
|
|
445
|
+
<span>
|
|
446
|
+
<Spinner />
|
|
447
|
+
</span>
|
|
448
|
+
</div>
|
|
449
|
+
) : (
|
|
450
|
+
<li
|
|
451
|
+
className="qbs-autocomplete-notfound"
|
|
452
|
+
onClick={handleBlur}
|
|
453
|
+
>
|
|
454
|
+
{notDataMessage ?? 'No Results Found'}
|
|
455
|
+
</li>
|
|
456
|
+
)}
|
|
457
|
+
</>
|
|
458
|
+
)}
|
|
459
|
+
{paginationEnabled &&
|
|
460
|
+
nextBlock !== 0 &&
|
|
461
|
+
nextBlock !== undefined &&
|
|
462
|
+
filteredData?.length > 0 && (
|
|
463
|
+
<div
|
|
464
|
+
className="loadMoreSection"
|
|
465
|
+
onClick={() => handleLoadMore()}
|
|
466
|
+
>
|
|
467
|
+
<p style={{ margin: 2 }}>Load More</p>
|
|
468
|
+
</div>
|
|
469
|
+
)}
|
|
470
|
+
</div>
|
|
471
|
+
</ul>
|
|
472
|
+
)}
|
|
473
|
+
</div>
|
|
474
|
+
|
|
475
|
+
{/* Displaying Validation Error */}
|
|
476
|
+
{errors && (
|
|
477
|
+
<div
|
|
478
|
+
className="text-error text-error-label mt-[1px]"
|
|
479
|
+
data-testid="autocomplete-error"
|
|
480
|
+
>
|
|
481
|
+
{errors.message}
|
|
482
|
+
</div>
|
|
483
|
+
)}
|
|
484
|
+
</div>
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
export default React.memo(AutoComplete);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
type ValueProps = {
|
|
2
|
+
[key: string]: string;
|
|
3
|
+
};
|
|
4
|
+
export interface AutoSuggestionInputProps {
|
|
5
|
+
id?: string;
|
|
6
|
+
label?: string;
|
|
7
|
+
fullWidth?: boolean;
|
|
8
|
+
required?: boolean;
|
|
9
|
+
value?: string;
|
|
10
|
+
onChange: (value?: ValueProps | ValueProps[]) => void;
|
|
11
|
+
data?: ValueProps[];
|
|
12
|
+
type?:
|
|
13
|
+
| 'custom_select'
|
|
14
|
+
| 'auto_complete'
|
|
15
|
+
| 'custom_search_select'
|
|
16
|
+
| 'auto_suggestion';
|
|
17
|
+
placeholder?: string;
|
|
18
|
+
getData?: (key?: string, nextBlock?: number) => any;
|
|
19
|
+
errors?: any;
|
|
20
|
+
name: string;
|
|
21
|
+
readOnly?: boolean;
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
isMultiple?: boolean;
|
|
24
|
+
desc: string;
|
|
25
|
+
descId: string;
|
|
26
|
+
singleSelect?: boolean;
|
|
27
|
+
className?: string;
|
|
28
|
+
async?: boolean;
|
|
29
|
+
paginationEnabled?: boolean;
|
|
30
|
+
nextBlock?: number;
|
|
31
|
+
initialLoad?: boolean;
|
|
32
|
+
handleAction?: () => void;
|
|
33
|
+
actionLabel?: string;
|
|
34
|
+
notDataMessage?: string;
|
|
35
|
+
selectedItems?: any[];
|
|
36
|
+
onFocus?: (e: React.FocusEvent<HTMLInputElement, Element>) => void;
|
|
37
|
+
dopDownRef?: HTMLInputElement;
|
|
38
|
+
}
|
package/src/index.html
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<div>sdfdsf</div>
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import './styles/global.css';
|
|
2
|
+
|
|
3
|
+
export { default as AutoComplete } from './AutoComplete';
|
|
4
|
+
export { default as CustomAutoComplete } from './AutoComplete';
|
|
5
|
+
export { default as ToolTip } from './utilities/tooltip';
|
|
6
|
+
export { deepEqual } from './utilities/deepEqual';
|
|
7
|
+
export type { AutoSuggestionInputProps } from './commontypes';
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
type ValueProps = {
|
|
4
|
+
[key: string]: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
type UseSuggestionsType = (
|
|
8
|
+
getData: (key?: string, next?: number) => Promise<ValueProps[]>,
|
|
9
|
+
initialData?: ValueProps[],
|
|
10
|
+
dropOpen?: boolean,
|
|
11
|
+
asyncFetch?: boolean,
|
|
12
|
+
paginationEnabled?: boolean,
|
|
13
|
+
initialLoad?: boolean,
|
|
14
|
+
inputValue?: string,
|
|
15
|
+
isMultiple?: boolean,
|
|
16
|
+
setNextPage?: (value: number) => void,
|
|
17
|
+
selectedItems?: any[]
|
|
18
|
+
) => {
|
|
19
|
+
suggestions: ValueProps[];
|
|
20
|
+
isLoading: boolean;
|
|
21
|
+
|
|
22
|
+
handlePickSuggestions: (
|
|
23
|
+
value?: string,
|
|
24
|
+
next?: number,
|
|
25
|
+
append?: boolean
|
|
26
|
+
) => Promise<void>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const useSuggestions: UseSuggestionsType = (
|
|
30
|
+
getData,
|
|
31
|
+
initialData = [],
|
|
32
|
+
dropOpen = false,
|
|
33
|
+
asyncFetch = false,
|
|
34
|
+
paginationEnabled = false,
|
|
35
|
+
initialLoad = false,
|
|
36
|
+
inputValue = '',
|
|
37
|
+
isMultiple,
|
|
38
|
+
setNextPage,
|
|
39
|
+
selectedItems = []
|
|
40
|
+
) => {
|
|
41
|
+
const [suggestions, setSuggestions] = useState<ValueProps[]>(initialData);
|
|
42
|
+
const [isLoading, setIsLoading] = useState<boolean>(false);
|
|
43
|
+
|
|
44
|
+
const handlePickSuggestions = async (
|
|
45
|
+
value?: string,
|
|
46
|
+
nextPage?: number,
|
|
47
|
+
appendData?: boolean
|
|
48
|
+
) => {
|
|
49
|
+
setIsLoading(true);
|
|
50
|
+
try {
|
|
51
|
+
const data = await (asyncFetch || initialLoad
|
|
52
|
+
? getData(value, nextPage)
|
|
53
|
+
: Promise.resolve(initialData));
|
|
54
|
+
if (!data || data?.length === 0) {
|
|
55
|
+
setNextPage(undefined);
|
|
56
|
+
}
|
|
57
|
+
const newSuggestions = appendData ? [...suggestions, ...data] : data;
|
|
58
|
+
setSuggestions(newSuggestions);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error('Error fetching data:', error);
|
|
61
|
+
} finally {
|
|
62
|
+
setIsLoading(false);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (dropOpen) {
|
|
68
|
+
if (!isMultiple && (!inputValue || suggestions.length === 0)) {
|
|
69
|
+
setNextPage(1);
|
|
70
|
+
handlePickSuggestions('', paginationEnabled ? 1 : undefined);
|
|
71
|
+
} else if (
|
|
72
|
+
isMultiple &&
|
|
73
|
+
(suggestions.length === 0 ||
|
|
74
|
+
!selectedItems ||
|
|
75
|
+
selectedItems?.length === 0)
|
|
76
|
+
) {
|
|
77
|
+
handlePickSuggestions('', paginationEnabled ? 1 : undefined);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}, [dropOpen]);
|
|
81
|
+
|
|
82
|
+
return { suggestions, isLoading, handlePickSuggestions };
|
|
83
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function debounce<F extends (...args: any[]) => any>(
|
|
2
|
+
fn: F,
|
|
3
|
+
delay: number
|
|
4
|
+
): (...args: Parameters<F>) => void {
|
|
5
|
+
let timerId: NodeJS.Timeout | null = null;
|
|
6
|
+
return (...args: Parameters<F>): void => {
|
|
7
|
+
if (timerId) {
|
|
8
|
+
clearTimeout(timerId);
|
|
9
|
+
}
|
|
10
|
+
timerId = setTimeout(() => {
|
|
11
|
+
fn(...args);
|
|
12
|
+
timerId = null;
|
|
13
|
+
}, delay);
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export function deepEqual(value1: any, value2: any): boolean {
|
|
2
|
+
// Check for strict equality first
|
|
3
|
+
if (value1 === value2) {
|
|
4
|
+
return true;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// If either is null (but not both), they're not equal
|
|
8
|
+
if (value1 === null || value2 === null) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// If types differ, they're not equal
|
|
13
|
+
if (typeof value1 !== typeof value2) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Check objects (including arrays)
|
|
18
|
+
if (typeof value1 === 'object' && typeof value2 === 'object') {
|
|
19
|
+
// Different constructors means they are different objects
|
|
20
|
+
if (value1.constructor !== value2.constructor) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (Array.isArray(value1)) {
|
|
25
|
+
// Array length comparison
|
|
26
|
+
if (value1.length !== value2.length) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
// Deep compare each element
|
|
30
|
+
for (let i = 0; i < value1.length; i++) {
|
|
31
|
+
if (!deepEqual(value1[i], value2[i])) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
} else {
|
|
37
|
+
// Compare object keys and values
|
|
38
|
+
const keys1 = Object.keys(value1);
|
|
39
|
+
const keys2 = new Set(Object.keys(value2));
|
|
40
|
+
if (keys1.length !== keys2.size) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
for (const key of keys1) {
|
|
44
|
+
if (!keys2.has(key) || !deepEqual(value1[key], value2[key])) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// If none of the above, they're not equal
|
|
53
|
+
return false;
|
|
54
|
+
}
|