@pushwoosh/dumb-components 1.1.142 → 1.1.144

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,10 +1,11 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useCallback, useId, useMemo, useRef, useState } from 'react';
3
- import { DEFAULT_DEBOUNCE_MS, FieldInput, FieldShell, defaultGetKey, useActiveIndex, useDropdownItems, useFieldOpenEffects, useListNavigation, useShellMouseDown } from '../shared/dropdownField';
3
+ import { DEFAULT_DEBOUNCE_MS, FieldInput, FieldShell, defaultGetKey, resolveDropdownText, useActiveIndex, useDropdownItems, useFieldOpenEffects, useListNavigation, useShellMouseDown } from '../shared/dropdownField';
4
4
  function FreeCombobox({
5
5
  items,
6
6
  loadItems,
7
7
  debounceMs = DEFAULT_DEBOUNCE_MS,
8
+ minChars,
8
9
  value,
9
10
  onChange,
10
11
  onClose,
@@ -12,13 +13,14 @@ function FreeCombobox({
12
13
  getLabel: getLabelProp,
13
14
  filter,
14
15
  renderItem,
15
- renderEmpty,
16
+ emptyText,
17
+ loadingText,
16
18
  placeholder,
17
19
  disabled,
18
20
  loading,
19
21
  clearable,
20
22
  autosize,
21
- $isErrored,
23
+ hasError,
22
24
  width,
23
25
  minWidth
24
26
  }) {
@@ -29,11 +31,14 @@ function FreeCombobox({
29
31
  const [isOpen, setIsOpen] = useState(false);
30
32
  const {
31
33
  filteredItems,
32
- loading: asyncLoading
34
+ loading: asyncLoading,
35
+ loadingMore,
36
+ loadMore
33
37
  } = useDropdownItems({
34
38
  items,
35
39
  loadItems,
36
40
  debounceMs,
41
+ minChars,
37
42
  getLabel,
38
43
  filter,
39
44
  query: value,
@@ -46,7 +51,9 @@ function FreeCombobox({
46
51
  setActiveIndex(-1);
47
52
  }, [setActiveIndex]);
48
53
  const pick = useCallback(item => {
49
- onChange(getLabel(item));
54
+ onChange(getLabel(item), {
55
+ source: 'select'
56
+ });
50
57
  setIsOpen(false);
51
58
  setActiveIndex(-1);
52
59
  }, [onChange, getLabel, setActiveIndex]);
@@ -57,7 +64,9 @@ function FreeCombobox({
57
64
  activeIndex,
58
65
  setActiveIndex,
59
66
  onSelectIndex: index => pick(filteredItems[index]),
60
- onEscape: close
67
+ onEscape: close,
68
+ onSubmit: close,
69
+ onReachEnd: loadMore
61
70
  });
62
71
  const onShellMouseDown = useShellMouseDown(inputRef, isOpen, open);
63
72
  useFieldOpenEffects({
@@ -82,18 +91,22 @@ function FreeCombobox({
82
91
  onSelect: () => pick(item)
83
92
  };
84
93
  }), [filteredItems, activeIndex, value, getLabel, renderItem, pick, baseId]);
85
- const dropdownVisible = isOpen && filteredItems.length > 0;
94
+ const isLoading = loading || asyncLoading;
95
+ const hasSource = items !== undefined || loadItems !== undefined;
96
+ const dropdownVisible = isOpen && (filteredItems.length > 0 || isLoading || hasSource);
86
97
  const activeDescendant = activeIndex >= 0 && activeIndex < filteredItems.length ? `${baseId}-opt-${filteredItems[activeIndex]}` : undefined;
87
98
  const onInputChange = event => {
88
- onChange(event.target.value);
99
+ onChange(event.target.value, {
100
+ source: 'input'
101
+ });
89
102
  if (!isOpen) open();
90
103
  };
91
104
  return _jsx(FieldShell, {
92
105
  isOpen: isOpen,
93
106
  dropdownOpen: dropdownVisible,
94
- isErrored: $isErrored,
107
+ isErrored: hasError,
95
108
  disabled: disabled,
96
- loading: loading || asyncLoading,
109
+ loading: isLoading,
97
110
  width: width,
98
111
  minWidth: minWidth,
99
112
  autosize: autosize,
@@ -102,11 +115,16 @@ function FreeCombobox({
102
115
  showClear: !!(clearable && value),
103
116
  onClear: () => {
104
117
  var _inputRef$current;
105
- onChange('');
118
+ onChange('', {
119
+ source: 'input'
120
+ });
106
121
  (_inputRef$current = inputRef.current) === null || _inputRef$current === void 0 || _inputRef$current.focus();
107
122
  },
108
123
  dropdownItems: dropdownItems,
109
- emptyText: renderEmpty ? renderEmpty(value) : undefined,
124
+ emptyText: resolveDropdownText(emptyText, value),
125
+ loadingText: resolveDropdownText(loadingText, value),
126
+ loadingMore: loadingMore,
127
+ onLoadMore: loadMore,
110
128
  listboxId: listboxId,
111
129
  children: _jsx(FieldInput, {
112
130
  ref: inputRef,
@@ -129,6 +147,7 @@ function SelectCombobox({
129
147
  items,
130
148
  loadItems,
131
149
  debounceMs = DEFAULT_DEBOUNCE_MS,
150
+ minChars,
132
151
  value,
133
152
  onChange,
134
153
  onClose,
@@ -137,13 +156,14 @@ function SelectCombobox({
137
156
  getKey: getKeyProp,
138
157
  filter,
139
158
  renderItem,
140
- renderEmpty,
159
+ emptyText,
160
+ loadingText,
141
161
  placeholder,
142
162
  disabled,
143
163
  loading,
144
164
  clearable,
145
165
  autosize,
146
- $isErrored,
166
+ hasError,
147
167
  width,
148
168
  minWidth
149
169
  }) {
@@ -157,11 +177,14 @@ function SelectCombobox({
157
177
  const {
158
178
  sourceItems,
159
179
  filteredItems,
160
- loading: asyncLoading
180
+ loading: asyncLoading,
181
+ loadingMore,
182
+ loadMore
161
183
  } = useDropdownItems({
162
184
  items,
163
185
  loadItems,
164
186
  debounceMs,
187
+ minChars,
165
188
  getLabel,
166
189
  filter,
167
190
  query,
@@ -198,7 +221,8 @@ function SelectCombobox({
198
221
  activeIndex,
199
222
  setActiveIndex,
200
223
  onSelectIndex: index => commit(filteredItems[index]),
201
- onEscape: close
224
+ onEscape: close,
225
+ onReachEnd: loadMore
202
226
  });
203
227
  const onShellMouseDown = useShellMouseDown(inputRef, isOpen, open);
204
228
  useFieldOpenEffects({
@@ -228,7 +252,7 @@ function SelectCombobox({
228
252
  return _jsx(FieldShell, {
229
253
  isOpen: isOpen,
230
254
  dropdownOpen: isOpen,
231
- isErrored: $isErrored,
255
+ isErrored: hasError,
232
256
  disabled: disabled,
233
257
  loading: loading || asyncLoading,
234
258
  width: width,
@@ -243,7 +267,10 @@ function SelectCombobox({
243
267
  (_inputRef$current3 = inputRef.current) === null || _inputRef$current3 === void 0 || _inputRef$current3.focus();
244
268
  },
245
269
  dropdownItems: dropdownItems,
246
- emptyText: renderEmpty ? renderEmpty(query) : undefined,
270
+ emptyText: resolveDropdownText(emptyText, query),
271
+ loadingText: resolveDropdownText(loadingText, query),
272
+ loadingMore: loadingMore,
273
+ onLoadMore: loadMore,
247
274
  listboxId: listboxId,
248
275
  children: _jsx(FieldInput, {
249
276
  ref: inputRef,
@@ -1,2 +1,2 @@
1
1
  export { Combobox } from './Combobox';
2
- export type { ComboboxProps, FreeComboboxProps, SelectComboboxProps, } from './types';
2
+ export type { ComboboxProps, ComboboxChangeSource, ComboboxChangeMeta, FreeComboboxProps, SelectComboboxProps, } from './types';
@@ -1,18 +1,25 @@
1
1
  import type { ReactNode } from 'react';
2
- import type { GetKeyProp, ItemState } from '../shared/dropdownField';
2
+ import type { DropdownText, GetKeyProp, ItemState, LoadItems } from '../shared/dropdownField';
3
3
  type ComboboxSource<T> = {
4
4
  items?: T[];
5
5
  loadItems?: never;
6
6
  debounceMs?: never;
7
+ minChars?: never;
7
8
  } | {
8
9
  items?: never;
9
- loadItems: (query: string) => Promise<T[]>;
10
+ loadItems: LoadItems<T>;
10
11
  debounceMs?: number;
12
+ minChars?: number;
13
+ };
14
+ export type ComboboxChangeSource = 'input' | 'select';
15
+ export type ComboboxChangeMeta = {
16
+ source: ComboboxChangeSource;
11
17
  };
12
18
  type CommonProps<T> = {
13
19
  filter?: (item: T, query: string) => boolean;
14
20
  renderItem?: (item: T, state: ItemState) => ReactNode;
15
- renderEmpty?: (query: string) => ReactNode;
21
+ emptyText?: DropdownText;
22
+ loadingText?: DropdownText;
16
23
  onClose?: () => void;
17
24
  autoFocus?: boolean;
18
25
  placeholder?: string;
@@ -20,14 +27,14 @@ type CommonProps<T> = {
20
27
  loading?: boolean;
21
28
  clearable?: boolean;
22
29
  autosize?: boolean;
23
- $isErrored?: boolean;
30
+ hasError?: boolean;
24
31
  width?: string;
25
32
  minWidth?: string;
26
33
  };
27
34
  export type FreeComboboxProps = ComboboxSource<string> & CommonProps<string> & {
28
35
  select?: false;
29
36
  value: string;
30
- onChange: (value: string) => void;
37
+ onChange: (value: string, meta: ComboboxChangeMeta) => void;
31
38
  getLabel?: (item: string) => string;
32
39
  };
33
40
  export type SelectComboboxProps<T> = ComboboxSource<T> & GetKeyProp<T> & CommonProps<T> & {
@@ -1,3 +1,3 @@
1
1
  import { type ReactElement } from 'react';
2
2
  import { type ComboboxMultiProps } from './types';
3
- export declare function ComboboxMulti<T>({ items, loadItems, debounceMs, value, onChange, onClose, autoFocus, getLabel, getKey: getKeyProp, filter, renderItem, renderEmpty, creatable, onCreate, canCreate, renderCreateLabel, maxValues, placeholder, disabled, loading, clearable, autosize, $isErrored, width, minWidth, }: ComboboxMultiProps<T>): ReactElement;
3
+ export declare function ComboboxMulti<T>({ items, loadItems, debounceMs, minChars, value, onChange, onClose, autoFocus, getLabel, getKey: getKeyProp, filter, renderItem, emptyText, loadingText, creatable, onCreate, canCreate, renderCreateLabel, maxValues, placeholder, disabled, loading, clearable, autosize, hasError, width, minWidth, }: ComboboxMultiProps<T>): ReactElement;
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useCallback, useId, useMemo, useRef, useState } from 'react';
3
3
  import { Chip } from '../Chip';
4
- import { DEFAULT_DEBOUNCE_MS, FieldInput, FieldShell, defaultGetKey, useActiveIndex, useDropdownItems, useFieldOpenEffects, useListNavigation, useShellMouseDown } from '../shared/dropdownField';
4
+ import { DEFAULT_DEBOUNCE_MS, FieldInput, FieldShell, defaultGetKey, resolveDropdownText, useActiveIndex, useDropdownItems, useFieldOpenEffects, useListNavigation, useShellMouseDown } from '../shared/dropdownField';
5
5
  function defaultCanCreate(query, items, getLabel) {
6
6
  const trimmed = query.trim();
7
7
  if (!trimmed) return false;
@@ -12,6 +12,7 @@ export function ComboboxMulti({
12
12
  items,
13
13
  loadItems,
14
14
  debounceMs = DEFAULT_DEBOUNCE_MS,
15
+ minChars,
15
16
  value,
16
17
  onChange,
17
18
  onClose,
@@ -20,7 +21,8 @@ export function ComboboxMulti({
20
21
  getKey: getKeyProp,
21
22
  filter,
22
23
  renderItem,
23
- renderEmpty,
24
+ emptyText,
25
+ loadingText,
24
26
  creatable,
25
27
  onCreate,
26
28
  canCreate,
@@ -31,7 +33,7 @@ export function ComboboxMulti({
31
33
  loading,
32
34
  clearable,
33
35
  autosize,
34
- $isErrored,
36
+ hasError,
35
37
  width,
36
38
  minWidth
37
39
  }) {
@@ -48,12 +50,15 @@ export function ComboboxMulti({
48
50
  const {
49
51
  sourceItems,
50
52
  filteredItems,
51
- loading: asyncLoading
53
+ loading: asyncLoading,
54
+ loadingMore,
55
+ loadMore
52
56
  } = useDropdownItems({
53
57
  items,
54
58
  loadItems,
55
59
  localItems: createdItems,
56
60
  debounceMs,
61
+ minChars,
57
62
  getLabel,
58
63
  filter,
59
64
  query,
@@ -103,6 +108,7 @@ export function ComboboxMulti({
103
108
  setActiveIndex,
104
109
  onSelectIndex: index => toggle(visibleItems[index]),
105
110
  onEscape: close,
111
+ onReachEnd: loadMore,
106
112
  hasExtraRow: canCreateNow,
107
113
  onSelectExtra: create,
108
114
  isQueryEmpty: !query && value.length > 0,
@@ -155,7 +161,7 @@ export function ComboboxMulti({
155
161
  return _jsxs(FieldShell, {
156
162
  isOpen: isOpen,
157
163
  dropdownOpen: isOpen,
158
- isErrored: $isErrored,
164
+ isErrored: hasError,
159
165
  disabled: disabled,
160
166
  loading: loading || asyncLoading,
161
167
  width: width,
@@ -170,7 +176,10 @@ export function ComboboxMulti({
170
176
  showChevron: true,
171
177
  onToggle: toggleOpen,
172
178
  dropdownItems: dropdownItems,
173
- emptyText: renderEmpty ? renderEmpty(query) : undefined,
179
+ emptyText: resolveDropdownText(emptyText, query),
180
+ loadingText: resolveDropdownText(loadingText, query),
181
+ loadingMore: loadingMore,
182
+ onLoadMore: loadMore,
174
183
  createLabel: createLabel,
175
184
  createActive: createActive,
176
185
  onCreate: create,
@@ -1,5 +1,5 @@
1
1
  import type { ReactNode } from 'react';
2
- import type { GetKeyProp, ItemState, ItemsSource } from '../shared/dropdownField';
2
+ import type { DropdownText, GetKeyProp, ItemState, ItemsSource } from '../shared/dropdownField';
3
3
  type CreatableProp<T> = [T] extends [string] ? {
4
4
  creatable?: boolean;
5
5
  onCreate?: (query: string) => T;
@@ -18,7 +18,8 @@ export type ComboboxMultiProps<T> = ItemsSource<T> & GetKeyProp<T> & CreatablePr
18
18
  getLabel: (item: T) => string;
19
19
  filter?: (item: T, query: string) => boolean;
20
20
  renderItem?: (item: T, state: ItemState) => ReactNode;
21
- renderEmpty?: (query: string) => ReactNode;
21
+ emptyText?: DropdownText;
22
+ loadingText?: DropdownText;
22
23
  canCreate?: (query: string, items: T[]) => boolean;
23
24
  renderCreateLabel?: (query: string) => ReactNode;
24
25
  maxValues?: number;
@@ -27,7 +28,7 @@ export type ComboboxMultiProps<T> = ItemsSource<T> & GetKeyProp<T> & CreatablePr
27
28
  loading?: boolean;
28
29
  clearable?: boolean;
29
30
  autosize?: boolean;
30
- $isErrored?: boolean;
31
+ hasError?: boolean;
31
32
  width?: string;
32
33
  minWidth?: string;
33
34
  };
package/index.d.ts CHANGED
@@ -22,7 +22,7 @@ export { ReCaptcha, useRecaptcha, asyncScriptLoad } from './ReCaptcha';
22
22
  export { Spinner } from './Spinner';
23
23
  export { SortButton, type SortButtonParams } from './SortButton';
24
24
  export { Select, SelectAsync, CreatableSelect, SelectComponents, CLASS_NAME_PREFIX, type SelectBase, type SelectOption, type MultiSelectOptions, type SingleValue, type MultiValue, } from './Select';
25
- export { Combobox, type ComboboxProps, type FreeComboboxProps, type SelectComboboxProps, } from './Combobox';
25
+ export { Combobox, type ComboboxProps, type ComboboxChangeSource, type ComboboxChangeMeta, type FreeComboboxProps, type SelectComboboxProps, } from './Combobox';
26
26
  export { ComboboxMulti, type ComboboxMultiProps } from './ComboboxMulti';
27
27
  export { Switch } from './Switch';
28
28
  export { TextSkeleton, RectangularSkeleton, CircularSkeleton } from './Skeleton';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushwoosh/dumb-components",
3
- "version": "1.1.142",
3
+ "version": "1.1.144",
4
4
  "description": "React components to build Pushwoosh products",
5
5
  "main": "index.js",
6
6
  "module": "index.js",
@@ -31,7 +31,7 @@
31
31
  "@types/react": "^18.2.69",
32
32
  "@types/react-dom": "^18.2.22",
33
33
  "@types/react-transition-group": "^4.4.12",
34
- "@types/styled-components": "^5.1.34",
34
+ "@types/styled-components": "^5.1.36",
35
35
  "glob": "^11.0.3",
36
36
  "jest": "^30.2.0",
37
37
  "jest-environment-jsdom": "^30.2.0",
@@ -20,11 +20,14 @@ type FieldShellProps = {
20
20
  onToggle?: () => void;
21
21
  dropdownItems: DropdownItemDescriptor[];
22
22
  emptyText?: ReactNode;
23
+ loadingText?: ReactNode;
24
+ loadingMore?: boolean;
25
+ onLoadMore?: () => void;
23
26
  createLabel?: ReactNode;
24
27
  createActive?: boolean;
25
28
  onCreate?: () => void;
26
29
  listboxId?: string;
27
30
  createId?: string;
28
31
  };
29
- export declare function FieldShell({ children, isOpen, dropdownOpen, isErrored, disabled, loading, width, minWidth, autosize, tightLeft, onShellMouseDown, onClose, showClear, onClear, clearLabel, showChevron, onToggle, dropdownItems, emptyText, createLabel, createActive, onCreate, listboxId, createId, }: FieldShellProps): ReactElement;
32
+ export declare function FieldShell({ children, isOpen, dropdownOpen, isErrored, disabled, loading, width, minWidth, autosize, tightLeft, onShellMouseDown, onClose, showClear, onClear, clearLabel, showChevron, onToggle, dropdownItems, emptyText, loadingText, loadingMore, onLoadMore, createLabel, createActive, onCreate, listboxId, createId, }: FieldShellProps): ReactElement;
30
33
  export {};
@@ -24,6 +24,9 @@ export function FieldShell({
24
24
  onToggle,
25
25
  dropdownItems,
26
26
  emptyText,
27
+ loadingText,
28
+ loadingMore,
29
+ onLoadMore,
27
30
  createLabel,
28
31
  createActive,
29
32
  onCreate,
@@ -99,6 +102,10 @@ export function FieldShell({
99
102
  floatingStyles: floatingStyles,
100
103
  items: dropdownItems,
101
104
  emptyText: emptyText,
105
+ loading: loading,
106
+ loadingText: loadingText,
107
+ loadingMore: loadingMore,
108
+ onLoadMore: onLoadMore,
102
109
  createLabel: createLabel,
103
110
  createActive: createActive,
104
111
  onCreate: onCreate,
@@ -5,6 +5,10 @@ type SuggestionsDropdownProps = {
5
5
  floatingStyles: CSSProperties;
6
6
  items: DropdownItemDescriptor[];
7
7
  emptyText?: ReactNode;
8
+ loading?: boolean;
9
+ loadingText?: ReactNode;
10
+ loadingMore?: boolean;
11
+ onLoadMore?: () => void;
8
12
  createLabel?: ReactNode;
9
13
  createActive?: boolean;
10
14
  onCreate?: () => void;
@@ -12,5 +16,5 @@ type SuggestionsDropdownProps = {
12
16
  createId?: string;
13
17
  zIndex?: number;
14
18
  };
15
- export declare function SuggestionsDropdown({ floatingRef, floatingStyles, items, emptyText, createLabel, createActive, onCreate, listboxId, createId, zIndex, }: SuggestionsDropdownProps): ReactElement | null;
19
+ export declare function SuggestionsDropdown({ floatingRef, floatingStyles, items, emptyText, loading, loadingText, loadingMore, onLoadMore, createLabel, createActive, onCreate, listboxId, createId, zIndex, }: SuggestionsDropdownProps): ReactElement | null;
16
20
  export {};
@@ -1,13 +1,19 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useRef } from 'react';
3
3
  import { createPortal } from 'react-dom';
4
- import { CreateItemLabel, Dropdown, DropdownEmpty, DropdownItem } from './styles';
4
+ import { CreateItemLabel, Dropdown, DropdownEmpty, DropdownItem, DropdownLoadingMore } from './styles';
5
+ import { Spinner } from '../../Spinner';
6
+ const LOAD_MORE_THRESHOLD = 24;
5
7
  const CREATE_KEY = '__create__';
6
8
  export function SuggestionsDropdown({
7
9
  floatingRef,
8
10
  floatingStyles,
9
11
  items,
10
12
  emptyText = 'No options',
13
+ loading,
14
+ loadingText = 'Loading…',
15
+ loadingMore,
16
+ onLoadMore,
11
17
  createLabel,
12
18
  createActive,
13
19
  onCreate,
@@ -16,6 +22,11 @@ export function SuggestionsDropdown({
16
22
  zIndex
17
23
  }) {
18
24
  const activeItemRef = useRef(null);
25
+ const onScroll = event => {
26
+ if (!onLoadMore) return;
27
+ const el = event.currentTarget;
28
+ if (el.scrollHeight - el.scrollTop - el.clientHeight <= LOAD_MORE_THRESHOLD) onLoadMore();
29
+ };
19
30
  const activeItem = items.find(item => item.active);
20
31
  const activeKey = createActive ? CREATE_KEY : (activeItem === null || activeItem === void 0 ? void 0 : activeItem.key) ?? null;
21
32
  useEffect(() => {
@@ -26,7 +37,7 @@ export function SuggestionsDropdown({
26
37
  });
27
38
  }, [activeKey]);
28
39
  if (typeof document === 'undefined') return null;
29
- const isEmpty = items.length === 0 && !createLabel;
40
+ const isEmpty = !loading && items.length === 0 && !createLabel;
30
41
  return createPortal(_jsxs(Dropdown, {
31
42
  ref: floatingRef,
32
43
  style: {
@@ -35,7 +46,10 @@ export function SuggestionsDropdown({
35
46
  },
36
47
  id: listboxId,
37
48
  role: "listbox",
38
- children: [createLabel && _jsx(DropdownItem, {
49
+ onScroll: onScroll,
50
+ children: [loading && _jsx(DropdownEmpty, {
51
+ children: loadingText
52
+ }), !loading && createLabel && _jsx(DropdownItem, {
39
53
  ref: createActive ? activeItemRef : undefined,
40
54
  id: createId,
41
55
  role: "option",
@@ -48,7 +62,7 @@ export function SuggestionsDropdown({
48
62
  children: _jsx(CreateItemLabel, {
49
63
  children: createLabel
50
64
  })
51
- }), items.map(item => _jsx(DropdownItem, {
65
+ }), !loading && items.map(item => _jsx(DropdownItem, {
52
66
  ref: item.active ? activeItemRef : undefined,
53
67
  id: item.id,
54
68
  role: "option",
@@ -61,7 +75,11 @@ export function SuggestionsDropdown({
61
75
  (_item$onSelect = item.onSelect) === null || _item$onSelect === void 0 || _item$onSelect.call(item);
62
76
  },
63
77
  children: item.label
64
- }, item.key)), isEmpty && _jsx(DropdownEmpty, {
78
+ }, item.key)), !loading && loadingMore && _jsx(DropdownLoadingMore, {
79
+ children: _jsx(Spinner, {
80
+ size: "small"
81
+ })
82
+ }), isEmpty && _jsx(DropdownEmpty, {
65
83
  children: emptyText
66
84
  })]
67
85
  }), document.body);
@@ -8,6 +8,6 @@ export { useDropdownItems } from './useDropdownItems';
8
8
  export { useActiveIndex } from './useActiveIndex';
9
9
  export { useFieldOpenEffects } from './useFieldOpenEffects';
10
10
  export { useListNavigation } from './useListNavigation';
11
- export { defaultGetKey, defaultLabelFilter, useShellMouseDown } from './shared';
11
+ export { defaultGetKey, defaultLabelFilter, resolveDropdownText, useShellMouseDown, } from './shared';
12
12
  export { DEFAULT_DEBOUNCE_MS } from './constants';
13
- export type { ItemState, DropdownItemDescriptor, GetKeyProp, ItemsSource, } from './types';
13
+ export type { ItemState, DropdownItemDescriptor, DropdownText, GetKeyProp, ItemsSource, LoadItems, LoadItemsResult, } from './types';
@@ -8,5 +8,5 @@ export { useDropdownItems } from './useDropdownItems';
8
8
  export { useActiveIndex } from './useActiveIndex';
9
9
  export { useFieldOpenEffects } from './useFieldOpenEffects';
10
10
  export { useListNavigation } from './useListNavigation';
11
- export { defaultGetKey, defaultLabelFilter, useShellMouseDown } from './shared';
11
+ export { defaultGetKey, defaultLabelFilter, resolveDropdownText, useShellMouseDown } from './shared';
12
12
  export { DEFAULT_DEBOUNCE_MS } from './constants';
@@ -1,4 +1,6 @@
1
- import { type MouseEvent, type RefObject } from 'react';
1
+ import { type MouseEvent, type ReactNode, type RefObject } from 'react';
2
+ import { type DropdownText } from './types';
3
+ export declare function resolveDropdownText(text: DropdownText, query: string): ReactNode;
2
4
  export declare function defaultGetKey<T>(item: T): string | number;
3
5
  export declare function defaultLabelFilter<T>(item: T, query: string, getLabel: (item: T) => string): boolean;
4
6
  export declare function useShellMouseDown(inputRef: RefObject<HTMLInputElement>, isOpen: boolean, open: () => void): (event: MouseEvent<HTMLDivElement>) => void;
@@ -1,4 +1,7 @@
1
1
  import { useCallback } from 'react';
2
+ export function resolveDropdownText(text, query) {
3
+ return typeof text === 'function' ? text(query) : text;
4
+ }
2
5
  export function defaultGetKey(item) {
3
6
  return item;
4
7
  }
@@ -21,4 +21,5 @@ export declare const DropdownItem: import("styled-components").StyledComponent<"
21
21
  $selected?: boolean;
22
22
  }, never>;
23
23
  export declare const DropdownEmpty: import("styled-components").StyledComponent<"div", any, {}, never>;
24
+ export declare const DropdownLoadingMore: import("styled-components").StyledComponent<"div", any, {}, never>;
24
25
  export declare const CreateItemLabel: import("styled-components").StyledComponent<"span", any, {}, never>;
@@ -75,7 +75,11 @@ export const DropdownEmpty = styled.div.withConfig({
75
75
  displayName: "DropdownEmpty",
76
76
  componentId: "sc-1pvwhhw-8"
77
77
  })(["padding:6px 12px;", ";color:", ";"], textBase, Color.PHANTOM);
78
+ export const DropdownLoadingMore = styled.div.withConfig({
79
+ displayName: "DropdownLoadingMore",
80
+ componentId: "sc-1pvwhhw-9"
81
+ })(["display:flex;align-items:center;justify-content:center;padding:8px 12px;"]);
78
82
  export const CreateItemLabel = styled.span.withConfig({
79
83
  displayName: "CreateItemLabel",
80
- componentId: "sc-1pvwhhw-9"
84
+ componentId: "sc-1pvwhhw-10"
81
85
  })(["color:", ";"], Color.PRIMARY);
@@ -1,4 +1,5 @@
1
1
  import type { ReactNode } from 'react';
2
+ export type DropdownText = ReactNode | ((query: string) => ReactNode);
2
3
  export type ItemState = {
3
4
  selected: boolean;
4
5
  active: boolean;
@@ -8,14 +9,21 @@ export type GetKeyProp<T> = [T] extends [string | number] ? {
8
9
  } : {
9
10
  getKey: (item: T) => string | number;
10
11
  };
12
+ export type LoadItemsResult<T> = {
13
+ items: T[];
14
+ hasMore?: boolean;
15
+ };
16
+ export type LoadItems<T> = (query: string, page: number) => Promise<LoadItemsResult<T>>;
11
17
  export type ItemsSource<T> = {
12
18
  items: T[];
13
19
  loadItems?: never;
14
20
  debounceMs?: never;
21
+ minChars?: never;
15
22
  } | {
16
23
  items?: never;
17
- loadItems: (query: string) => Promise<T[]>;
24
+ loadItems: LoadItems<T>;
18
25
  debounceMs?: number;
26
+ minChars?: number;
19
27
  };
20
28
  export type DropdownItemDescriptor = {
21
29
  key: string | number;
@@ -1,11 +1,16 @@
1
+ import { type LoadItems } from './types';
1
2
  type Args<T> = {
2
- loadItems?: (query: string) => Promise<T[]>;
3
+ loadItems?: LoadItems<T>;
3
4
  query: string;
4
5
  isOpen: boolean;
5
6
  debounceMs?: number;
7
+ minChars?: number;
6
8
  };
7
- export declare function useAsyncItems<T>({ loadItems, query, isOpen, debounceMs, }: Args<T>): {
9
+ type Result<T> = {
8
10
  items: T[];
9
11
  loading: boolean;
12
+ loadingMore: boolean;
13
+ loadMore: () => void;
10
14
  };
15
+ export declare function useAsyncItems<T>({ loadItems, query, isOpen, debounceMs, minChars, }: Args<T>): Result<T>;
11
16
  export {};
@@ -1,46 +1,78 @@
1
- import { useEffect, useRef, useState } from 'react';
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
2
  import { DEFAULT_DEBOUNCE_MS } from './constants';
3
3
  import { useDebouncedValue } from './useDebouncedValue';
4
4
  export function useAsyncItems({
5
5
  loadItems,
6
6
  query,
7
7
  isOpen,
8
- debounceMs = DEFAULT_DEBOUNCE_MS
8
+ debounceMs = DEFAULT_DEBOUNCE_MS,
9
+ minChars = 0
9
10
  }) {
10
11
  const [items, setItems] = useState([]);
11
12
  const [loading, setLoading] = useState(false);
13
+ const [loadingMore, setLoadingMore] = useState(false);
12
14
  const debouncedQuery = useDebouncedValue(query, debounceMs);
13
15
  const loadItemsRef = useRef(loadItems);
14
16
  loadItemsRef.current = loadItems;
15
- const lastQueryRef = useRef(null);
17
+ const sessionRef = useRef(0);
18
+ const pageRef = useRef(0);
19
+ const queryRef = useRef('');
20
+ const loadingRef = useRef(false);
21
+ const hasMoreRef = useRef(false);
16
22
  useEffect(() => {
17
23
  const loader = loadItemsRef.current;
18
- if (!loader || !isOpen) {
19
- lastQueryRef.current = null;
20
- return undefined;
21
- }
22
- if (lastQueryRef.current === debouncedQuery) {
24
+ const belowMin = debouncedQuery.trim().length < minChars;
25
+ sessionRef.current += 1;
26
+ pageRef.current = 0;
27
+ loadingRef.current = false;
28
+ hasMoreRef.current = false;
29
+ setLoadingMore(false);
30
+ if (!loader || !isOpen || belowMin) {
23
31
  setLoading(false);
32
+ if (!loader || belowMin) setItems([]);
24
33
  return undefined;
25
34
  }
26
- let cancelled = false;
35
+ const session = sessionRef.current;
36
+ queryRef.current = debouncedQuery;
37
+ loadingRef.current = true;
27
38
  setLoading(true);
28
- Promise.resolve(loader(debouncedQuery)).then(result => {
29
- if (cancelled) return;
30
- lastQueryRef.current = debouncedQuery;
31
- setItems(result);
39
+ Promise.resolve(loader(debouncedQuery, 0)).then(result => {
40
+ if (session !== sessionRef.current) return;
41
+ setItems(result.items);
42
+ hasMoreRef.current = result.hasMore ?? false;
43
+ loadingRef.current = false;
32
44
  setLoading(false);
33
45
  }).catch(() => {
34
- if (cancelled) return;
46
+ if (session !== sessionRef.current) return;
47
+ loadingRef.current = false;
35
48
  setLoading(false);
36
49
  });
37
- return () => {
38
- cancelled = true;
39
- setLoading(false);
40
- };
41
- }, [debouncedQuery, isOpen]);
50
+ return undefined;
51
+ }, [debouncedQuery, isOpen, minChars]);
52
+ const loadMore = useCallback(() => {
53
+ const loader = loadItemsRef.current;
54
+ if (!loader || loadingRef.current || !hasMoreRef.current) return;
55
+ const session = sessionRef.current;
56
+ const nextPage = pageRef.current + 1;
57
+ loadingRef.current = true;
58
+ setLoadingMore(true);
59
+ Promise.resolve(loader(queryRef.current, nextPage)).then(result => {
60
+ if (session !== sessionRef.current) return;
61
+ pageRef.current = nextPage;
62
+ setItems(prev => [...prev, ...result.items]);
63
+ hasMoreRef.current = result.hasMore ?? false;
64
+ loadingRef.current = false;
65
+ setLoadingMore(false);
66
+ }).catch(() => {
67
+ if (session !== sessionRef.current) return;
68
+ loadingRef.current = false;
69
+ setLoadingMore(false);
70
+ });
71
+ }, []);
42
72
  return {
43
73
  items,
44
- loading
74
+ loading,
75
+ loadingMore,
76
+ loadMore
45
77
  };
46
78
  }
@@ -1,16 +1,21 @@
1
+ import { type LoadItems } from './types';
1
2
  type Args<T> = {
2
3
  items?: T[];
3
- loadItems?: (query: string) => Promise<T[]>;
4
+ loadItems?: LoadItems<T>;
4
5
  localItems?: T[];
5
6
  debounceMs?: number;
7
+ minChars?: number;
6
8
  getLabel: (item: T) => string;
7
9
  filter?: (item: T, query: string) => boolean;
8
10
  query: string;
9
11
  isOpen: boolean;
10
12
  };
11
- export declare function useDropdownItems<T>({ items, loadItems, localItems, debounceMs, getLabel, filter, query, isOpen, }: Args<T>): {
13
+ type Result<T> = {
12
14
  sourceItems: T[];
13
15
  filteredItems: T[];
14
16
  loading: boolean;
17
+ loadingMore: boolean;
18
+ loadMore: () => void;
15
19
  };
20
+ export declare function useDropdownItems<T>({ items, loadItems, localItems, debounceMs, minChars, getLabel, filter, query, isOpen, }: Args<T>): Result<T>;
16
21
  export {};
@@ -7,6 +7,7 @@ export function useDropdownItems({
7
7
  loadItems,
8
8
  localItems,
9
9
  debounceMs = DEFAULT_DEBOUNCE_MS,
10
+ minChars,
10
11
  getLabel,
11
12
  filter,
12
13
  query,
@@ -15,12 +16,15 @@ export function useDropdownItems({
15
16
  const isAsync = !!loadItems;
16
17
  const {
17
18
  items: loadedItems,
18
- loading
19
+ loading,
20
+ loadingMore,
21
+ loadMore
19
22
  } = useAsyncItems({
20
23
  loadItems,
21
24
  query,
22
25
  isOpen,
23
- debounceMs
26
+ debounceMs,
27
+ minChars
24
28
  });
25
29
  const sourceItems = useMemo(() => isAsync ? loadedItems : [...(items ?? []), ...(localItems ?? [])], [isAsync, loadedItems, items, localItems]);
26
30
  const filteredItems = useMemo(() => {
@@ -31,6 +35,8 @@ export function useDropdownItems({
31
35
  return {
32
36
  sourceItems,
33
37
  filteredItems,
34
- loading
38
+ loading,
39
+ loadingMore,
40
+ loadMore
35
41
  };
36
42
  }
@@ -7,10 +7,12 @@ type Args = {
7
7
  setActiveIndex: Dispatch<SetStateAction<number>>;
8
8
  onSelectIndex: (index: number) => void;
9
9
  onEscape: () => void;
10
+ onSubmit?: () => void;
11
+ onReachEnd?: () => void;
10
12
  hasExtraRow?: boolean;
11
13
  onSelectExtra?: () => void;
12
14
  isQueryEmpty?: boolean;
13
15
  onBackspaceEmpty?: () => void;
14
16
  };
15
- export declare function useListNavigation({ isOpen, open, itemCount, activeIndex, setActiveIndex, onSelectIndex, onEscape, hasExtraRow, onSelectExtra, isQueryEmpty, onBackspaceEmpty, }: Args): (event: KeyboardEvent<HTMLInputElement>) => void;
17
+ export declare function useListNavigation({ isOpen, open, itemCount, activeIndex, setActiveIndex, onSelectIndex, onEscape, onSubmit, onReachEnd, hasExtraRow, onSelectExtra, isQueryEmpty, onBackspaceEmpty, }: Args): (event: KeyboardEvent<HTMLInputElement>) => void;
16
18
  export {};
@@ -7,6 +7,8 @@ export function useListNavigation({
7
7
  setActiveIndex,
8
8
  onSelectIndex,
9
9
  onEscape,
10
+ onSubmit,
11
+ onReachEnd,
10
12
  hasExtraRow,
11
13
  onSelectExtra,
12
14
  isQueryEmpty,
@@ -16,7 +18,15 @@ export function useListNavigation({
16
18
  return useCallback(event => {
17
19
  if (event.key === 'ArrowDown') {
18
20
  event.preventDefault();
19
- if (!isOpen) open();else if (total > 0) setActiveIndex(prev => prev >= total - 1 ? 0 : prev + 1);
21
+ if (!isOpen) {
22
+ open();
23
+ return;
24
+ }
25
+ if (total > 0) {
26
+ const next = activeIndex >= total - 1 ? 0 : activeIndex + 1;
27
+ setActiveIndex(next);
28
+ if (next > 0 && next >= total - 1) onReachEnd === null || onReachEnd === void 0 || onReachEnd();
29
+ }
20
30
  return;
21
31
  }
22
32
  if (event.key === 'ArrowUp') {
@@ -25,7 +35,14 @@ export function useListNavigation({
25
35
  return;
26
36
  }
27
37
  if (event.key === 'Enter') {
28
- if (!isOpen || activeIndex < 0 || activeIndex >= total) return;
38
+ if (!isOpen) return;
39
+ if (activeIndex < 0 || activeIndex >= total) {
40
+ if (onSubmit) {
41
+ event.preventDefault();
42
+ onSubmit();
43
+ }
44
+ return;
45
+ }
29
46
  event.preventDefault();
30
47
  if (hasExtraRow && activeIndex === 0) onSelectExtra === null || onSelectExtra === void 0 || onSelectExtra();else onSelectIndex(hasExtraRow ? activeIndex - 1 : activeIndex);
31
48
  return;
@@ -41,5 +58,5 @@ export function useListNavigation({
41
58
  if (event.key === 'Backspace' && isQueryEmpty && onBackspaceEmpty) {
42
59
  onBackspaceEmpty();
43
60
  }
44
- }, [isOpen, open, total, activeIndex, setActiveIndex, onSelectIndex, onSelectExtra, onEscape, hasExtraRow, isQueryEmpty, onBackspaceEmpty]);
61
+ }, [isOpen, open, total, activeIndex, setActiveIndex, onSelectIndex, onSelectExtra, onEscape, onSubmit, onReachEnd, hasExtraRow, isQueryEmpty, onBackspaceEmpty]);
45
62
  }