@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.
- package/Combobox/Combobox.d.ts +4 -0
- package/Combobox/Combobox.js +254 -0
- package/Combobox/index.d.ts +2 -0
- package/Combobox/index.js +1 -0
- package/Combobox/types.d.ts +38 -0
- package/Combobox/types.js +1 -0
- package/ComboboxMulti/ComboboxMulti.d.ts +3 -0
- package/ComboboxMulti/ComboboxMulti.js +194 -0
- package/ComboboxMulti/index.d.ts +2 -0
- package/ComboboxMulti/index.js +1 -0
- package/ComboboxMulti/types.d.ts +32 -0
- package/ComboboxMulti/types.js +1 -0
- package/hooks/useLayer.js +41 -14
- package/hooks/useOnClickOutside.js +6 -5
- package/index.d.ts +2 -0
- package/index.js +2 -0
- package/package.json +2 -2
- package/shared/dropdownField/FieldInput.d.ts +5 -0
- package/shared/dropdownField/FieldInput.js +43 -0
- package/shared/dropdownField/FieldShell.d.ts +30 -0
- package/shared/dropdownField/FieldShell.js +99 -0
- package/shared/dropdownField/SuggestionsDropdown.d.ts +16 -0
- package/shared/dropdownField/SuggestionsDropdown.js +68 -0
- package/shared/dropdownField/constants.d.ts +6 -0
- package/shared/dropdownField/constants.js +6 -0
- package/shared/dropdownField/index.d.ts +12 -0
- package/shared/dropdownField/index.js +11 -0
- package/shared/dropdownField/shared.d.ts +4 -0
- package/shared/dropdownField/shared.js +18 -0
- package/shared/dropdownField/styles.d.ts +24 -0
- package/shared/dropdownField/styles.js +81 -0
- package/shared/dropdownField/types.d.ts +27 -0
- package/shared/dropdownField/types.js +1 -0
- package/shared/dropdownField/useActiveIndex.d.ts +2 -0
- package/shared/dropdownField/useActiveIndex.js +12 -0
- package/shared/dropdownField/useAsyncItems.d.ts +11 -0
- package/shared/dropdownField/useAsyncItems.js +46 -0
- package/shared/dropdownField/useDebouncedValue.d.ts +1 -0
- package/shared/dropdownField/useDebouncedValue.js +13 -0
- package/shared/dropdownField/useDropdownItems.d.ts +16 -0
- package/shared/dropdownField/useDropdownItems.js +36 -0
- package/shared/dropdownField/useDropdownPosition.d.ts +10 -0
- package/shared/dropdownField/useDropdownPosition.js +40 -0
- package/shared/dropdownField/useListNavigation.d.ts +16 -0
- 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 @@
|
|
|
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 @@
|
|
|
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
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
10
|
-
|
|
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';
|