@pushwoosh/dumb-components 1.1.140 → 1.1.141

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.
Files changed (45) hide show
  1. package/Combobox/Combobox.d.ts +4 -0
  2. package/Combobox/Combobox.js +254 -0
  3. package/Combobox/index.d.ts +2 -0
  4. package/Combobox/index.js +1 -0
  5. package/Combobox/types.d.ts +38 -0
  6. package/Combobox/types.js +1 -0
  7. package/ComboboxMulti/ComboboxMulti.d.ts +3 -0
  8. package/ComboboxMulti/ComboboxMulti.js +194 -0
  9. package/ComboboxMulti/index.d.ts +2 -0
  10. package/ComboboxMulti/index.js +1 -0
  11. package/ComboboxMulti/types.d.ts +32 -0
  12. package/ComboboxMulti/types.js +1 -0
  13. package/hooks/useLayer.js +41 -14
  14. package/hooks/useOnClickOutside.js +6 -5
  15. package/index.d.ts +2 -0
  16. package/index.js +2 -0
  17. package/package.json +2 -2
  18. package/shared/dropdownField/FieldInput.d.ts +5 -0
  19. package/shared/dropdownField/FieldInput.js +43 -0
  20. package/shared/dropdownField/FieldShell.d.ts +30 -0
  21. package/shared/dropdownField/FieldShell.js +99 -0
  22. package/shared/dropdownField/SuggestionsDropdown.d.ts +16 -0
  23. package/shared/dropdownField/SuggestionsDropdown.js +68 -0
  24. package/shared/dropdownField/constants.d.ts +6 -0
  25. package/shared/dropdownField/constants.js +6 -0
  26. package/shared/dropdownField/index.d.ts +12 -0
  27. package/shared/dropdownField/index.js +11 -0
  28. package/shared/dropdownField/shared.d.ts +4 -0
  29. package/shared/dropdownField/shared.js +18 -0
  30. package/shared/dropdownField/styles.d.ts +24 -0
  31. package/shared/dropdownField/styles.js +81 -0
  32. package/shared/dropdownField/types.d.ts +27 -0
  33. package/shared/dropdownField/types.js +1 -0
  34. package/shared/dropdownField/useActiveIndex.d.ts +2 -0
  35. package/shared/dropdownField/useActiveIndex.js +12 -0
  36. package/shared/dropdownField/useAsyncItems.d.ts +11 -0
  37. package/shared/dropdownField/useAsyncItems.js +46 -0
  38. package/shared/dropdownField/useDebouncedValue.d.ts +1 -0
  39. package/shared/dropdownField/useDebouncedValue.js +13 -0
  40. package/shared/dropdownField/useDropdownItems.d.ts +16 -0
  41. package/shared/dropdownField/useDropdownItems.js +36 -0
  42. package/shared/dropdownField/useDropdownPosition.d.ts +10 -0
  43. package/shared/dropdownField/useDropdownPosition.js +40 -0
  44. package/shared/dropdownField/useListNavigation.d.ts +16 -0
  45. package/shared/dropdownField/useListNavigation.js +45 -0
