@pushwoosh/dumb-components 1.1.141 → 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,22 +1,26 @@
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, 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,
11
+ onClose,
12
+ autoFocus,
10
13
  getLabel: getLabelProp,
11
14
  filter,
12
15
  renderItem,
13
- renderEmpty,
16
+ emptyText,
17
+ loadingText,
14
18
  placeholder,
15
19
  disabled,
16
20
  loading,
17
21
  clearable,
18
22
  autosize,
19
- $isErrored,
23
+ hasError,
20
24
  width,
21
25
  minWidth
22
26
  }) {
@@ -27,11 +31,14 @@ function FreeCombobox({
27
31
  const [isOpen, setIsOpen] = useState(false);
28
32
  const {
29
33
  filteredItems,
30
- loading: asyncLoading
34
+ loading: asyncLoading,
35
+ loadingMore,
36
+ loadMore
31
37
  } = useDropdownItems({
32
38
  items,
33
39
  loadItems,
34
40
  debounceMs,
41
+ minChars,
35
42
  getLabel,
36
43
  filter,
37
44
  query: value,
@@ -44,7 +51,9 @@ function FreeCombobox({
44
51
  setActiveIndex(-1);
45
52
  }, [setActiveIndex]);
46
53
  const pick = useCallback(item => {
47
- onChange(getLabel(item));
54
+ onChange(getLabel(item), {
55
+ source: 'select'
56
+ });
48
57
  setIsOpen(false);
49
58
  setActiveIndex(-1);
50
59
  }, [onChange, getLabel, setActiveIndex]);
@@ -55,9 +64,17 @@ function FreeCombobox({
55
64
  activeIndex,
56
65
  setActiveIndex,
57
66
  onSelectIndex: index => pick(filteredItems[index]),
58
- onEscape: close
67
+ onEscape: close,
68
+ onReachEnd: loadMore
59
69
  });
60
70
  const onShellMouseDown = useShellMouseDown(inputRef, isOpen, open);
71
+ useFieldOpenEffects({
72
+ isOpen,
73
+ open,
74
+ onClose,
75
+ autoFocus,
76
+ inputRef
77
+ });
61
78
  const dropdownItems = useMemo(() => filteredItems.map((item, idx) => {
62
79
  const selected = getLabel(item) === value;
63
80
  const state = {
@@ -73,18 +90,22 @@ function FreeCombobox({
73
90
  onSelect: () => pick(item)
74
91
  };
75
92
  }), [filteredItems, activeIndex, value, getLabel, renderItem, pick, baseId]);
76
- 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);
77
96
  const activeDescendant = activeIndex >= 0 && activeIndex < filteredItems.length ? `${baseId}-opt-${filteredItems[activeIndex]}` : undefined;
78
97
  const onInputChange = event => {
79
- onChange(event.target.value);
98
+ onChange(event.target.value, {
99
+ source: 'input'
100
+ });
80
101
  if (!isOpen) open();
81
102
  };
82
103
  return _jsx(FieldShell, {
83
104
  isOpen: isOpen,
84
105
  dropdownOpen: dropdownVisible,
85
- isErrored: $isErrored,
106
+ isErrored: hasError,
86
107
  disabled: disabled,
87
- loading: loading || asyncLoading,
108
+ loading: isLoading,
88
109
  width: width,
89
110
  minWidth: minWidth,
90
111
  autosize: autosize,
@@ -93,11 +114,16 @@ function FreeCombobox({
93
114
  showClear: !!(clearable && value),
94
115
  onClear: () => {
95
116
  var _inputRef$current;
96
- onChange('');
117
+ onChange('', {
118
+ source: 'input'
119
+ });
97
120
  (_inputRef$current = inputRef.current) === null || _inputRef$current === void 0 || _inputRef$current.focus();
98
121
  },
99
122
  dropdownItems: dropdownItems,
100
- emptyText: renderEmpty ? renderEmpty(value) : undefined,
123
+ emptyText: resolveDropdownText(emptyText, value),
124
+ loadingText: resolveDropdownText(loadingText, value),
125
+ loadingMore: loadingMore,
126
+ onLoadMore: loadMore,
101
127
  listboxId: listboxId,
102
128
  children: _jsx(FieldInput, {
103
129
  ref: inputRef,
@@ -120,19 +146,23 @@ function SelectCombobox({
120
146
  items,
121
147
  loadItems,
122
148
  debounceMs = DEFAULT_DEBOUNCE_MS,
149
+ minChars,
123
150
  value,
124
151
  onChange,
152
+ onClose,
153
+ autoFocus,
125
154
  getLabel,
126
155
  getKey: getKeyProp,
127
156
  filter,
128
157
  renderItem,
129
- renderEmpty,
158
+ emptyText,
159
+ loadingText,
130
160
  placeholder,
131
161
  disabled,
132
162
  loading,
133
163
  clearable,
134
164
  autosize,
135
- $isErrored,
165
+ hasError,
136
166
  width,
137
167
  minWidth
138
168
  }) {
@@ -146,11 +176,14 @@ function SelectCombobox({
146
176
  const {
147
177
  sourceItems,
148
178
  filteredItems,
149
- loading: asyncLoading
179
+ loading: asyncLoading,
180
+ loadingMore,
181
+ loadMore
150
182
  } = useDropdownItems({
151
183
  items,
152
184
  loadItems,
153
185
  debounceMs,
186
+ minChars,
154
187
  getLabel,
155
188
  filter,
156
189
  query,
@@ -187,9 +220,17 @@ function SelectCombobox({
187
220
  activeIndex,
188
221
  setActiveIndex,
189
222
  onSelectIndex: index => commit(filteredItems[index]),
190
- onEscape: close
223
+ onEscape: close,
224
+ onReachEnd: loadMore
191
225
  });
192
226
  const onShellMouseDown = useShellMouseDown(inputRef, isOpen, open);
227
+ useFieldOpenEffects({
228
+ isOpen,
229
+ open,
230
+ onClose,
231
+ autoFocus,
232
+ inputRef
233
+ });
193
234
  const dropdownItems = useMemo(() => filteredItems.map((item, idx) => {
194
235
  const key = getKey(item);
195
236
  const selected = key === selectedKey;
@@ -210,7 +251,7 @@ function SelectCombobox({
210
251
  return _jsx(FieldShell, {
211
252
  isOpen: isOpen,
212
253
  dropdownOpen: isOpen,
213
- isErrored: $isErrored,
254
+ isErrored: hasError,
214
255
  disabled: disabled,
215
256
  loading: loading || asyncLoading,
216
257
  width: width,
@@ -225,7 +266,10 @@ function SelectCombobox({
225
266
  (_inputRef$current3 = inputRef.current) === null || _inputRef$current3 === void 0 || _inputRef$current3.focus();
226
267
  },
227
268
  dropdownItems: dropdownItems,
228
- emptyText: renderEmpty ? renderEmpty(query) : undefined,
269
+ emptyText: resolveDropdownText(emptyText, query),
270
+ loadingText: resolveDropdownText(loadingText, query),
271
+ loadingMore: loadingMore,
272
+ onLoadMore: loadMore,
229
273
  listboxId: listboxId,
230
274
  children: _jsx(FieldInput, {
231
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,31 +1,40 @@
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;
23
+ onClose?: () => void;
24
+ autoFocus?: boolean;
16
25
  placeholder?: string;
17
26
  disabled?: boolean;
18
27
  loading?: boolean;
19
28
  clearable?: boolean;
20
29
  autosize?: boolean;
21
- $isErrored?: boolean;
30
+ hasError?: boolean;
22
31
  width?: string;
23
32
  minWidth?: string;
24
33
  };
25
34
  export type FreeComboboxProps = ComboboxSource<string> & CommonProps<string> & {
26
35
  select?: false;
27
36
  value: string;
28
- onChange: (value: string) => void;
37
+ onChange: (value: string, meta: ComboboxChangeMeta) => void;
29
38
  getLabel?: (item: string) => string;
30
39
  };
31
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, 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, 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,13 +12,17 @@ export function ComboboxMulti({
12
12
  items,
13
13
  loadItems,
14
14
  debounceMs = DEFAULT_DEBOUNCE_MS,
15
+ minChars,
15
16
  value,
16
17
  onChange,
18
+ onClose,
19
+ autoFocus,
17
20
  getLabel,
18
21
  getKey: getKeyProp,
19
22
  filter,
20
23
  renderItem,
21
- renderEmpty,
24
+ emptyText,
25
+ loadingText,
22
26
  creatable,
23
27
  onCreate,
24
28
  canCreate,
@@ -29,7 +33,7 @@ export function ComboboxMulti({
29
33
  loading,
30
34
  clearable,
31
35
  autosize,
32
- $isErrored,
36
+ hasError,
33
37
  width,
34
38
  minWidth
35
39
  }) {
@@ -46,12 +50,15 @@ export function ComboboxMulti({
46
50
  const {
47
51
  sourceItems,
48
52
  filteredItems,
49
- loading: asyncLoading
53
+ loading: asyncLoading,
54
+ loadingMore,
55
+ loadMore
50
56
  } = useDropdownItems({
51
57
  items,
52
58
  loadItems,
53
59
  localItems: createdItems,
54
60
  debounceMs,
61
+ minChars,
55
62
  getLabel,
56
63
  filter,
57
64
  query,
@@ -101,12 +108,20 @@ export function ComboboxMulti({
101
108
  setActiveIndex,
102
109
  onSelectIndex: index => toggle(visibleItems[index]),
103
110
  onEscape: close,
111
+ onReachEnd: loadMore,
104
112
  hasExtraRow: canCreateNow,
105
113
  onSelectExtra: create,
106
114
  isQueryEmpty: !query && value.length > 0,
107
115
  onBackspaceEmpty: () => onChange(value.slice(0, -1))
108
116
  });
109
117
  const onShellMouseDown = useShellMouseDown(inputRef, isOpen, open);
118
+ useFieldOpenEffects({
119
+ isOpen,
120
+ open,
121
+ onClose,
122
+ autoFocus,
123
+ inputRef
124
+ });
110
125
  const toggleOpen = useCallback(() => {
111
126
  if (isOpen) {
112
127
  var _inputRef$current2;
@@ -146,7 +161,7 @@ export function ComboboxMulti({
146
161
  return _jsxs(FieldShell, {
147
162
  isOpen: isOpen,
148
163
  dropdownOpen: isOpen,
149
- isErrored: $isErrored,
164
+ isErrored: hasError,
150
165
  disabled: disabled,
151
166
  loading: loading || asyncLoading,
152
167
  width: width,
@@ -161,7 +176,10 @@ export function ComboboxMulti({
161
176
  showChevron: true,
162
177
  onToggle: toggleOpen,
163
178
  dropdownItems: dropdownItems,
164
- emptyText: renderEmpty ? renderEmpty(query) : undefined,
179
+ emptyText: resolveDropdownText(emptyText, query),
180
+ loadingText: resolveDropdownText(loadingText, query),
181
+ loadingMore: loadingMore,
182
+ onLoadMore: loadMore,
165
183
  createLabel: createLabel,
166
184
  createActive: createActive,
167
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;
@@ -13,10 +13,13 @@ type CreatableProp<T> = [T] extends [string] ? {
13
13
  export type ComboboxMultiProps<T> = ItemsSource<T> & GetKeyProp<T> & CreatableProp<T> & {
14
14
  value: T[];
15
15
  onChange: (value: T[]) => void;
16
+ onClose?: () => void;
17
+ autoFocus?: boolean;
16
18
  getLabel: (item: T) => string;
17
19
  filter?: (item: T, query: string) => boolean;
18
20
  renderItem?: (item: T, state: ItemState) => ReactNode;
19
- renderEmpty?: (query: string) => ReactNode;
21
+ emptyText?: DropdownText;
22
+ loadingText?: DropdownText;
20
23
  canCreate?: (query: string, items: T[]) => boolean;
21
24
  renderCreateLabel?: (query: string) => ReactNode;
22
25
  maxValues?: number;
@@ -25,7 +28,7 @@ export type ComboboxMultiProps<T> = ItemsSource<T> & GetKeyProp<T> & CreatablePr
25
28
  loading?: boolean;
26
29
  clearable?: boolean;
27
30
  autosize?: boolean;
28
- $isErrored?: boolean;
31
+ hasError?: boolean;
29
32
  width?: string;
30
33
  minWidth?: string;
31
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.141",
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,
@@ -33,6 +36,7 @@ export function FieldShell({
33
36
  const {
34
37
  setReference,
35
38
  setFloating,
39
+ floatingRef,
36
40
  floatingStyles
37
41
  } = useDropdownPosition({
38
42
  isOpen: dropdownOpen,
@@ -44,6 +48,15 @@ export function FieldShell({
44
48
  isOpen: !!dropdownOpen,
45
49
  onClose
46
50
  });
51
+ const onShellBlur = event => {
52
+ var _floatingRef$current;
53
+ if (!isOpen) return;
54
+ const next = event.relatedTarget;
55
+ if (!next) return;
56
+ if (event.currentTarget.contains(next)) return;
57
+ if ((_floatingRef$current = floatingRef.current) !== null && _floatingRef$current !== void 0 && _floatingRef$current.contains(next)) return;
58
+ onClose === null || onClose === void 0 || onClose();
59
+ };
47
60
  return _jsxs(_Fragment, {
48
61
  children: [_jsxs(Shell, {
49
62
  ref: setReference,
@@ -56,6 +69,7 @@ export function FieldShell({
56
69
  "$autosize": autosize,
57
70
  "$tightLeft": tightLeft,
58
71
  onMouseDown: onShellMouseDown,
72
+ onBlur: onShellBlur,
59
73
  children: [_jsx(ShellInner, {
60
74
  children: children
61
75
  }), _jsxs(Indicators, {
@@ -88,6 +102,10 @@ export function FieldShell({
88
102
  floatingStyles: floatingStyles,
89
103
  items: dropdownItems,
90
104
  emptyText: emptyText,
105
+ loading: loading,
106
+ loadingText: loadingText,
107
+ loadingMore: loadingMore,
108
+ onLoadMore: onLoadMore,
91
109
  createLabel: createLabel,
92
110
  createActive: createActive,
93
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);
@@ -6,7 +6,8 @@ export { useDebouncedValue } from './useDebouncedValue';
6
6
  export { useAsyncItems } from './useAsyncItems';
7
7
  export { useDropdownItems } from './useDropdownItems';
8
8
  export { useActiveIndex } from './useActiveIndex';
9
+ export { useFieldOpenEffects } from './useFieldOpenEffects';
9
10
  export { useListNavigation } from './useListNavigation';
10
- export { defaultGetKey, defaultLabelFilter, useShellMouseDown } from './shared';
11
+ export { defaultGetKey, defaultLabelFilter, resolveDropdownText, useShellMouseDown, } from './shared';
11
12
  export { DEFAULT_DEBOUNCE_MS } from './constants';
12
- export type { ItemState, DropdownItemDescriptor, GetKeyProp, ItemsSource, } from './types';
13
+ export type { ItemState, DropdownItemDescriptor, DropdownText, GetKeyProp, ItemsSource, LoadItems, LoadItemsResult, } from './types';
@@ -6,6 +6,7 @@ export { useDebouncedValue } from './useDebouncedValue';
6
6
  export { useAsyncItems } from './useAsyncItems';
7
7
  export { useDropdownItems } from './useDropdownItems';
8
8
  export { useActiveIndex } from './useActiveIndex';
9
+ export { useFieldOpenEffects } from './useFieldOpenEffects';
9
10
  export { useListNavigation } from './useListNavigation';
10
- export { defaultGetKey, defaultLabelFilter, useShellMouseDown } from './shared';
11
+ export { defaultGetKey, defaultLabelFilter, resolveDropdownText, useShellMouseDown } from './shared';
11
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
  }
@@ -5,6 +5,7 @@ type Args = {
5
5
  export declare function useDropdownPosition({ isOpen, onClickOutside }: Args): {
6
6
  setReference: (node: import("@floating-ui/react-dom").ReferenceType | null) => void;
7
7
  setFloating: (node: HTMLElement | null) => void;
8
+ floatingRef: import("react").MutableRefObject<HTMLElement | null>;
8
9
  floatingStyles: import("react").CSSProperties;
9
10
  };
10
11
  export {};
@@ -35,6 +35,7 @@ export function useDropdownPosition({
35
35
  return {
36
36
  setReference: refs.setReference,
37
37
  setFloating: refs.setFloating,
38
+ floatingRef: refs.floating,
38
39
  floatingStyles
39
40
  };
40
41
  }
@@ -0,0 +1,10 @@
1
+ import { type RefObject } from 'react';
2
+ type Args = {
3
+ isOpen: boolean;
4
+ open: () => void;
5
+ onClose?: () => void;
6
+ autoFocus?: boolean;
7
+ inputRef: RefObject<HTMLInputElement>;
8
+ };
9
+ export declare function useFieldOpenEffects({ isOpen, open, onClose, autoFocus, inputRef, }: Args): void;
10
+ export {};
@@ -0,0 +1,25 @@
1
+ import { useEffect, useRef } from 'react';
2
+ export function useFieldOpenEffects({
3
+ isOpen,
4
+ open,
5
+ onClose,
6
+ autoFocus,
7
+ inputRef
8
+ }) {
9
+ const onCloseRef = useRef(onClose);
10
+ onCloseRef.current = onClose;
11
+ const wasOpenRef = useRef(isOpen);
12
+ useEffect(() => {
13
+ var _onCloseRef$current;
14
+ if (wasOpenRef.current && !isOpen) (_onCloseRef$current = onCloseRef.current) === null || _onCloseRef$current === void 0 || _onCloseRef$current.call(onCloseRef);
15
+ wasOpenRef.current = isOpen;
16
+ }, [isOpen]);
17
+ const didAutoFocusRef = useRef(false);
18
+ useEffect(() => {
19
+ var _inputRef$current;
20
+ if (!autoFocus || didAutoFocusRef.current) return;
21
+ didAutoFocusRef.current = true;
22
+ (_inputRef$current = inputRef.current) === null || _inputRef$current === void 0 || _inputRef$current.focus();
23
+ open();
24
+ }, [autoFocus, open, inputRef]);
25
+ }
@@ -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
  }