@pushwoosh/dumb-components 1.1.140 → 1.1.142
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 +272 -0
- package/Combobox/index.d.ts +2 -0
- package/Combobox/index.js +1 -0
- package/Combobox/types.d.ts +40 -0
- package/Combobox/types.js +1 -0
- package/ComboboxMulti/ComboboxMulti.d.ts +3 -0
- package/ComboboxMulti/ComboboxMulti.js +203 -0
- package/ComboboxMulti/index.d.ts +2 -0
- package/ComboboxMulti/index.js +1 -0
- package/ComboboxMulti/types.d.ts +34 -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 +110 -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 +13 -0
- package/shared/dropdownField/index.js +12 -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 +11 -0
- package/shared/dropdownField/useDropdownPosition.js +41 -0
- package/shared/dropdownField/useFieldOpenEffects.d.ts +10 -0
- package/shared/dropdownField/useFieldOpenEffects.js +25 -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,272 @@
|
|
|
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, useFieldOpenEffects, useListNavigation, useShellMouseDown } from '../shared/dropdownField';
|
|
4
|
+
function FreeCombobox({
|
|
5
|
+
items,
|
|
6
|
+
loadItems,
|
|
7
|
+
debounceMs = DEFAULT_DEBOUNCE_MS,
|
|
8
|
+
value,
|
|
9
|
+
onChange,
|
|
10
|
+
onClose,
|
|
11
|
+
autoFocus,
|
|
12
|
+
getLabel: getLabelProp,
|
|
13
|
+
filter,
|
|
14
|
+
renderItem,
|
|
15
|
+
renderEmpty,
|
|
16
|
+
placeholder,
|
|
17
|
+
disabled,
|
|
18
|
+
loading,
|
|
19
|
+
clearable,
|
|
20
|
+
autosize,
|
|
21
|
+
$isErrored,
|
|
22
|
+
width,
|
|
23
|
+
minWidth
|
|
24
|
+
}) {
|
|
25
|
+
const inputRef = useRef(null);
|
|
26
|
+
const baseId = useId();
|
|
27
|
+
const listboxId = `${baseId}-list`;
|
|
28
|
+
const getLabel = useMemo(() => getLabelProp ?? (item => item), [getLabelProp]);
|
|
29
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
30
|
+
const {
|
|
31
|
+
filteredItems,
|
|
32
|
+
loading: asyncLoading
|
|
33
|
+
} = useDropdownItems({
|
|
34
|
+
items,
|
|
35
|
+
loadItems,
|
|
36
|
+
debounceMs,
|
|
37
|
+
getLabel,
|
|
38
|
+
filter,
|
|
39
|
+
query: value,
|
|
40
|
+
isOpen
|
|
41
|
+
});
|
|
42
|
+
const [activeIndex, setActiveIndex] = useActiveIndex(filteredItems.length, -1);
|
|
43
|
+
const open = useCallback(() => setIsOpen(true), []);
|
|
44
|
+
const close = useCallback(() => {
|
|
45
|
+
setIsOpen(false);
|
|
46
|
+
setActiveIndex(-1);
|
|
47
|
+
}, [setActiveIndex]);
|
|
48
|
+
const pick = useCallback(item => {
|
|
49
|
+
onChange(getLabel(item));
|
|
50
|
+
setIsOpen(false);
|
|
51
|
+
setActiveIndex(-1);
|
|
52
|
+
}, [onChange, getLabel, setActiveIndex]);
|
|
53
|
+
const onInputKeyDown = useListNavigation({
|
|
54
|
+
isOpen,
|
|
55
|
+
open,
|
|
56
|
+
itemCount: filteredItems.length,
|
|
57
|
+
activeIndex,
|
|
58
|
+
setActiveIndex,
|
|
59
|
+
onSelectIndex: index => pick(filteredItems[index]),
|
|
60
|
+
onEscape: close
|
|
61
|
+
});
|
|
62
|
+
const onShellMouseDown = useShellMouseDown(inputRef, isOpen, open);
|
|
63
|
+
useFieldOpenEffects({
|
|
64
|
+
isOpen,
|
|
65
|
+
open,
|
|
66
|
+
onClose,
|
|
67
|
+
autoFocus,
|
|
68
|
+
inputRef
|
|
69
|
+
});
|
|
70
|
+
const dropdownItems = useMemo(() => filteredItems.map((item, idx) => {
|
|
71
|
+
const selected = getLabel(item) === value;
|
|
72
|
+
const state = {
|
|
73
|
+
active: idx === activeIndex,
|
|
74
|
+
selected
|
|
75
|
+
};
|
|
76
|
+
return {
|
|
77
|
+
key: item,
|
|
78
|
+
id: `${baseId}-opt-${item}`,
|
|
79
|
+
label: renderItem ? renderItem(item, state) : getLabel(item),
|
|
80
|
+
active: state.active,
|
|
81
|
+
selected,
|
|
82
|
+
onSelect: () => pick(item)
|
|
83
|
+
};
|
|
84
|
+
}), [filteredItems, activeIndex, value, getLabel, renderItem, pick, baseId]);
|
|
85
|
+
const dropdownVisible = isOpen && filteredItems.length > 0;
|
|
86
|
+
const activeDescendant = activeIndex >= 0 && activeIndex < filteredItems.length ? `${baseId}-opt-${filteredItems[activeIndex]}` : undefined;
|
|
87
|
+
const onInputChange = event => {
|
|
88
|
+
onChange(event.target.value);
|
|
89
|
+
if (!isOpen) open();
|
|
90
|
+
};
|
|
91
|
+
return _jsx(FieldShell, {
|
|
92
|
+
isOpen: isOpen,
|
|
93
|
+
dropdownOpen: dropdownVisible,
|
|
94
|
+
isErrored: $isErrored,
|
|
95
|
+
disabled: disabled,
|
|
96
|
+
loading: loading || asyncLoading,
|
|
97
|
+
width: width,
|
|
98
|
+
minWidth: minWidth,
|
|
99
|
+
autosize: autosize,
|
|
100
|
+
onShellMouseDown: onShellMouseDown,
|
|
101
|
+
onClose: close,
|
|
102
|
+
showClear: !!(clearable && value),
|
|
103
|
+
onClear: () => {
|
|
104
|
+
var _inputRef$current;
|
|
105
|
+
onChange('');
|
|
106
|
+
(_inputRef$current = inputRef.current) === null || _inputRef$current === void 0 || _inputRef$current.focus();
|
|
107
|
+
},
|
|
108
|
+
dropdownItems: dropdownItems,
|
|
109
|
+
emptyText: renderEmpty ? renderEmpty(value) : undefined,
|
|
110
|
+
listboxId: listboxId,
|
|
111
|
+
children: _jsx(FieldInput, {
|
|
112
|
+
ref: inputRef,
|
|
113
|
+
autosize: autosize,
|
|
114
|
+
value: value,
|
|
115
|
+
onChange: onInputChange,
|
|
116
|
+
onKeyDown: onInputKeyDown,
|
|
117
|
+
onFocus: open,
|
|
118
|
+
placeholder: placeholder,
|
|
119
|
+
disabled: disabled,
|
|
120
|
+
role: "combobox",
|
|
121
|
+
"aria-expanded": dropdownVisible,
|
|
122
|
+
"aria-controls": listboxId,
|
|
123
|
+
"aria-autocomplete": "list",
|
|
124
|
+
"aria-activedescendant": activeDescendant
|
|
125
|
+
})
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
function SelectCombobox({
|
|
129
|
+
items,
|
|
130
|
+
loadItems,
|
|
131
|
+
debounceMs = DEFAULT_DEBOUNCE_MS,
|
|
132
|
+
value,
|
|
133
|
+
onChange,
|
|
134
|
+
onClose,
|
|
135
|
+
autoFocus,
|
|
136
|
+
getLabel,
|
|
137
|
+
getKey: getKeyProp,
|
|
138
|
+
filter,
|
|
139
|
+
renderItem,
|
|
140
|
+
renderEmpty,
|
|
141
|
+
placeholder,
|
|
142
|
+
disabled,
|
|
143
|
+
loading,
|
|
144
|
+
clearable,
|
|
145
|
+
autosize,
|
|
146
|
+
$isErrored,
|
|
147
|
+
width,
|
|
148
|
+
minWidth
|
|
149
|
+
}) {
|
|
150
|
+
const inputRef = useRef(null);
|
|
151
|
+
const baseId = useId();
|
|
152
|
+
const listboxId = `${baseId}-list`;
|
|
153
|
+
const optionId = key => `${baseId}-opt-${key}`;
|
|
154
|
+
const getKey = getKeyProp ?? defaultGetKey;
|
|
155
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
156
|
+
const [query, setQuery] = useState('');
|
|
157
|
+
const {
|
|
158
|
+
sourceItems,
|
|
159
|
+
filteredItems,
|
|
160
|
+
loading: asyncLoading
|
|
161
|
+
} = useDropdownItems({
|
|
162
|
+
items,
|
|
163
|
+
loadItems,
|
|
164
|
+
debounceMs,
|
|
165
|
+
getLabel,
|
|
166
|
+
filter,
|
|
167
|
+
query,
|
|
168
|
+
isOpen
|
|
169
|
+
});
|
|
170
|
+
const [activeIndex, setActiveIndex] = useActiveIndex(filteredItems.length, 0);
|
|
171
|
+
const hasValue = value !== null && value !== undefined;
|
|
172
|
+
const valueLabel = hasValue ? getLabel(value) : '';
|
|
173
|
+
const selectedKey = hasValue ? getKey(value) : null;
|
|
174
|
+
const inputValue = isOpen ? query : valueLabel;
|
|
175
|
+
const open = useCallback(() => {
|
|
176
|
+
if (isOpen) return;
|
|
177
|
+
setIsOpen(true);
|
|
178
|
+
setQuery('');
|
|
179
|
+
const idx = sourceItems.findIndex(it => getKey(it) === selectedKey);
|
|
180
|
+
setActiveIndex(idx >= 0 ? idx : 0);
|
|
181
|
+
}, [isOpen, sourceItems, getKey, selectedKey, setActiveIndex]);
|
|
182
|
+
const close = useCallback(() => {
|
|
183
|
+
setIsOpen(false);
|
|
184
|
+
setQuery('');
|
|
185
|
+
}, []);
|
|
186
|
+
const commit = useCallback(item => {
|
|
187
|
+
var _inputRef$current2;
|
|
188
|
+
onChange(item);
|
|
189
|
+
setIsOpen(false);
|
|
190
|
+
setQuery('');
|
|
191
|
+
setActiveIndex(0);
|
|
192
|
+
(_inputRef$current2 = inputRef.current) === null || _inputRef$current2 === void 0 || _inputRef$current2.blur();
|
|
193
|
+
}, [onChange, setActiveIndex]);
|
|
194
|
+
const onInputKeyDown = useListNavigation({
|
|
195
|
+
isOpen,
|
|
196
|
+
open,
|
|
197
|
+
itemCount: filteredItems.length,
|
|
198
|
+
activeIndex,
|
|
199
|
+
setActiveIndex,
|
|
200
|
+
onSelectIndex: index => commit(filteredItems[index]),
|
|
201
|
+
onEscape: close
|
|
202
|
+
});
|
|
203
|
+
const onShellMouseDown = useShellMouseDown(inputRef, isOpen, open);
|
|
204
|
+
useFieldOpenEffects({
|
|
205
|
+
isOpen,
|
|
206
|
+
open,
|
|
207
|
+
onClose,
|
|
208
|
+
autoFocus,
|
|
209
|
+
inputRef
|
|
210
|
+
});
|
|
211
|
+
const dropdownItems = useMemo(() => filteredItems.map((item, idx) => {
|
|
212
|
+
const key = getKey(item);
|
|
213
|
+
const selected = key === selectedKey;
|
|
214
|
+
const state = {
|
|
215
|
+
active: idx === activeIndex,
|
|
216
|
+
selected
|
|
217
|
+
};
|
|
218
|
+
return {
|
|
219
|
+
key,
|
|
220
|
+
id: `${baseId}-opt-${key}`,
|
|
221
|
+
label: renderItem ? renderItem(item, state) : getLabel(item),
|
|
222
|
+
active: state.active,
|
|
223
|
+
selected,
|
|
224
|
+
onSelect: () => commit(item)
|
|
225
|
+
};
|
|
226
|
+
}), [filteredItems, activeIndex, selectedKey, getLabel, getKey, renderItem, commit, baseId]);
|
|
227
|
+
const activeDescendant = activeIndex >= 0 && activeIndex < filteredItems.length ? optionId(getKey(filteredItems[activeIndex])) : undefined;
|
|
228
|
+
return _jsx(FieldShell, {
|
|
229
|
+
isOpen: isOpen,
|
|
230
|
+
dropdownOpen: isOpen,
|
|
231
|
+
isErrored: $isErrored,
|
|
232
|
+
disabled: disabled,
|
|
233
|
+
loading: loading || asyncLoading,
|
|
234
|
+
width: width,
|
|
235
|
+
minWidth: minWidth,
|
|
236
|
+
autosize: autosize,
|
|
237
|
+
onShellMouseDown: onShellMouseDown,
|
|
238
|
+
onClose: close,
|
|
239
|
+
showClear: !!(clearable && hasValue),
|
|
240
|
+
onClear: () => {
|
|
241
|
+
var _inputRef$current3;
|
|
242
|
+
onChange(null);
|
|
243
|
+
(_inputRef$current3 = inputRef.current) === null || _inputRef$current3 === void 0 || _inputRef$current3.focus();
|
|
244
|
+
},
|
|
245
|
+
dropdownItems: dropdownItems,
|
|
246
|
+
emptyText: renderEmpty ? renderEmpty(query) : undefined,
|
|
247
|
+
listboxId: listboxId,
|
|
248
|
+
children: _jsx(FieldInput, {
|
|
249
|
+
ref: inputRef,
|
|
250
|
+
autosize: autosize,
|
|
251
|
+
value: inputValue,
|
|
252
|
+
onChange: event => setQuery(event.target.value),
|
|
253
|
+
onKeyDown: onInputKeyDown,
|
|
254
|
+
onFocus: open,
|
|
255
|
+
placeholder: placeholder,
|
|
256
|
+
disabled: disabled,
|
|
257
|
+
role: "combobox",
|
|
258
|
+
"aria-expanded": isOpen,
|
|
259
|
+
"aria-controls": listboxId,
|
|
260
|
+
"aria-autocomplete": "list",
|
|
261
|
+
"aria-activedescendant": activeDescendant
|
|
262
|
+
})
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
export function Combobox(props) {
|
|
266
|
+
if (props.select) return _jsx(SelectCombobox, {
|
|
267
|
+
...props
|
|
268
|
+
});
|
|
269
|
+
return _jsx(FreeCombobox, {
|
|
270
|
+
...props
|
|
271
|
+
});
|
|
272
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Combobox } from './Combobox';
|
|
@@ -0,0 +1,40 @@
|
|
|
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
|
+
onClose?: () => void;
|
|
17
|
+
autoFocus?: boolean;
|
|
18
|
+
placeholder?: string;
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
loading?: boolean;
|
|
21
|
+
clearable?: boolean;
|
|
22
|
+
autosize?: boolean;
|
|
23
|
+
$isErrored?: boolean;
|
|
24
|
+
width?: string;
|
|
25
|
+
minWidth?: string;
|
|
26
|
+
};
|
|
27
|
+
export type FreeComboboxProps = ComboboxSource<string> & CommonProps<string> & {
|
|
28
|
+
select?: false;
|
|
29
|
+
value: string;
|
|
30
|
+
onChange: (value: string) => void;
|
|
31
|
+
getLabel?: (item: string) => string;
|
|
32
|
+
};
|
|
33
|
+
export type SelectComboboxProps<T> = ComboboxSource<T> & GetKeyProp<T> & CommonProps<T> & {
|
|
34
|
+
select: true;
|
|
35
|
+
value: T | null;
|
|
36
|
+
onChange: (value: T | null) => void;
|
|
37
|
+
getLabel: (item: T) => string;
|
|
38
|
+
};
|
|
39
|
+
export type ComboboxProps<T> = FreeComboboxProps | SelectComboboxProps<T>;
|
|
40
|
+
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, onClose, autoFocus, 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,203 @@
|
|
|
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, useFieldOpenEffects, 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
|
+
onClose,
|
|
18
|
+
autoFocus,
|
|
19
|
+
getLabel,
|
|
20
|
+
getKey: getKeyProp,
|
|
21
|
+
filter,
|
|
22
|
+
renderItem,
|
|
23
|
+
renderEmpty,
|
|
24
|
+
creatable,
|
|
25
|
+
onCreate,
|
|
26
|
+
canCreate,
|
|
27
|
+
renderCreateLabel,
|
|
28
|
+
maxValues,
|
|
29
|
+
placeholder,
|
|
30
|
+
disabled,
|
|
31
|
+
loading,
|
|
32
|
+
clearable,
|
|
33
|
+
autosize,
|
|
34
|
+
$isErrored,
|
|
35
|
+
width,
|
|
36
|
+
minWidth
|
|
37
|
+
}) {
|
|
38
|
+
const inputRef = useRef(null);
|
|
39
|
+
const isAsync = !!loadItems;
|
|
40
|
+
const getKey = getKeyProp ?? defaultGetKey;
|
|
41
|
+
const baseId = useId();
|
|
42
|
+
const listboxId = `${baseId}-list`;
|
|
43
|
+
const createId = `${baseId}-create`;
|
|
44
|
+
const optionId = key => `${baseId}-opt-${key}`;
|
|
45
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
46
|
+
const [query, setQuery] = useState('');
|
|
47
|
+
const [createdItems, setCreatedItems] = useState([]);
|
|
48
|
+
const {
|
|
49
|
+
sourceItems,
|
|
50
|
+
filteredItems,
|
|
51
|
+
loading: asyncLoading
|
|
52
|
+
} = useDropdownItems({
|
|
53
|
+
items,
|
|
54
|
+
loadItems,
|
|
55
|
+
localItems: createdItems,
|
|
56
|
+
debounceMs,
|
|
57
|
+
getLabel,
|
|
58
|
+
filter,
|
|
59
|
+
query,
|
|
60
|
+
isOpen
|
|
61
|
+
});
|
|
62
|
+
const selectedKeys = useMemo(() => {
|
|
63
|
+
const set = new Set();
|
|
64
|
+
value.forEach(item => set.add(getKey(item)));
|
|
65
|
+
return set;
|
|
66
|
+
}, [value, getKey]);
|
|
67
|
+
const visibleItems = useMemo(() => filteredItems.filter(item => !selectedKeys.has(getKey(item))), [filteredItems, selectedKeys, getKey]);
|
|
68
|
+
const atMax = maxValues !== undefined && value.length >= maxValues;
|
|
69
|
+
const canCreateNow = !!(creatable && query.trim() && !atMax && (canCreate ? canCreate(query, sourceItems) : defaultCanCreate(query, sourceItems, getLabel)));
|
|
70
|
+
const totalRows = visibleItems.length + (canCreateNow ? 1 : 0);
|
|
71
|
+
const [activeIndex, setActiveIndex] = useActiveIndex(totalRows, 0);
|
|
72
|
+
const open = useCallback(() => setIsOpen(true), []);
|
|
73
|
+
const close = useCallback(() => {
|
|
74
|
+
setIsOpen(false);
|
|
75
|
+
setQuery('');
|
|
76
|
+
}, []);
|
|
77
|
+
const toggle = useCallback(item => {
|
|
78
|
+
var _inputRef$current;
|
|
79
|
+
const key = getKey(item);
|
|
80
|
+
if (selectedKeys.has(key)) {
|
|
81
|
+
onChange(value.filter(v => getKey(v) !== key));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (maxValues !== undefined && value.length >= maxValues) return;
|
|
85
|
+
onChange([...value, item]);
|
|
86
|
+
setIsOpen(true);
|
|
87
|
+
(_inputRef$current = inputRef.current) === null || _inputRef$current === void 0 || _inputRef$current.focus();
|
|
88
|
+
}, [value, getKey, selectedKeys, onChange, maxValues]);
|
|
89
|
+
const create = useCallback(() => {
|
|
90
|
+
const trimmed = query.trim();
|
|
91
|
+
if (!trimmed || atMax) return;
|
|
92
|
+
const item = onCreate ? onCreate(trimmed) : trimmed;
|
|
93
|
+
if (!isAsync) setCreatedItems(prev => [...prev, item]);
|
|
94
|
+
onChange([...value, item]);
|
|
95
|
+
setQuery('');
|
|
96
|
+
setActiveIndex(0);
|
|
97
|
+
}, [query, atMax, onCreate, isAsync, onChange, value, setActiveIndex]);
|
|
98
|
+
const onInputKeyDown = useListNavigation({
|
|
99
|
+
isOpen,
|
|
100
|
+
open,
|
|
101
|
+
itemCount: visibleItems.length,
|
|
102
|
+
activeIndex,
|
|
103
|
+
setActiveIndex,
|
|
104
|
+
onSelectIndex: index => toggle(visibleItems[index]),
|
|
105
|
+
onEscape: close,
|
|
106
|
+
hasExtraRow: canCreateNow,
|
|
107
|
+
onSelectExtra: create,
|
|
108
|
+
isQueryEmpty: !query && value.length > 0,
|
|
109
|
+
onBackspaceEmpty: () => onChange(value.slice(0, -1))
|
|
110
|
+
});
|
|
111
|
+
const onShellMouseDown = useShellMouseDown(inputRef, isOpen, open);
|
|
112
|
+
useFieldOpenEffects({
|
|
113
|
+
isOpen,
|
|
114
|
+
open,
|
|
115
|
+
onClose,
|
|
116
|
+
autoFocus,
|
|
117
|
+
inputRef
|
|
118
|
+
});
|
|
119
|
+
const toggleOpen = useCallback(() => {
|
|
120
|
+
if (isOpen) {
|
|
121
|
+
var _inputRef$current2;
|
|
122
|
+
close();
|
|
123
|
+
(_inputRef$current2 = inputRef.current) === null || _inputRef$current2 === void 0 || _inputRef$current2.blur();
|
|
124
|
+
} else {
|
|
125
|
+
var _inputRef$current3;
|
|
126
|
+
open();
|
|
127
|
+
(_inputRef$current3 = inputRef.current) === null || _inputRef$current3 === void 0 || _inputRef$current3.focus();
|
|
128
|
+
}
|
|
129
|
+
}, [isOpen, open, close]);
|
|
130
|
+
const itemOffset = canCreateNow ? 1 : 0;
|
|
131
|
+
const dropdownItems = useMemo(() => visibleItems.map((item, idx) => {
|
|
132
|
+
const key = getKey(item);
|
|
133
|
+
const state = {
|
|
134
|
+
active: idx + itemOffset === activeIndex,
|
|
135
|
+
selected: false
|
|
136
|
+
};
|
|
137
|
+
return {
|
|
138
|
+
key,
|
|
139
|
+
id: `${baseId}-opt-${key}`,
|
|
140
|
+
label: renderItem ? renderItem(item, state) : getLabel(item),
|
|
141
|
+
active: state.active,
|
|
142
|
+
selected: state.selected,
|
|
143
|
+
onSelect: () => toggle(item)
|
|
144
|
+
};
|
|
145
|
+
}), [visibleItems, itemOffset, activeIndex, renderItem, getLabel, getKey, toggle, baseId]);
|
|
146
|
+
const createActive = canCreateNow && activeIndex === 0;
|
|
147
|
+
const activeItem = visibleItems[activeIndex - itemOffset];
|
|
148
|
+
let activeDescendant;
|
|
149
|
+
if (createActive) activeDescendant = createId;else if (activeItem !== undefined) activeDescendant = optionId(getKey(activeItem));
|
|
150
|
+
let createLabel;
|
|
151
|
+
if (canCreateNow) {
|
|
152
|
+
createLabel = renderCreateLabel ? renderCreateLabel(query.trim()) : `Create "${query.trim()}"`;
|
|
153
|
+
}
|
|
154
|
+
const hasTags = value.length > 0;
|
|
155
|
+
return _jsxs(FieldShell, {
|
|
156
|
+
isOpen: isOpen,
|
|
157
|
+
dropdownOpen: isOpen,
|
|
158
|
+
isErrored: $isErrored,
|
|
159
|
+
disabled: disabled,
|
|
160
|
+
loading: loading || asyncLoading,
|
|
161
|
+
width: width,
|
|
162
|
+
minWidth: minWidth,
|
|
163
|
+
autosize: autosize,
|
|
164
|
+
tightLeft: hasTags,
|
|
165
|
+
onShellMouseDown: onShellMouseDown,
|
|
166
|
+
onClose: close,
|
|
167
|
+
showClear: !!(clearable && hasTags),
|
|
168
|
+
onClear: () => onChange([]),
|
|
169
|
+
clearLabel: "Clear all",
|
|
170
|
+
showChevron: true,
|
|
171
|
+
onToggle: toggleOpen,
|
|
172
|
+
dropdownItems: dropdownItems,
|
|
173
|
+
emptyText: renderEmpty ? renderEmpty(query) : undefined,
|
|
174
|
+
createLabel: createLabel,
|
|
175
|
+
createActive: createActive,
|
|
176
|
+
onCreate: create,
|
|
177
|
+
listboxId: listboxId,
|
|
178
|
+
createId: createId,
|
|
179
|
+
children: [value.map(item => {
|
|
180
|
+
const key = getKey(item);
|
|
181
|
+
return _jsx(Chip, {
|
|
182
|
+
"$readOnly": disabled,
|
|
183
|
+
"$onClose": disabled ? undefined : () => onChange(value.filter(v => getKey(v) !== key)),
|
|
184
|
+
"$maxWidth": "100%",
|
|
185
|
+
children: getLabel(item)
|
|
186
|
+
}, key);
|
|
187
|
+
}), _jsx(FieldInput, {
|
|
188
|
+
ref: inputRef,
|
|
189
|
+
autosize: autosize,
|
|
190
|
+
value: query,
|
|
191
|
+
onChange: e => setQuery(e.target.value),
|
|
192
|
+
onKeyDown: onInputKeyDown,
|
|
193
|
+
onFocus: open,
|
|
194
|
+
placeholder: hasTags ? undefined : placeholder,
|
|
195
|
+
disabled: disabled,
|
|
196
|
+
role: "combobox",
|
|
197
|
+
"aria-expanded": isOpen,
|
|
198
|
+
"aria-controls": listboxId,
|
|
199
|
+
"aria-autocomplete": "list",
|
|
200
|
+
"aria-activedescendant": activeDescendant
|
|
201
|
+
})]
|
|
202
|
+
});
|
|
203
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ComboboxMulti } from './ComboboxMulti';
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
onClose?: () => void;
|
|
17
|
+
autoFocus?: boolean;
|
|
18
|
+
getLabel: (item: T) => string;
|
|
19
|
+
filter?: (item: T, query: string) => boolean;
|
|
20
|
+
renderItem?: (item: T, state: ItemState) => ReactNode;
|
|
21
|
+
renderEmpty?: (query: string) => ReactNode;
|
|
22
|
+
canCreate?: (query: string, items: T[]) => boolean;
|
|
23
|
+
renderCreateLabel?: (query: string) => ReactNode;
|
|
24
|
+
maxValues?: number;
|
|
25
|
+
placeholder?: string;
|
|
26
|
+
disabled?: boolean;
|
|
27
|
+
loading?: boolean;
|
|
28
|
+
clearable?: boolean;
|
|
29
|
+
autosize?: boolean;
|
|
30
|
+
$isErrored?: boolean;
|
|
31
|
+
width?: string;
|
|
32
|
+
minWidth?: string;
|
|
33
|
+
};
|
|
34
|
+
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(() => {
|