@@ -0,0 +1,4 @@
1
+ import { type ReactElement } from 'react';
2
+ import { type FreeComboboxProps, type SelectComboboxProps } from './types';
3
+ export declare function Combobox(props: FreeComboboxProps): ReactElement;
4
+ export declare function Combobox<T>(props: SelectComboboxProps<T>): ReactElement;
@@ -0,0 +1,254 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useCallback, useId, useMemo, useRef, useState } from 'react';
3
+ import { DEFAULT_DEBOUNCE_MS, FieldInput, FieldShell, defaultGetKey, useActiveIndex, useDropdownItems, useListNavigation, useShellMouseDown } from '../shared/dropdownField';
4
+ function FreeCombobox({
5
+ items,
6
+ loadItems,
7
+ debounceMs = DEFAULT_DEBOUNCE_MS,
8
+ value,
9
+ onChange,
10
+ getLabel: getLabelProp,
11
+ filter,
12
+ renderItem,
13
+ renderEmpty,
14
+ placeholder,
15
+ disabled,
16
+ loading,
17
+ clearable,
18
+ autosize,
19
+ $isErrored,
20
+ width,
21
+ minWidth
22
+ }) {
23
+ const inputRef = useRef(null);
24
+ const baseId = useId();
25
+ const listboxId = `${baseId}-list`;
26
+ const getLabel = useMemo(() => getLabelProp ?? (item => item), [getLabelProp]);
27
+ const [isOpen, setIsOpen] = useState(false);
28
+ const {
29
+ filteredItems,
30
+ loading: asyncLoading
31
+ } = useDropdownItems({
32
+ items,
33
+ loadItems,
34
+ debounceMs,
35
+ getLabel,
36
+ filter,
37
+ query: value,
38
+ isOpen
39
+ });
40
+ const [activeIndex, setActiveIndex] = useActiveIndex(filteredItems.length, -1);
41
+ const open = useCallback(() => setIsOpen(true), []);
42
+ const close = useCallback(() => {
43
+ setIsOpen(false);
44
+ setActiveIndex(-1);
45
+ }, [setActiveIndex]);
46
+ const pick = useCallback(item => {
47
+ onChange(getLabel(item));
48
+ setIsOpen(false);
49
+ setActiveIndex(-1);
50
+ }, [onChange, getLabel, setActiveIndex]);
51
+ const onInputKeyDown = useListNavigation({
52
+ isOpen,
53
+ open,
54
+ itemCount: filteredItems.length,
55
+ activeIndex,
56
+ setActiveIndex,
57
+ onSelectIndex: index => pick(filteredItems[index]),
58
+ onEscape: close
59
+ });
60
+ const onShellMouseDown = useShellMouseDown(inputRef, isOpen, open);
61
+ const dropdownItems = useMemo(() => filteredItems.map((item, idx) => {
62
+ const selected = getLabel(item) === value;
63
+ const state = {
64
+ active: idx === activeIndex,
65
+ selected
66
+ };
67
+ return {
68
+ key: item,
69
+ id: `${baseId}-opt-${item}`,
70
+ label: renderItem ? renderItem(item, state) : getLabel(item),
71
+ active: state.active,
72
+ selected,
73
+ onSelect: () => pick(item)
74
+ };
75
+ }), [filteredItems, activeIndex, value, getLabel, renderItem, pick, baseId]);
76
+ const dropdownVisible = isOpen && filteredItems.length > 0;
77
+ const activeDescendant = activeIndex >= 0 && activeIndex < filteredItems.length ? `${baseId}-opt-${filteredItems[activeIndex]}` : undefined;
78
+ const onInputChange = event => {
79
+ onChange(event.target.value);
80
+ if (!isOpen) open();
81
+ };
82
+ return _jsx(FieldShell, {
83
+ isOpen: isOpen,
84
+ dropdownOpen: dropdownVisible,
85
+ isErrored: $isErrored,
86
+ disabled: disabled,
87
+ loading: loading || asyncLoading,
88
+ width: width,
89
+ minWidth: minWidth,
90
+ autosize: autosize,
91
+ onShellMouseDown: onShellMouseDown,
92
+ onClose: close,
93
+ showClear: !!(clearable && value),
94
+ onClear: () => {
95
+ var _inputRef$current;
96
+ onChange('');
97
+ (_inputRef$current = inputRef.current) === null || _inputRef$current === void 0 || _inputRef$current.focus();
98
+ },
99
+ dropdownItems: dropdownItems,
100
+ emptyText: renderEmpty ? renderEmpty(value) : undefined,
101
+ listboxId: listboxId,
102
+ children: _jsx(FieldInput, {
103
+ ref: inputRef,
104
+ autosize: autosize,
105
+ value: value,
106
+ onChange: onInputChange,
107
+ onKeyDown: onInputKeyDown,
108
+ onFocus: open,
109
+ placeholder: placeholder,
110
+ disabled: disabled,
111
+ role: "combobox",
112
+ "aria-expanded": dropdownVisible,
113
+ "aria-controls": listboxId,
114
+ "aria-autocomplete": "list",
115
+ "aria-activedescendant": activeDescendant
116
+ })
117
+ });
118
+ }
119
+ function SelectCombobox({
120
+ items,
121
+ loadItems,
122
+ debounceMs = DEFAULT_DEBOUNCE_MS,
123
+ value,
124
+ onChange,
125
+ getLabel,
126
+ getKey: getKeyProp,
127
+ filter,
128
+ renderItem,
129
+ renderEmpty,
130
+ placeholder,
131
+ disabled,
132
+ loading,
133
+ clearable,
134
+ autosize,
135
+ $isErrored,
136
+ width,
137
+ minWidth
138
+ }) {
139
+ const inputRef = useRef(null);
140
+ const baseId = useId();
141
+ const listboxId = `${baseId}-list`;
142
+ const optionId = key => `${baseId}-opt-${key}`;
143
+ const getKey = getKeyProp ?? defaultGetKey;
144
+ const [isOpen, setIsOpen] = useState(false);
145
+ const [query, setQuery] = useState('');
146
+ const {
147
+ sourceItems,
148
+ filteredItems,
149
+ loading: asyncLoading
150
+ } = useDropdownItems({
151
+ items,
152
+ loadItems,
153
+ debounceMs,
154
+ getLabel,
155
+ filter,
156
+ query,
157
+ isOpen
158
+ });
159
+ const [activeIndex, setActiveIndex] = useActiveIndex(filteredItems.length, 0);
160
+ const hasValue = value !== null && value !== undefined;
161
+ const valueLabel = hasValue ? getLabel(value) : '';
162
+ const selectedKey = hasValue ? getKey(value) : null;
163
+ const inputValue = isOpen ? query : valueLabel;
164
+ const open = useCallback(() => {
165
+ if (isOpen) return;
166
+ setIsOpen(true);
167
+ setQuery('');
168
+ const idx = sourceItems.findIndex(it => getKey(it) === selectedKey);
169
+ setActiveIndex(idx >= 0 ? idx : 0);
170
+ }, [isOpen, sourceItems, getKey, selectedKey, setActiveIndex]);
171
+ const close = useCallback(() => {
172
+ setIsOpen(false);
173
+ setQuery('');
174
+ }, []);
175
+ const commit = useCallback(item => {
176
+ var _inputRef$current2;
177
+ onChange(item);
178
+ setIsOpen(false);
179
+ setQuery('');
180
+ setActiveIndex(0);
181
+ (_inputRef$current2 = inputRef.current) === null || _inputRef$current2 === void 0 || _inputRef$current2.blur();
182
+ }, [onChange, setActiveIndex]);
183
+ const onInputKeyDown = useListNavigation({
184
+ isOpen,
185
+ open,
186
+ itemCount: filteredItems.length,
187
+ activeIndex,
188
+ setActiveIndex,
189
+ onSelectIndex: index => commit(filteredItems[index]),
190
+ onEscape: close
191
+ });
192
+ const onShellMouseDown = useShellMouseDown(inputRef, isOpen, open);
193
+ const dropdownItems = useMemo(() => filteredItems.map((item, idx) => {
194
+ const key = getKey(item);
195
+ const selected = key === selectedKey;
196
+ const state = {
197
+ active: idx === activeIndex,
198
+ selected
199
+ };
200
+ return {
201
+ key,
202
+ id: `${baseId}-opt-${key}`,
203
+ label: renderItem ? renderItem(item, state) : getLabel(item),
204
+ active: state.active,
205
+ selected,
206
+ onSelect: () => commit(item)
207
+ };
208
+ }), [filteredItems, activeIndex, selectedKey, getLabel, getKey, renderItem, commit, baseId]);
209
+ const activeDescendant = activeIndex >= 0 && activeIndex < filteredItems.length ? optionId(getKey(filteredItems[activeIndex])) : undefined;
210
+ return _jsx(FieldShell, {
211
+ isOpen: isOpen,
212
+ dropdownOpen: isOpen,
213
+ isErrored: $isErrored,
214
+ disabled: disabled,
215
+ loading: loading || asyncLoading,
216
+ width: width,
217
+ minWidth: minWidth,
218
+ autosize: autosize,
219
+ onShellMouseDown: onShellMouseDown,
220
+ onClose: close,
221
+ showClear: !!(clearable && hasValue),
222
+ onClear: () => {
223
+ var _inputRef$current3;
224
+ onChange(null);
225
+ (_inputRef$current3 = inputRef.current) === null || _inputRef$current3 === void 0 || _inputRef$current3.focus();
226
+ },
227
+ dropdownItems: dropdownItems,
228
+ emptyText: renderEmpty ? renderEmpty(query) : undefined,
229
+ listboxId: listboxId,
230
+ children: _jsx(FieldInput, {
231
+ ref: inputRef,
232
+ autosize: autosize,
233
+ value: inputValue,
234
+ onChange: event => setQuery(event.target.value),
235
+ onKeyDown: onInputKeyDown,
236
+ onFocus: open,
237
+ placeholder: placeholder,
238
+ disabled: disabled,
239
+ role: "combobox",
240
+ "aria-expanded": isOpen,
241
+ "aria-controls": listboxId,
242
+ "aria-autocomplete": "list",
243
+ "aria-activedescendant": activeDescendant
244
+ })
245
+ });
246
+ }
247
+ export function Combobox(props) {
248
+ if (props.select) return _jsx(SelectCombobox, {
249
+ ...props
250
+ });
251
+ return _jsx(FreeCombobox, {
252
+ ...props
253
+ });
254
+ }
@@ -0,0 +1,2 @@
1
+ export { Combobox } from './Combobox';
2
+ export type { ComboboxProps, FreeComboboxProps, SelectComboboxProps, } from './types';
@@ -0,0 +1 @@
1
+ export { Combobox } from './Combobox';
@@ -0,0 +1,38 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { GetKeyProp, ItemState } from '../shared/dropdownField';
3
+ type ComboboxSource<T> = {
4
+ items?: T[];
5
+ loadItems?: never;
6
+ debounceMs?: never;
7
+ } | {
8
+ items?: never;
9
+ loadItems: (query: string) => Promise<T[]>;
10
+ debounceMs?: number;
11
+ };
12
+ type CommonProps<T> = {
13
+ filter?: (item: T, query: string) => boolean;
14
+ renderItem?: (item: T, state: ItemState) => ReactNode;
15
+ renderEmpty?: (query: string) => ReactNode;
16
+ placeholder?: string;
17
+ disabled?: boolean;
18
+ loading?: boolean;
19
+ clearable?: boolean;
20
+ autosize?: boolean;
21
+ $isErrored?: boolean;
22
+ width?: string;
23
+ minWidth?: string;
24
+ };
25
+ export type FreeComboboxProps = ComboboxSource<string> & CommonProps<string> & {
26
+ select?: false;
27
+ value: string;
28
+ onChange: (value: string) => void;
29
+ getLabel?: (item: string) => string;
30
+ };
31
+ export type SelectComboboxProps<T> = ComboboxSource<T> & GetKeyProp<T> & CommonProps<T> & {
32
+ select: true;
33
+ value: T | null;
34
+ onChange: (value: T | null) => void;
35
+ getLabel: (item: T) => string;
36
+ };
37
+ export type ComboboxProps<T> = FreeComboboxProps | SelectComboboxProps<T>;
38
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ import { type ReactElement } from 'react';
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;
@@ -0,0 +1,194 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useId, useMemo, useRef, useState } from 'react';
3
+ import { Chip } from '../Chip';
4
+ import { DEFAULT_DEBOUNCE_MS, FieldInput, FieldShell, defaultGetKey, useActiveIndex, useDropdownItems, useListNavigation, useShellMouseDown } from '../shared/dropdownField';
5
+ function defaultCanCreate(query, items, getLabel) {
6
+ const trimmed = query.trim();
7
+ if (!trimmed) return false;
8
+ const lower = trimmed.toLowerCase();
9
+ return !items.some(item => getLabel(item).toLowerCase() === lower);
10
+ }
11
+ export function ComboboxMulti({
12
+ items,
13
+ loadItems,
14
+ debounceMs = DEFAULT_DEBOUNCE_MS,
15
+ value,
16
+ onChange,
17
+ getLabel,
18
+ getKey: getKeyProp,
19
+ filter,
20
+ renderItem,
21
+ renderEmpty,
22
+ creatable,
23
+ onCreate,
24
+ canCreate,
25
+ renderCreateLabel,
26
+ maxValues,
27
+ placeholder,
28
+ disabled,
29
+ loading,
30
+ clearable,
31
+ autosize,
32
+ $isErrored,
33
+ width,
34
+ minWidth
35
+ }) {
36
+ const inputRef = useRef(null);
37
+ const isAsync = !!loadItems;
38
+ const getKey = getKeyProp ?? defaultGetKey;
39
+ const baseId = useId();
40
+ const listboxId = `${baseId}-list`;
41
+ const createId = `${baseId}-create`;
42
+ const optionId = key => `${baseId}-opt-${key}`;
43
+ const [isOpen, setIsOpen] = useState(false);
44
+ const [query, setQuery] = useState('');
45
+ const [createdItems, setCreatedItems] = useState([]);
46
+ const {
47
+ sourceItems,
48
+ filteredItems,
49
+ loading: asyncLoading
50
+ } = useDropdownItems({
51
+ items,
52
+ loadItems,
53
+ localItems: createdItems,
54
+ debounceMs,
55
+ getLabel,
56
+ filter,
57
+ query,
58
+ isOpen
59
+ });
60
+ const selectedKeys = useMemo(() => {
61
+ const set = new Set();
62
+ value.forEach(item => set.add(getKey(item)));
63
+ return set;
64
+ }, [value, getKey]);
65
+ const visibleItems = useMemo(() => filteredItems.filter(item => !selectedKeys.has(getKey(item))), [filteredItems, selectedKeys, getKey]);
66
+ const atMax = maxValues !== undefined && value.length >= maxValues;
67
+ const canCreateNow = !!(creatable && query.trim() && !atMax && (canCreate ? canCreate(query, sourceItems) : defaultCanCreate(query, sourceItems, getLabel)));
68
+ const totalRows = visibleItems.length + (canCreateNow ? 1 : 0);
69
+ const [activeIndex, setActiveIndex] = useActiveIndex(totalRows, 0);
70
+ const open = useCallback(() => setIsOpen(true), []);
71
+ const close = useCallback(() => {
72
+ setIsOpen(false);
73
+ setQuery('');
74
+ }, []);
75
+ const toggle = useCallback(item => {
76
+ var _inputRef$current;
77
+ const key = getKey(item);
78
+ if (selectedKeys.has(key)) {
79
+ onChange(value.filter(v => getKey(v) !== key));
80
+ return;
81
+ }
82
+ if (maxValues !== undefined && value.length >= maxValues) return;
83
+ onChange([...value, item]);
84
+ setIsOpen(true);
85
+ (_inputRef$current = inputRef.current) === null || _inputRef$current === void 0 || _inputRef$current.focus();
86
+ }, [value, getKey, selectedKeys, onChange, maxValues]);
87
+ const create = useCallback(() => {
88
+ const trimmed = query.trim();
89
+ if (!trimmed || atMax) return;
90
+ const item = onCreate ? onCreate(trimmed) : trimmed;
91
+ if (!isAsync) setCreatedItems(prev => [...prev, item]);
92
+ onChange([...value, item]);
93
+ setQuery('');
94
+ setActiveIndex(0);
95
+ }, [query, atMax, onCreate, isAsync, onChange, value, setActiveIndex]);
96
+ const onInputKeyDown = useListNavigation({
97
+ isOpen,
98
+ open,
99
+ itemCount: visibleItems.length,
100
+ activeIndex,
101
+ setActiveIndex,
102
+ onSelectIndex: index => toggle(visibleItems[index]),
103
+ onEscape: close,
104
+ hasExtraRow: canCreateNow,
105
+ onSelectExtra: create,
106
+ isQueryEmpty: !query && value.length > 0,
107
+ onBackspaceEmpty: () => onChange(value.slice(0, -1))
108
+ });
109
+ const onShellMouseDown = useShellMouseDown(inputRef, isOpen, open);
110
+ const toggleOpen = useCallback(() => {
111
+ if (isOpen) {
112
+ var _inputRef$current2;
113
+ close();
114
+ (_inputRef$current2 = inputRef.current) === null || _inputRef$current2 === void 0 || _inputRef$current2.blur();
115
+ } else {
116
+ var _inputRef$current3;
117
+ open();
118
+ (_inputRef$current3 = inputRef.current) === null || _inputRef$current3 === void 0 || _inputRef$current3.focus();
119
+ }
120
+ }, [isOpen, open, close]);
121
+ const itemOffset = canCreateNow ? 1 : 0;
122
+ const dropdownItems = useMemo(() => visibleItems.map((item, idx) => {
123
+ const key = getKey(item);
124
+ const state = {
125
+ active: idx + itemOffset === activeIndex,
126
+ selected: false
127
+ };
128
+ return {
129
+ key,
130
+ id: `${baseId}-opt-${key}`,
131
+ label: renderItem ? renderItem(item, state) : getLabel(item),
132
+ active: state.active,
133
+ selected: state.selected,
134
+ onSelect: () => toggle(item)
135
+ };
136
+ }), [visibleItems, itemOffset, activeIndex, renderItem, getLabel, getKey, toggle, baseId]);
137
+ const createActive = canCreateNow && activeIndex === 0;
138
+ const activeItem = visibleItems[activeIndex - itemOffset];
139
+ let activeDescendant;
140
+ if (createActive) activeDescendant = createId;else if (activeItem !== undefined) activeDescendant = optionId(getKey(activeItem));
141
+ let createLabel;
142
+ if (canCreateNow) {
143
+ createLabel = renderCreateLabel ? renderCreateLabel(query.trim()) : `Create "${query.trim()}"`;
144
+ }
145
+ const hasTags = value.length > 0;
146
+ return _jsxs(FieldShell, {
147
+ isOpen: isOpen,
148
+ dropdownOpen: isOpen,
149
+ isErrored: $isErrored,
150
+ disabled: disabled,
151
+ loading: loading || asyncLoading,
152
+ width: width,
153
+ minWidth: minWidth,
154
+ autosize: autosize,
155
+ tightLeft: hasTags,
156
+ onShellMouseDown: onShellMouseDown,
157
+ onClose: close,
158
+ showClear: !!(clearable && hasTags),
159
+ onClear: () => onChange([]),
160
+ clearLabel: "Clear all",
161
+ showChevron: true,
162
+ onToggle: toggleOpen,
163
+ dropdownItems: dropdownItems,
164
+ emptyText: renderEmpty ? renderEmpty(query) : undefined,
165
+ createLabel: createLabel,
166
+ createActive: createActive,
167
+ onCreate: create,
168
+ listboxId: listboxId,
169
+ createId: createId,
170
+ children: [value.map(item => {
171
+ const key = getKey(item);
172
+ return _jsx(Chip, {
173
+ "$readOnly": disabled,
174
+ "$onClose": disabled ? undefined : () => onChange(value.filter(v => getKey(v) !== key)),
175
+ "$maxWidth": "100%",
176
+ children: getLabel(item)
177
+ }, key);
178
+ }), _jsx(FieldInput, {
179
+ ref: inputRef,
180
+ autosize: autosize,
181
+ value: query,
182
+ onChange: e => setQuery(e.target.value),
183
+ onKeyDown: onInputKeyDown,
184
+ onFocus: open,
185
+ placeholder: hasTags ? undefined : placeholder,
186
+ disabled: disabled,
187
+ role: "combobox",
188
+ "aria-expanded": isOpen,
189
+ "aria-controls": listboxId,
190
+ "aria-autocomplete": "list",
191
+ "aria-activedescendant": activeDescendant
192
+ })]
193
+ });
194
+ }
@@ -0,0 +1,2 @@
1
+ export { ComboboxMulti } from './ComboboxMulti';
2
+ export type { ComboboxMultiProps } from './types';
@@ -0,0 +1 @@
1
+ export { ComboboxMulti } from './ComboboxMulti';
@@ -0,0 +1,32 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { GetKeyProp, ItemState, ItemsSource } from '../shared/dropdownField';
3
+ type CreatableProp<T> = [T] extends [string] ? {
4
+ creatable?: boolean;
5
+ onCreate?: (query: string) => T;
6
+ } : {
7
+ creatable?: false;
8
+ onCreate?: never;
9
+ } | {
10
+ creatable: true;
11
+ onCreate: (query: string) => T;
12
+ };
13
+ export type ComboboxMultiProps<T> = ItemsSource<T> & GetKeyProp<T> & CreatableProp<T> & {
14
+ value: T[];
15
+ onChange: (value: T[]) => void;
16
+ getLabel: (item: T) => string;
17
+ filter?: (item: T, query: string) => boolean;
18
+ renderItem?: (item: T, state: ItemState) => ReactNode;
19
+ renderEmpty?: (query: string) => ReactNode;
20
+ canCreate?: (query: string, items: T[]) => boolean;
21
+ renderCreateLabel?: (query: string) => ReactNode;
22
+ maxValues?: number;
23
+ placeholder?: string;
24
+ disabled?: boolean;
25
+ loading?: boolean;
26
+ clearable?: boolean;
27
+ autosize?: boolean;
28
+ $isErrored?: boolean;
29
+ width?: string;
30
+ minWidth?: string;
31
+ };
32
+ export {};
@@ -0,0 +1 @@
1
+ export {};
package/hooks/useLayer.js CHANGED
@@ -1,38 +1,65 @@
1
1
  import { useEffect, useId, useLayoutEffect, useRef, useState } from 'react';
