@pushwoosh/dumb-components 1.1.142 → 1.1.143

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,8 @@ function FreeCombobox({
57
64
  activeIndex,
58
65
  setActiveIndex,
59
66
  onSelectIndex: index => pick(filteredItems[index]),
60
- onEscape: close
67
+ onEscape: close,
68
+ onReachEnd: loadMore
61
69
  });
62
70
  const onShellMouseDown = useShellMouseDown(inputRef, isOpen, open);
63
71
  useFieldOpenEffects({
@@ -82,18 +90,22 @@ function FreeCombobox({
82
90
  onSelect: () => pick(item)
83
91
  };
84
92
  }), [filteredItems, activeIndex, value, getLabel, renderItem, pick, baseId]);
85
- const dropdownVisible = isOpen && filteredItems.length > 0;
93
+ const isLoading = loading || asyncLoading;
94
+ const hasSource = items !== undefined || loadItems !== undefined;
95
+ const dropdownVisible = isOpen && (filteredItems.length > 0 || isLoading || hasSource);
86
96
  const activeDescendant = activeIndex >= 0 && activeIndex < filteredItems.length ? `${baseId}-opt-${filteredItems[activeIndex]}` : undefined;
87
97
  const onInputChange = event => {
88
- onChange(event.target.value);
98
+ onChange(event.target.value, {
99
+ source: 'input'
100
+ });
89
101
  if (!isOpen) open();
90
102
  };