2
2
  const BASE_Z_INDEX = 10000;
3
3
  const Z_INDEX_STEP = 100;
4
- const stack = [];
5
- const subscribers = new Set();
6
- let keydownAttached = false;
4
+ const GLOBAL_KEY = '__pushwoosh_dumb_components_layer_state__';
5
+ function createState() {
6
+ return {
7
+ stack: [],
8
+ subscribers: new Set(),
9
+ keydownHandler: null
10
+ };
11
+ }
12
+ function isLayerState(value) {
13
+ if (typeof value !== 'object' || value === null) {
14
+ return false;
15
+ }
16
+ const candidate = value;
17
+ return Array.isArray(candidate.stack) && candidate.subscribers instanceof Set;
18
+ }
19
+ function getState() {
20
+ const globalScope = window;
21
+ if (!isLayerState(globalScope[GLOBAL_KEY])) {
22
+ globalScope[GLOBAL_KEY] = createState();
23
+ }
24
+ return globalScope[GLOBAL_KEY];
25
+ }
7
26
  function notify() {
8
- subscribers.forEach(subscriber => subscriber());
27
+ getState().subscribers.forEach(subscriber => subscriber());
9
28
  }
10
29
  function handleKeydown(event) {
11
30
  var _top$close;
12
31
  if (event.key !== 'Escape') {
13
32
  return;
14
33
  }
34
+ const {
35
+ stack
36
+ } = getState();
15
37
  const top = stack[stack.length - 1];
16
38
  top === null || top === void 0 || (_top$close = top.close) === null || _top$close === void 0 || _top$close.call(top);
17
39
  }
18
40
  function ensureKeydownListener() {
19
- if (!keydownAttached && stack.length > 0) {
20
- window.addEventListener('keydown', handleKeydown);
21
- keydownAttached = true;
41
+ const state = getState();
42
+ if (!state.keydownHandler && state.stack.length > 0) {
43
+ state.keydownHandler = handleKeydown;
44
+ window.addEventListener('keydown', state.keydownHandler);
22
45
  }
23
46
  }
24
47
  function teardownKeydownListener() {
25
- if (keydownAttached && stack.length === 0) {
26
- window.removeEventListener('keydown', handleKeydown);
27
- keydownAttached = false;
48
+ const state = getState();
49
+ if (state.keydownHandler && state.stack.length === 0) {
50
+ window.removeEventListener('keydown', state.keydownHandler);
51
+ state.keydownHandler = null;
28
52
  }
29
53
  }
30
54
  function registerLayer(entry) {
31
- stack.push(entry);
55
+ getState().stack.push(entry);
32
56
  ensureKeydownListener();
33
57
  notify();
34
58
  }
35
59
  function unregisterLayer(id) {
60
+ const {
61
+ stack
62
+ } = getState();
36
63
  const index = stack.findIndex(entry => entry.id === id);
37
64
  if (index !== -1) {
38
65
  stack.splice(index, 1);
@@ -41,7 +68,7 @@ function unregisterLayer(id) {
41
68
  notify();
42
69
  }
43
70
  function getDepth(id) {
44
- return stack.findIndex(entry => entry.id === id);
71
+ return getState().stack.findIndex(entry => entry.id === id);
45
72
  }
46
73
  export function useLayer({
47
74
  isOpen,
@@ -54,9 +81,9 @@ export function useLayer({
54
81
  const [, forceRender] = useState(0);
55
82
  useEffect(() => {
56
83
  const subscriber = () => forceRender(value => value + 1);
57
- subscribers.add(subscriber);
84
+ getState().subscribers.add(subscriber);
58
85
  return () => {
59
- subscribers.delete(subscriber);
86
+ getState().subscribers.delete(subscriber);
60
87
  };
61
88
  }, []);
62
89
  useLayoutEffect(() => {
@@ -2,12 +2,13 @@ import { useEffect } from 'react';
2
2
  export function useOnClickOutside(ref, handler) {
3
3
  useEffect(() => {
4
4
  const references = Array.isArray(ref) ? ref : [ref];
5
- function listener({
6
- target
7
- }) {
5
+ function listener(event) {
6
+ const target = event.target;
7
+ const path = typeof event.composedPath === 'function' ? event.composedPath() : [];
8
8
  const isClickInArea = references.some(reference => {
9
- var _reference$current;
10
- return (_reference$current = reference.current) === null || _reference$current === void 0 ? void 0 : _reference$current.contains(target);
9
+ const element = reference.current;
10
+ if (!element) return false;
11
+ return path.includes(element) || (target ? element.contains(target) : false);
11
12
  });
12
13
  if (!isClickInArea) {
13
14
  handler();
package/index.d.ts CHANGED
@@ -22,6 +22,8 @@ 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';
26
+ export { ComboboxMulti, type ComboboxMultiProps } from './ComboboxMulti';
25
27
  export { Switch } from './Switch';
26
28
  export { TextSkeleton, RectangularSkeleton, CircularSkeleton } from './Skeleton';
27
29
  export { Tooltip, type TooltipPosition, TooltipTrigger } from './Tooltip';
package/index.js CHANGED
@@ -22,6 +22,8 @@ export { ReCaptcha, useRecaptcha, asyncScriptLoad } from './ReCaptcha';
22
22
  export { Spinner } from './Spinner';
23
23
  export { SortButton } from './SortButton';
24
24
  export { Select, SelectAsync, CreatableSelect, SelectComponents, CLASS_NAME_PREFIX } from './Select';
25
+ export { Combobox } from './Combobox';
26
+ export { ComboboxMulti } from './ComboboxMulti';
25
27
  export { Switch } from './Switch';
26
28
  export { TextSkeleton, RectangularSkeleton, CircularSkeleton } from './Skeleton';
27
29
  export { Tooltip, TooltipTrigger } from './Tooltip';