91
103
  return _jsx(FieldShell, {
92
104
  isOpen: isOpen,
93
105
  dropdownOpen: dropdownVisible,
94
- isErrored: $isErrored,
106
+ isErrored: hasError,
95
107
  disabled: disabled,
96
- loading: loading || asyncLoading,
108
+ loading: isLoading,
97
109
  width: width,
98
110
  minWidth: minWidth,
99
111
  autosize: autosize,
@@ -102,11 +114,16 @@ function FreeCombobox({
102
114
  showClear: !!(clearable && value),
103
115
  onClear: () => {
104
116
  var _inputRef$current;
105
- onChange('');
117
+ onChange('', {
118
+ source: 'input'
119
+ });
106
120
  (_inputRef$current = inputRef.current) === null || _inputRef$current === void 0 || _inputRef$current.focus();
107
121
  },
108
122
  dropdownItems: dropdownItems,
109
- emptyText: renderEmpty ? renderEmpty(value) : undefined,
123
+ emptyText: resolveDropdownText(emptyText, value),
124
+ loadingText: resolveDropdownText(loadingText, value),
125
+ loadingMore: loadingMore,
126
+ onLoadMore: loadMore,
110
127
  listboxId: listboxId,
111
128
  children: _jsx(FieldInput, {
112
129
  ref: inputRef,
@@ -129,6 +146,7 @@ function SelectCombobox({
129
146
  items,
130
147
  loadItems,
131
148
  debounceMs = DEFAULT_DEBOUNCE_MS,
149
+ minChars,
132
150
  value,
133
151
  onChange,
134
152
  onClose,
@@ -137,13 +155,14 @@ function SelectCombobox({
137
155
  getKey: getKeyProp,
138
156
  filter,
139
157
  renderItem,
140
- renderEmpty,
158
+ emptyText,
159
+ loadingText,
141
160
  placeholder,
142
161
  disabled,
143
162
  loading,
144
163
  clearable,
145
164
  autosize,
146
- $isErrored,
165
+ hasError,
147
166
  width,
148
167
  minWidth
149
168
  }) {
@@ -157,11 +176,14 @@ function SelectCombobox({
157
176
  const {
158
177
  sourceItems,
159
178
  filteredItems,
160
- loading: asyncLoading
179
+ loading: asyncLoading,
180
+ loadingMore,
181
+ loadMore
161
182
  } = useDropdownItems({
162
183
  items,
163
184
  loadItems,
164
185
  debounceMs,
186
+ minChars,
165
187
  getLabel,
166
188
  filter,
167
189
  query,
@@ -198,7 +220,8 @@ function SelectCombobox({
198
220
  activeIndex,
199
221
  setActiveIndex,
200
222
  onSelectIndex: index => commit(filteredItems[index]),
201
- onEscape: close
223
+ onEscape: close,
224
+ onReachEnd: loadMore
202
225
  });
203
226
  const onShellMouseDown = useShellMouseDown(inputRef, isOpen, open);
204
227
  useFieldOpenEffects({
@@ -228,7 +251,7 @@ function SelectCombobox({
228
251
  return _jsx(FieldShell, {
229
252
  isOpen: isOpen,
230
253
  dropdownOpen: isOpen,
231
- isErrored: $isErrored,
254
+ isErrored: hasError,
232
255
  disabled: disabled,
233
256
  loading: loading || asyncLoading,
234
257
  width: width,
@@ -243,7 +266,10 @@ function SelectCombobox({
243
266
  (_inputRef$current3 = inputRef.current) === null || _inputRef$current3 === void 0 || _inputRef$current3.focus();
244
267
  },
245
268
  dropdownItems: dropdownItems,
246
- emptyText: renderEmpty ? renderEmpty(query) : undefined,
269
+ emptyText: resolveDropdownText(emptyText, query),
270
+ loadingText: resolveDropdownText(loadingText, query),
271
+ loadingMore: loadingMore,
272
+ onLoadMore: loadMore,
247
273
  listboxId: listboxId,
248
274
  children: _jsx(FieldInput, {
249
275
  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.143",
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,11 @@ type Args = {
7
7
  setActiveIndex: Dispatch<SetStateAction<number>>;
8
8
  onSelectIndex: (index: number) => void;
9
9
  onEscape: () => void;
10
+ onReachEnd?: () => void;
10
11
  hasExtraRow?: boolean;
11
12
  onSelectExtra?: () => void;
12
13
  isQueryEmpty?: boolean;
13
14
  onBackspaceEmpty?: () => void;
14
15
  };
15
- export declare function useListNavigation({ isOpen, open, itemCount, activeIndex, setActiveIndex, onSelectIndex, onEscape, hasExtraRow, onSelectExtra, isQueryEmpty, onBackspaceEmpty, }: Args): (event: KeyboardEvent<HTMLInputElement>) => void;
16
+ export declare function useListNavigation({ isOpen, open, itemCount, activeIndex, setActiveIndex, onSelectIndex, onEscape, onReachEnd, hasExtraRow, onSelectExtra, isQueryEmpty, onBackspaceEmpty, }: Args): (event: KeyboardEvent<HTMLInputElement>) => void;
16
17
  export {};
@@ -7,6 +7,7 @@ export function useListNavigation({
7
7
  setActiveIndex,
8
8
  onSelectIndex,
9
9
  onEscape,
10
+ onReachEnd,
10
11
  hasExtraRow,
11
12
  onSelectExtra,
12
13
  isQueryEmpty,
@@ -16,7 +17,15 @@ export function useListNavigation({
16
17
  return useCallback(event => {
17
18
  if (event.key === 'ArrowDown') {
18
19
  event.preventDefault();
19
- if (!isOpen) open();else if (total > 0) setActiveIndex(prev => prev >= total - 1 ? 0 : prev + 1);
20
+ if (!isOpen) {
21
+ open();
22
+ return;
23
+ }
24
+ if (total > 0) {
25
+ const next = activeIndex >= total - 1 ? 0 : activeIndex + 1;
26
+ setActiveIndex(next);
27
+ if (next > 0 && next >= total - 1) onReachEnd === null || onReachEnd === void 0 || onReachEnd();
28
+ }
20
29
  return;
21
30
  }
22
31
  if (event.key === 'ArrowUp') {
@@ -41,5 +50,5 @@ export function useListNavigation({
41
50
  if (event.key === 'Backspace' && isQueryEmpty && onBackspaceEmpty) {
42
51
  onBackspaceEmpty();
43
52
  }
44
- }, [isOpen, open, total, activeIndex, setActiveIndex, onSelectIndex, onSelectExtra, onEscape, hasExtraRow, isQueryEmpty, onBackspaceEmpty]);
53
+ }, [isOpen, open, total, activeIndex, setActiveIndex, onSelectIndex, onSelectExtra, onEscape, onReachEnd, hasExtraRow, isQueryEmpty, onBackspaceEmpty]);
45
54
  }