@acusti/dropdown 0.38.7 → 0.40.0
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/dist/Dropdown.d.ts +1 -23
- package/dist/Dropdown.js +328 -461
- package/dist/Dropdown.js.flow +58 -58
- package/dist/Dropdown.test.js +47 -80
- package/dist/helpers.d.ts +32 -47
- package/dist/helpers.js +36 -35
- package/dist/helpers.js.flow +36 -36
- package/dist/styles.d.ts +13 -14
- package/dist/styles.js +1 -1
- package/dist/styles.js.flow +12 -12
- package/package.json +3 -3
package/dist/Dropdown.js
CHANGED
|
@@ -2,57 +2,16 @@
|
|
|
2
2
|
import InputText from '@acusti/input-text';
|
|
3
3
|
import { Style } from '@acusti/styling';
|
|
4
4
|
import useIsOutOfBounds from '@acusti/use-is-out-of-bounds';
|
|
5
|
-
import useKeyboardEvents, {
|
|
6
|
-
isEventTargetUsingKeyEvent,
|
|
7
|
-
} from '@acusti/use-keyboard-events';
|
|
5
|
+
import useKeyboardEvents, { isEventTargetUsingKeyEvent, } from '@acusti/use-keyboard-events';
|
|
8
6
|
import clsx from 'clsx';
|
|
9
7
|
import * as React from 'react';
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
getItemElements,
|
|
13
|
-
ITEM_SELECTOR,
|
|
14
|
-
setActiveItem,
|
|
15
|
-
} from './helpers.js';
|
|
16
|
-
import {
|
|
17
|
-
BODY_CLASS_NAME,
|
|
18
|
-
BODY_MAX_HEIGHT_VAR,
|
|
19
|
-
BODY_MAX_WIDTH_VAR,
|
|
20
|
-
BODY_SELECTOR,
|
|
21
|
-
LABEL_CLASS_NAME,
|
|
22
|
-
LABEL_TEXT_CLASS_NAME,
|
|
23
|
-
ROOT_CLASS_NAME,
|
|
24
|
-
STYLES,
|
|
25
|
-
TRIGGER_CLASS_NAME,
|
|
26
|
-
} from './styles.js';
|
|
8
|
+
import { getActiveItemElement, getItemElements, ITEM_SELECTOR, setActiveItem, } from './helpers.js';
|
|
9
|
+
import { BODY_CLASS_NAME, BODY_MAX_HEIGHT_VAR, BODY_MAX_WIDTH_VAR, BODY_SELECTOR, LABEL_CLASS_NAME, LABEL_TEXT_CLASS_NAME, ROOT_CLASS_NAME, STYLES, TRIGGER_CLASS_NAME, } from './styles.js';
|
|
27
10
|
const { Children, Fragment, useCallback, useEffect, useMemo, useRef, useState } = React;
|
|
28
|
-
const noop = () => {}; // eslint-disable-line @typescript-eslint/no-empty-function
|
|
29
|
-
const CHILDREN_ERROR =
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
'input:not([type=radio]):not([type=checkbox]):not([type=range]),textarea';
|
|
33
|
-
export default function Dropdown({
|
|
34
|
-
allowCreate,
|
|
35
|
-
allowEmpty = true,
|
|
36
|
-
children,
|
|
37
|
-
className,
|
|
38
|
-
disabled,
|
|
39
|
-
hasItems = true,
|
|
40
|
-
isOpenOnMount,
|
|
41
|
-
isSearchable,
|
|
42
|
-
keepOpenOnSubmit = !hasItems,
|
|
43
|
-
label,
|
|
44
|
-
name,
|
|
45
|
-
onClick,
|
|
46
|
-
onClose,
|
|
47
|
-
onMouseDown,
|
|
48
|
-
onMouseUp,
|
|
49
|
-
onOpen,
|
|
50
|
-
onSubmitItem,
|
|
51
|
-
placeholder,
|
|
52
|
-
style: styleFromProps,
|
|
53
|
-
tabIndex,
|
|
54
|
-
value,
|
|
55
|
-
}) {
|
|
11
|
+
const noop = () => { }; // eslint-disable-line @typescript-eslint/no-empty-function
|
|
12
|
+
const CHILDREN_ERROR = '@acusti/dropdown requires either 1 child (the dropdown body) or 2 children: the dropdown trigger and the dropdown body.';
|
|
13
|
+
const TEXT_INPUT_SELECTOR = 'input:not([type=radio]):not([type=checkbox]):not([type=range]),textarea';
|
|
14
|
+
export default function Dropdown({ allowCreate, allowEmpty = true, children, className, disabled, hasItems = true, isOpenOnMount, isSearchable, keepOpenOnSubmit = !hasItems, label, name, onClick, onClose, onMouseDown, onMouseUp, onOpen, onSubmitItem, placeholder, style: styleFromProps, tabIndex, value, }) {
|
|
56
15
|
const childrenCount = Children.count(children);
|
|
57
16
|
if (childrenCount !== 1 && childrenCount !== 2) {
|
|
58
17
|
if (childrenCount === 0) {
|
|
@@ -65,9 +24,7 @@ export default function Dropdown({
|
|
|
65
24
|
trigger = children[0];
|
|
66
25
|
}
|
|
67
26
|
const isTriggerFromProps = React.isValidElement(trigger);
|
|
68
|
-
const [isOpen, setIsOpen] = useState(
|
|
69
|
-
isOpenOnMount !== null && isOpenOnMount !== void 0 ? isOpenOnMount : false,
|
|
70
|
-
);
|
|
27
|
+
const [isOpen, setIsOpen] = useState(isOpenOnMount !== null && isOpenOnMount !== void 0 ? isOpenOnMount : false);
|
|
71
28
|
const [isOpening, setIsOpening] = useState(!isOpenOnMount);
|
|
72
29
|
const [dropdownBodyElement, setDropdownBodyElement] = useState(null);
|
|
73
30
|
const dropdownElementRef = useRef(null);
|
|
@@ -125,7 +82,8 @@ export default function Dropdown({
|
|
|
125
82
|
}
|
|
126
83
|
if (isOpen && onOpenRef.current) {
|
|
127
84
|
onOpenRef.current();
|
|
128
|
-
}
|
|
85
|
+
}
|
|
86
|
+
else if (!isOpen && onCloseRef.current) {
|
|
129
87
|
onCloseRef.current();
|
|
130
88
|
}
|
|
131
89
|
}, [isOpen]);
|
|
@@ -138,94 +96,76 @@ export default function Dropdown({
|
|
|
138
96
|
closingTimerRef.current = null;
|
|
139
97
|
}
|
|
140
98
|
}, []);
|
|
141
|
-
const handleSubmitItem = useCallback(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
keepOpen.dataset.uktKeepOpen === 'false'
|
|
154
|
-
) {
|
|
155
|
-
// A short timeout before closing is better UX when user selects an item so dropdown
|
|
156
|
-
// doesn’t close before expected. It also enables using <Link />s in the dropdown body.
|
|
157
|
-
closingTimerRef.current = setTimeout(closeDropdown, 90);
|
|
158
|
-
}
|
|
99
|
+
const handleSubmitItem = useCallback((event) => {
|
|
100
|
+
var _a, _b, _c;
|
|
101
|
+
const eventTarget = event.target;
|
|
102
|
+
if (isOpenRef.current && !keepOpenOnSubmitRef.current) {
|
|
103
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
|
104
|
+
const keepOpen = eventTarget.closest('[data-ukt-keep-open]');
|
|
105
|
+
// Don’t close dropdown if event occurs w/in data-ukt-keep-open element
|
|
106
|
+
if (!(keepOpen === null || keepOpen === void 0 ? void 0 : keepOpen.dataset.uktKeepOpen) ||
|
|
107
|
+
keepOpen.dataset.uktKeepOpen === 'false') {
|
|
108
|
+
// A short timeout before closing is better UX when user selects an item so dropdown
|
|
109
|
+
// doesn’t close before expected. It also enables using <Link />s in the dropdown body.
|
|
110
|
+
closingTimerRef.current = setTimeout(closeDropdown, 90);
|
|
159
111
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
112
|
+
}
|
|
113
|
+
if (!hasItemsRef.current)
|
|
114
|
+
return;
|
|
115
|
+
const element = getActiveItemElement(dropdownElementRef.current);
|
|
116
|
+
if (!element && !allowCreateRef.current) {
|
|
117
|
+
// If not allowEmpty, don’t allow submitting an empty item
|
|
118
|
+
if (!allowEmptyRef.current)
|
|
119
|
+
return;
|
|
120
|
+
// If we have an input element as trigger & the user didn’t clear the text, do nothing
|
|
121
|
+
if ((_a = inputElementRef.current) === null || _a === void 0 ? void 0 : _a.value)
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
let label = (_b = element === null || element === void 0 ? void 0 : element.innerText) !== null && _b !== void 0 ? _b : '';
|
|
125
|
+
if (inputElementRef.current) {
|
|
126
|
+
if (!element) {
|
|
127
|
+
label = inputElementRef.current.value;
|
|
172
128
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
element === null || element === void 0
|
|
176
|
-
? void 0
|
|
177
|
-
: element.innerText) !== null && _b !== void 0
|
|
178
|
-
? _b
|
|
179
|
-
: '';
|
|
180
|
-
if (inputElementRef.current) {
|
|
181
|
-
if (!element) {
|
|
182
|
-
label = inputElementRef.current.value;
|
|
183
|
-
} else {
|
|
184
|
-
inputElementRef.current.value = label;
|
|
185
|
-
}
|
|
186
|
-
if (
|
|
187
|
-
inputElementRef.current ===
|
|
188
|
-
inputElementRef.current.ownerDocument.activeElement
|
|
189
|
-
) {
|
|
190
|
-
inputElementRef.current.blur();
|
|
191
|
-
}
|
|
129
|
+
else {
|
|
130
|
+
inputElementRef.current.value = label;
|
|
192
131
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
? void 0
|
|
197
|
-
: element.dataset.uktValue) !== null && _c !== void 0
|
|
198
|
-
? _c
|
|
199
|
-
: label;
|
|
200
|
-
// If parent is controlling Dropdown via props.value and nextValue is the same, do nothing
|
|
201
|
-
if (valueRef.current && valueRef.current === nextValue) return;
|
|
202
|
-
if (onSubmitItemRef.current) {
|
|
203
|
-
onSubmitItemRef.current({ element, event, label, value: nextValue });
|
|
132
|
+
if (inputElementRef.current ===
|
|
133
|
+
inputElementRef.current.ownerDocument.activeElement) {
|
|
134
|
+
inputElementRef.current.blur();
|
|
204
135
|
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
|
|
136
|
+
}
|
|
137
|
+
const nextValue = (_c = element === null || element === void 0 ? void 0 : element.dataset.uktValue) !== null && _c !== void 0 ? _c : label;
|
|
138
|
+
// If parent is controlling Dropdown via props.value and nextValue is the same, do nothing
|
|
139
|
+
if (valueRef.current && valueRef.current === nextValue)
|
|
140
|
+
return;
|
|
141
|
+
if (onSubmitItemRef.current) {
|
|
142
|
+
onSubmitItemRef.current({ element, event, label, value: nextValue });
|
|
143
|
+
}
|
|
144
|
+
}, [closeDropdown]);
|
|
208
145
|
const handleMouseMove = useCallback(({ clientX, clientY }) => {
|
|
209
146
|
currentInputMethodRef.current = 'mouse';
|
|
210
147
|
const initialPosition = mouseDownPositionRef.current;
|
|
211
|
-
if (!initialPosition)
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
Math.abs(initialPosition.clientY - clientY) < 12
|
|
215
|
-
) {
|
|
148
|
+
if (!initialPosition)
|
|
149
|
+
return;
|
|
150
|
+
if (Math.abs(initialPosition.clientX - clientX) < 12 &&
|
|
151
|
+
Math.abs(initialPosition.clientY - clientY) < 12) {
|
|
216
152
|
return;
|
|
217
153
|
}
|
|
218
154
|
setIsOpening(false);
|
|
219
155
|
}, []);
|
|
220
156
|
const handleMouseOver = useCallback((event) => {
|
|
221
|
-
if (!hasItemsRef.current)
|
|
157
|
+
if (!hasItemsRef.current)
|
|
158
|
+
return;
|
|
222
159
|
// If user isn’t currently using the mouse to navigate the dropdown, do nothing
|
|
223
|
-
if (currentInputMethodRef.current !== 'mouse')
|
|
160
|
+
if (currentInputMethodRef.current !== 'mouse')
|
|
161
|
+
return;
|
|
224
162
|
// Ensure we have the dropdown root HTMLElement
|
|
225
163
|
const dropdownElement = dropdownElementRef.current;
|
|
226
|
-
if (!dropdownElement)
|
|
164
|
+
if (!dropdownElement)
|
|
165
|
+
return;
|
|
227
166
|
const itemElements = getItemElements(dropdownElement);
|
|
228
|
-
if (!itemElements)
|
|
167
|
+
if (!itemElements)
|
|
168
|
+
return;
|
|
229
169
|
const eventTarget = event.target;
|
|
230
170
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
|
231
171
|
const item = eventTarget.closest(ITEM_SELECTOR);
|
|
@@ -238,9 +178,11 @@ export default function Dropdown({
|
|
|
238
178
|
}
|
|
239
179
|
}, []);
|
|
240
180
|
const handleMouseOut = useCallback((event) => {
|
|
241
|
-
if (!hasItemsRef.current)
|
|
181
|
+
if (!hasItemsRef.current)
|
|
182
|
+
return;
|
|
242
183
|
const activeItem = getActiveItemElement(dropdownElementRef.current);
|
|
243
|
-
if (!activeItem)
|
|
184
|
+
if (!activeItem)
|
|
185
|
+
return;
|
|
244
186
|
const eventRelatedTarget = event.relatedTarget;
|
|
245
187
|
if (activeItem !== event.target || activeItem.contains(eventRelatedTarget)) {
|
|
246
188
|
return;
|
|
@@ -248,371 +190,296 @@ export default function Dropdown({
|
|
|
248
190
|
// If user moused out of activeItem (not into a descendant), it’s no longer active
|
|
249
191
|
delete activeItem.dataset.uktActive;
|
|
250
192
|
}, []);
|
|
251
|
-
const handleMouseDown = useCallback(
|
|
252
|
-
(
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
},
|
|
266
|
-
|
|
267
|
-
)
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
193
|
+
const handleMouseDown = useCallback((event) => {
|
|
194
|
+
if (onMouseDown)
|
|
195
|
+
onMouseDown(event);
|
|
196
|
+
if (isOpenRef.current)
|
|
197
|
+
return;
|
|
198
|
+
setIsOpen(true);
|
|
199
|
+
setIsOpening(true);
|
|
200
|
+
mouseDownPositionRef.current = {
|
|
201
|
+
clientX: event.clientX,
|
|
202
|
+
clientY: event.clientY,
|
|
203
|
+
};
|
|
204
|
+
isOpeningTimerRef.current = setTimeout(() => {
|
|
205
|
+
setIsOpening(false);
|
|
206
|
+
isOpeningTimerRef.current = null;
|
|
207
|
+
}, 1000);
|
|
208
|
+
}, [onMouseDown]);
|
|
209
|
+
const handleMouseUp = useCallback((event) => {
|
|
210
|
+
if (onMouseUp)
|
|
211
|
+
onMouseUp(event);
|
|
212
|
+
// If dropdown is still opening or isn’t open or is closing, do nothing
|
|
213
|
+
if (isOpeningRef.current || !isOpenRef.current || closingTimerRef.current) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const eventTarget = event.target;
|
|
217
|
+
// If click was outside dropdown body, don’t trigger submit
|
|
218
|
+
if (!eventTarget.closest(BODY_SELECTOR)) {
|
|
219
|
+
// Don’t close dropdown if isOpening or search input is focused
|
|
220
|
+
if (!isOpeningRef.current &&
|
|
221
|
+
inputElementRef.current !== eventTarget.ownerDocument.activeElement) {
|
|
222
|
+
closeDropdown();
|
|
274
223
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
// If dropdown has no items and click was within dropdown body, do nothing
|
|
227
|
+
if (!hasItemsRef.current)
|
|
228
|
+
return;
|
|
229
|
+
handleSubmitItem(event);
|
|
230
|
+
}, [closeDropdown, handleSubmitItem, onMouseUp]);
|
|
231
|
+
const handleKeyDown = useCallback((event) => {
|
|
232
|
+
const { altKey, ctrlKey, key, metaKey } = event;
|
|
233
|
+
const eventTarget = event.target;
|
|
234
|
+
const dropdownElement = dropdownElementRef.current;
|
|
235
|
+
if (!dropdownElement)
|
|
236
|
+
return;
|
|
237
|
+
const onEventHandled = () => {
|
|
238
|
+
event.stopPropagation();
|
|
239
|
+
event.preventDefault();
|
|
240
|
+
currentInputMethodRef.current = 'keyboard';
|
|
241
|
+
};
|
|
242
|
+
const isEventTargetingDropdown = dropdownElement.contains(eventTarget);
|
|
243
|
+
if (!isOpenRef.current) {
|
|
244
|
+
// If dropdown is closed, don’t handle key events if event target isn’t within dropdown
|
|
245
|
+
if (!isEventTargetingDropdown)
|
|
285
246
|
return;
|
|
247
|
+
// Open the dropdown on spacebar, enter, or if isSearchable and user hits the ↑/↓ arrows
|
|
248
|
+
if (key === ' ' ||
|
|
249
|
+
key === 'Enter' ||
|
|
250
|
+
(hasItemsRef.current && (key === 'ArrowUp' || key === 'ArrowDown'))) {
|
|
251
|
+
onEventHandled();
|
|
252
|
+
setIsOpen(true);
|
|
286
253
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
const dropdownElement = dropdownElementRef.current;
|
|
298
|
-
if (!dropdownElement) return;
|
|
299
|
-
const onEventHandled = () => {
|
|
300
|
-
event.stopPropagation();
|
|
301
|
-
event.preventDefault();
|
|
302
|
-
currentInputMethodRef.current = 'keyboard';
|
|
303
|
-
};
|
|
304
|
-
const isEventTargetingDropdown = dropdownElement.contains(eventTarget);
|
|
305
|
-
if (!isOpenRef.current) {
|
|
306
|
-
// If dropdown is closed, don’t handle key events if event target isn’t within dropdown
|
|
307
|
-
if (!isEventTargetingDropdown) return;
|
|
308
|
-
// Open the dropdown on spacebar, enter, or if isSearchable and user hits the ↑/↓ arrows
|
|
309
|
-
if (
|
|
310
|
-
key === ' ' ||
|
|
311
|
-
key === 'Enter' ||
|
|
312
|
-
(hasItemsRef.current && (key === 'ArrowUp' || key === 'ArrowDown'))
|
|
313
|
-
) {
|
|
314
|
-
onEventHandled();
|
|
315
|
-
setIsOpen(true);
|
|
316
|
-
}
|
|
317
|
-
return;
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const isTargetUsingKeyEvents = isEventTargetUsingKeyEvent(event);
|
|
257
|
+
// If dropdown isOpen + hasItems & eventTargetNotUsingKeyEvents, handle characters
|
|
258
|
+
if (hasItemsRef.current && !isTargetUsingKeyEvents) {
|
|
259
|
+
let isEditingCharacters = !ctrlKey && !metaKey && /^[A-Za-z0-9]$/.test(key);
|
|
260
|
+
// User could also be editing characters if there are already characters entered
|
|
261
|
+
// and they are hitting delete or spacebar
|
|
262
|
+
if (!isEditingCharacters && enteredCharactersRef.current) {
|
|
263
|
+
isEditingCharacters = key === ' ' || key === 'Backspace';
|
|
318
264
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
!ctrlKey && !metaKey && /^[A-Za-z0-9]$/.test(key);
|
|
324
|
-
// User could also be editing characters if there are already characters entered
|
|
325
|
-
// and they are hitting delete or spacebar
|
|
326
|
-
if (!isEditingCharacters && enteredCharactersRef.current) {
|
|
327
|
-
isEditingCharacters = key === ' ' || key === 'Backspace';
|
|
265
|
+
if (isEditingCharacters) {
|
|
266
|
+
onEventHandled();
|
|
267
|
+
if (key === 'Backspace') {
|
|
268
|
+
enteredCharactersRef.current = enteredCharactersRef.current.slice(0, -1);
|
|
328
269
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
if (key === 'Backspace') {
|
|
332
|
-
enteredCharactersRef.current = enteredCharactersRef.current.slice(
|
|
333
|
-
0,
|
|
334
|
-
-1,
|
|
335
|
-
);
|
|
336
|
-
} else {
|
|
337
|
-
enteredCharactersRef.current += key;
|
|
338
|
-
}
|
|
339
|
-
setActiveItem({
|
|
340
|
-
dropdownElement,
|
|
341
|
-
// If props.allowCreate, only override the input’s value with an
|
|
342
|
-
// exact text match so user can enter a value not in items
|
|
343
|
-
isExactMatch: allowCreateRef.current,
|
|
344
|
-
text: enteredCharactersRef.current,
|
|
345
|
-
});
|
|
346
|
-
if (clearEnteredCharactersTimerRef.current) {
|
|
347
|
-
clearTimeout(clearEnteredCharactersTimerRef.current);
|
|
348
|
-
}
|
|
349
|
-
clearEnteredCharactersTimerRef.current = setTimeout(() => {
|
|
350
|
-
enteredCharactersRef.current = '';
|
|
351
|
-
clearEnteredCharactersTimerRef.current = null;
|
|
352
|
-
}, 1500);
|
|
353
|
-
return;
|
|
270
|
+
else {
|
|
271
|
+
enteredCharactersRef.current += key;
|
|
354
272
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
273
|
+
setActiveItem({
|
|
274
|
+
dropdownElement,
|
|
275
|
+
// If props.allowCreate, only override the input’s value with an
|
|
276
|
+
// exact text match so user can enter a value not in items
|
|
277
|
+
isExactMatch: allowCreateRef.current,
|
|
278
|
+
text: enteredCharactersRef.current,
|
|
279
|
+
});
|
|
280
|
+
if (clearEnteredCharactersTimerRef.current) {
|
|
281
|
+
clearTimeout(clearEnteredCharactersTimerRef.current);
|
|
282
|
+
}
|
|
283
|
+
clearEnteredCharactersTimerRef.current = setTimeout(() => {
|
|
284
|
+
enteredCharactersRef.current = '';
|
|
285
|
+
clearEnteredCharactersTimerRef.current = null;
|
|
286
|
+
}, 1500);
|
|
360
287
|
return;
|
|
361
288
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
)
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
289
|
+
}
|
|
290
|
+
// If dropdown isOpen, handle submitting the value
|
|
291
|
+
if (key === 'Enter' || (key === ' ' && !inputElementRef.current)) {
|
|
292
|
+
onEventHandled();
|
|
293
|
+
handleSubmitItem(event);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
// If dropdown isOpen, handle closing it on escape or spacebar if !hasItems
|
|
297
|
+
if (key === 'Escape' ||
|
|
298
|
+
(isEventTargetingDropdown && key === ' ' && !hasItemsRef.current)) {
|
|
299
|
+
// Close dropdown if hasItems or event target not using key events
|
|
300
|
+
if (hasItemsRef.current || !isTargetUsingKeyEvents) {
|
|
301
|
+
closeDropdown();
|
|
302
|
+
}
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
// Handle ↑/↓ arrows
|
|
306
|
+
if (hasItemsRef.current) {
|
|
307
|
+
if (key === 'ArrowUp') {
|
|
308
|
+
onEventHandled();
|
|
309
|
+
if (altKey || metaKey) {
|
|
310
|
+
setActiveItem({ dropdownElement, index: 0 });
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
setActiveItem({ dropdownElement, indexAddend: -1 });
|
|
370
314
|
}
|
|
371
315
|
return;
|
|
372
316
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
if (
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
setActiveItem({ dropdownElement, index: 0 });
|
|
379
|
-
} else {
|
|
380
|
-
setActiveItem({ dropdownElement, indexAddend: -1 });
|
|
381
|
-
}
|
|
382
|
-
return;
|
|
317
|
+
if (key === 'ArrowDown') {
|
|
318
|
+
onEventHandled();
|
|
319
|
+
if (altKey || metaKey) {
|
|
320
|
+
// Using a negative index counts back from the end
|
|
321
|
+
setActiveItem({ dropdownElement, index: -1 });
|
|
383
322
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
if (altKey || metaKey) {
|
|
387
|
-
// Using a negative index counts back from the end
|
|
388
|
-
setActiveItem({ dropdownElement, index: -1 });
|
|
389
|
-
} else {
|
|
390
|
-
setActiveItem({ dropdownElement, indexAddend: 1 });
|
|
391
|
-
}
|
|
392
|
-
return;
|
|
323
|
+
else {
|
|
324
|
+
setActiveItem({ dropdownElement, indexAddend: 1 });
|
|
393
325
|
}
|
|
326
|
+
return;
|
|
394
327
|
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
);
|
|
328
|
+
}
|
|
329
|
+
}, [closeDropdown, handleSubmitItem]);
|
|
398
330
|
useKeyboardEvents({ ignoreUsedKeyboardEvents: false, onKeyDown: handleKeyDown });
|
|
399
331
|
const cleanupEventListenersRef = useRef(noop);
|
|
400
|
-
const handleRef = useCallback(
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
332
|
+
const handleRef = useCallback((ref) => {
|
|
333
|
+
dropdownElementRef.current = ref;
|
|
334
|
+
if (!ref) {
|
|
335
|
+
// If component was unmounted, cleanup handlers
|
|
336
|
+
cleanupEventListenersRef.current();
|
|
337
|
+
cleanupEventListenersRef.current = noop;
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const { ownerDocument } = ref;
|
|
341
|
+
let inputElement = inputElementRef.current;
|
|
342
|
+
// Check if trigger from props is a textual input or textarea element
|
|
343
|
+
if (isTriggerFromProps && !inputElement && ref.firstElementChild) {
|
|
344
|
+
if (ref.firstElementChild.matches(TEXT_INPUT_SELECTOR)) {
|
|
345
|
+
inputElement = ref.firstElementChild;
|
|
408
346
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
if (isTriggerFromProps && !inputElement && ref.firstElementChild) {
|
|
413
|
-
if (ref.firstElementChild.matches(TEXT_INPUT_SELECTOR)) {
|
|
414
|
-
inputElement = ref.firstElementChild;
|
|
415
|
-
} else {
|
|
416
|
-
inputElement =
|
|
417
|
-
ref.firstElementChild.querySelector(TEXT_INPUT_SELECTOR);
|
|
418
|
-
}
|
|
419
|
-
inputElementRef.current = inputElement;
|
|
347
|
+
else {
|
|
348
|
+
inputElement =
|
|
349
|
+
ref.firstElementChild.querySelector(TEXT_INPUT_SELECTOR);
|
|
420
350
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
) {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
const eventTarget = target;
|
|
444
|
-
// Only handle mouseup events from outside the dropdown here
|
|
445
|
-
if (
|
|
446
|
-
!((_a = dropdownElementRef.current) === null || _a === void 0
|
|
447
|
-
? void 0
|
|
448
|
-
: _a.contains(eventTarget))
|
|
449
|
-
) {
|
|
450
|
-
closeDropdown();
|
|
451
|
-
}
|
|
452
|
-
};
|
|
453
|
-
// Close dropdown if any element is focused outside of this dropdown
|
|
454
|
-
const handleGlobalFocusIn = ({ target }) => {
|
|
455
|
-
if (!isOpenRef.current) return;
|
|
456
|
-
const eventTarget = target;
|
|
457
|
-
// If focused element is a descendant or a parent of the dropdown, do nothing
|
|
458
|
-
if (
|
|
459
|
-
!dropdownElementRef.current ||
|
|
460
|
-
dropdownElementRef.current.contains(eventTarget) ||
|
|
461
|
-
eventTarget.contains(dropdownElementRef.current)
|
|
462
|
-
) {
|
|
463
|
-
return;
|
|
351
|
+
inputElementRef.current = inputElement;
|
|
352
|
+
}
|
|
353
|
+
const handleGlobalMouseDown = ({ target }) => {
|
|
354
|
+
const eventTarget = target;
|
|
355
|
+
if (dropdownElementRef.current &&
|
|
356
|
+
!dropdownElementRef.current.contains(eventTarget)) {
|
|
357
|
+
// Close dropdown on an outside click
|
|
358
|
+
closeDropdown();
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
const handleGlobalMouseUp = ({ target }) => {
|
|
362
|
+
var _a;
|
|
363
|
+
if (!isOpenRef.current || closingTimerRef.current)
|
|
364
|
+
return;
|
|
365
|
+
// If still isOpening (gets set false 1s after open triggers), set it to false onMouseUp
|
|
366
|
+
if (isOpeningRef.current) {
|
|
367
|
+
setIsOpening(false);
|
|
368
|
+
if (isOpeningTimerRef.current) {
|
|
369
|
+
clearTimeout(isOpeningTimerRef.current);
|
|
370
|
+
isOpeningTimerRef.current = null;
|
|
464
371
|
}
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const eventTarget = target;
|
|
375
|
+
// Only handle mouseup events from outside the dropdown here
|
|
376
|
+
if (!((_a = dropdownElementRef.current) === null || _a === void 0 ? void 0 : _a.contains(eventTarget))) {
|
|
465
377
|
closeDropdown();
|
|
466
|
-
};
|
|
467
|
-
document.addEventListener('focusin', handleGlobalFocusIn);
|
|
468
|
-
document.addEventListener('mousedown', handleGlobalMouseDown);
|
|
469
|
-
document.addEventListener('mouseup', handleGlobalMouseUp);
|
|
470
|
-
if (ownerDocument !== document) {
|
|
471
|
-
ownerDocument.addEventListener('focusin', handleGlobalFocusIn);
|
|
472
|
-
ownerDocument.addEventListener('mousedown', handleGlobalMouseDown);
|
|
473
|
-
ownerDocument.addEventListener('mouseup', handleGlobalMouseUp);
|
|
474
378
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
379
|
+
};
|
|
380
|
+
// Close dropdown if any element is focused outside of this dropdown
|
|
381
|
+
const handleGlobalFocusIn = ({ target }) => {
|
|
382
|
+
if (!isOpenRef.current)
|
|
383
|
+
return;
|
|
384
|
+
const eventTarget = target;
|
|
385
|
+
// If focused element is a descendant or a parent of the dropdown, do nothing
|
|
386
|
+
if (!dropdownElementRef.current ||
|
|
387
|
+
dropdownElementRef.current.contains(eventTarget) ||
|
|
388
|
+
eventTarget.contains(dropdownElementRef.current)) {
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
closeDropdown();
|
|
392
|
+
};
|
|
393
|
+
document.addEventListener('focusin', handleGlobalFocusIn);
|
|
394
|
+
document.addEventListener('mousedown', handleGlobalMouseDown);
|
|
395
|
+
document.addEventListener('mouseup', handleGlobalMouseUp);
|
|
396
|
+
if (ownerDocument !== document) {
|
|
397
|
+
ownerDocument.addEventListener('focusin', handleGlobalFocusIn);
|
|
398
|
+
ownerDocument.addEventListener('mousedown', handleGlobalMouseDown);
|
|
399
|
+
ownerDocument.addEventListener('mouseup', handleGlobalMouseUp);
|
|
400
|
+
}
|
|
401
|
+
// If dropdown should be open on mount, focus it
|
|
402
|
+
if (isOpenOnMount) {
|
|
403
|
+
ref.focus();
|
|
404
|
+
}
|
|
405
|
+
const handleInput = (event) => {
|
|
406
|
+
const dropdownElement = dropdownElementRef.current;
|
|
407
|
+
if (!dropdownElement)
|
|
408
|
+
return;
|
|
409
|
+
if (!isOpenRef.current)
|
|
410
|
+
setIsOpen(true);
|
|
411
|
+
const input = event.target;
|
|
412
|
+
const isDeleting = enteredCharactersRef.current.length > input.value.length;
|
|
413
|
+
enteredCharactersRef.current = input.value;
|
|
414
|
+
// When deleting text, if there’s already an active item and
|
|
415
|
+
// input isn’t empty, preserve the active item, else update it
|
|
416
|
+
if (isDeleting &&
|
|
417
|
+
input.value.length &&
|
|
418
|
+
getActiveItemElement(dropdownElement)) {
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
setActiveItem({
|
|
422
|
+
dropdownElement,
|
|
423
|
+
// If props.allowCreate, only override the input’s value with an
|
|
424
|
+
// exact text match so user can enter a value not in items
|
|
425
|
+
isExactMatch: allowCreateRef.current,
|
|
426
|
+
text: enteredCharactersRef.current,
|
|
427
|
+
});
|
|
428
|
+
};
|
|
429
|
+
if (inputElement) {
|
|
430
|
+
inputElement.addEventListener('input', handleInput);
|
|
431
|
+
}
|
|
432
|
+
cleanupEventListenersRef.current = () => {
|
|
433
|
+
document.removeEventListener('focusin', handleGlobalFocusIn);
|
|
434
|
+
document.removeEventListener('mousedown', handleGlobalMouseDown);
|
|
435
|
+
document.removeEventListener('mouseup', handleGlobalMouseUp);
|
|
436
|
+
if (ownerDocument !== document) {
|
|
437
|
+
ownerDocument.removeEventListener('focusin', handleGlobalFocusIn);
|
|
438
|
+
ownerDocument.removeEventListener('mousedown', handleGlobalMouseDown);
|
|
439
|
+
ownerDocument.removeEventListener('mouseup', handleGlobalMouseUp);
|
|
478
440
|
}
|
|
479
|
-
const handleInput = (event) => {
|
|
480
|
-
const dropdownElement = dropdownElementRef.current;
|
|
481
|
-
if (!dropdownElement) return;
|
|
482
|
-
if (!isOpenRef.current) setIsOpen(true);
|
|
483
|
-
const input = event.target;
|
|
484
|
-
const isDeleting =
|
|
485
|
-
enteredCharactersRef.current.length > input.value.length;
|
|
486
|
-
enteredCharactersRef.current = input.value;
|
|
487
|
-
// When deleting text, if there’s already an active item and
|
|
488
|
-
// input isn’t empty, preserve the active item, else update it
|
|
489
|
-
if (
|
|
490
|
-
isDeleting &&
|
|
491
|
-
input.value.length &&
|
|
492
|
-
getActiveItemElement(dropdownElement)
|
|
493
|
-
) {
|
|
494
|
-
return;
|
|
495
|
-
}
|
|
496
|
-
setActiveItem({
|
|
497
|
-
dropdownElement,
|
|
498
|
-
// If props.allowCreate, only override the input’s value with an
|
|
499
|
-
// exact text match so user can enter a value not in items
|
|
500
|
-
isExactMatch: allowCreateRef.current,
|
|
501
|
-
text: enteredCharactersRef.current,
|
|
502
|
-
});
|
|
503
|
-
};
|
|
504
441
|
if (inputElement) {
|
|
505
|
-
inputElement.
|
|
442
|
+
inputElement.removeEventListener('input', handleInput);
|
|
506
443
|
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
document.removeEventListener('mousedown', handleGlobalMouseDown);
|
|
510
|
-
document.removeEventListener('mouseup', handleGlobalMouseUp);
|
|
511
|
-
if (ownerDocument !== document) {
|
|
512
|
-
ownerDocument.removeEventListener('focusin', handleGlobalFocusIn);
|
|
513
|
-
ownerDocument.removeEventListener('mousedown', handleGlobalMouseDown);
|
|
514
|
-
ownerDocument.removeEventListener('mouseup', handleGlobalMouseUp);
|
|
515
|
-
}
|
|
516
|
-
if (inputElement) {
|
|
517
|
-
inputElement.removeEventListener('input', handleInput);
|
|
518
|
-
}
|
|
519
|
-
};
|
|
520
|
-
},
|
|
521
|
-
[closeDropdown, isOpenOnMount, isTriggerFromProps],
|
|
522
|
-
);
|
|
444
|
+
};
|
|
445
|
+
}, [closeDropdown, isOpenOnMount, isTriggerFromProps]);
|
|
523
446
|
if (!isTriggerFromProps) {
|
|
524
447
|
if (isSearchable) {
|
|
525
|
-
trigger = React.createElement(InputText, {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
initialValue: value !== null && value !== void 0 ? value : '',
|
|
530
|
-
name: name,
|
|
531
|
-
onFocus: setDropdownOpenRef.current,
|
|
532
|
-
placeholder: placeholder,
|
|
533
|
-
ref: inputElementRef,
|
|
534
|
-
selectTextOnFocus: true,
|
|
535
|
-
tabIndex: tabIndex,
|
|
536
|
-
type: 'text',
|
|
537
|
-
});
|
|
538
|
-
} else {
|
|
539
|
-
trigger = React.createElement(
|
|
540
|
-
'button',
|
|
541
|
-
{ className: TRIGGER_CLASS_NAME, tabIndex: 0 },
|
|
542
|
-
trigger,
|
|
543
|
-
);
|
|
448
|
+
trigger = (React.createElement(InputText, { autoComplete: "off", className: TRIGGER_CLASS_NAME, disabled: disabled, initialValue: value !== null && value !== void 0 ? value : '', name: name, onFocus: setDropdownOpenRef.current, placeholder: placeholder, ref: inputElementRef, selectTextOnFocus: true, tabIndex: tabIndex, type: "text" }));
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
trigger = (React.createElement("button", { className: TRIGGER_CLASS_NAME, tabIndex: 0 }, trigger));
|
|
544
452
|
}
|
|
545
453
|
}
|
|
546
454
|
if (label) {
|
|
547
|
-
trigger = React.createElement(
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
React.createElement('div', { className: LABEL_TEXT_CLASS_NAME }, label),
|
|
551
|
-
trigger,
|
|
552
|
-
);
|
|
455
|
+
trigger = (React.createElement("label", { className: LABEL_CLASS_NAME },
|
|
456
|
+
React.createElement("div", { className: LABEL_TEXT_CLASS_NAME }, label),
|
|
457
|
+
trigger));
|
|
553
458
|
}
|
|
554
|
-
const style = useMemo(
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
),
|
|
565
|
-
outOfBounds.maxWidth
|
|
566
|
-
? {
|
|
567
|
-
[BODY_MAX_WIDTH_VAR]: `calc(${outOfBounds.maxWidth}px - var(--uktdd-body-buffer))`,
|
|
568
|
-
}
|
|
569
|
-
: null,
|
|
570
|
-
),
|
|
571
|
-
[outOfBounds.maxHeight, outOfBounds.maxWidth, styleFromProps],
|
|
572
|
-
);
|
|
573
|
-
return React.createElement(
|
|
574
|
-
Fragment,
|
|
575
|
-
null,
|
|
459
|
+
const style = useMemo(() => (Object.assign(Object.assign(Object.assign({}, styleFromProps), (outOfBounds.maxHeight
|
|
460
|
+
? {
|
|
461
|
+
[BODY_MAX_HEIGHT_VAR]: `calc(${outOfBounds.maxHeight}px - var(--uktdd-body-buffer))`,
|
|
462
|
+
}
|
|
463
|
+
: null)), (outOfBounds.maxWidth
|
|
464
|
+
? {
|
|
465
|
+
[BODY_MAX_WIDTH_VAR]: `calc(${outOfBounds.maxWidth}px - var(--uktdd-body-buffer))`,
|
|
466
|
+
}
|
|
467
|
+
: null))), [outOfBounds.maxHeight, outOfBounds.maxWidth, styleFromProps]);
|
|
468
|
+
return (React.createElement(Fragment, null,
|
|
576
469
|
React.createElement(Style, null, STYLES),
|
|
577
|
-
React.createElement(
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
'is-open': isOpen,
|
|
583
|
-
'is-searchable': isSearchable,
|
|
584
|
-
}),
|
|
585
|
-
onClick: onClick,
|
|
586
|
-
onMouseDown: handleMouseDown,
|
|
587
|
-
onMouseMove: handleMouseMove,
|
|
588
|
-
onMouseOut: handleMouseOut,
|
|
589
|
-
onMouseOver: handleMouseOver,
|
|
590
|
-
onMouseUp: handleMouseUp,
|
|
591
|
-
ref: handleRef,
|
|
592
|
-
style: style,
|
|
593
|
-
},
|
|
470
|
+
React.createElement("div", { className: clsx(ROOT_CLASS_NAME, className, {
|
|
471
|
+
disabled,
|
|
472
|
+
'is-open': isOpen,
|
|
473
|
+
'is-searchable': isSearchable,
|
|
474
|
+
}), onClick: onClick, onMouseDown: handleMouseDown, onMouseMove: handleMouseMove, onMouseOut: handleMouseOut, onMouseOver: handleMouseOver, onMouseUp: handleMouseUp, ref: handleRef, style: style },
|
|
594
475
|
trigger,
|
|
595
|
-
isOpen
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
outOfBounds.bottom && !outOfBounds.top,
|
|
604
|
-
'out-of-bounds-left':
|
|
605
|
-
outOfBounds.left && !outOfBounds.right,
|
|
606
|
-
'out-of-bounds-right':
|
|
607
|
-
outOfBounds.right && !outOfBounds.left,
|
|
608
|
-
'out-of-bounds-top': outOfBounds.top && !outOfBounds.bottom,
|
|
609
|
-
}),
|
|
610
|
-
ref: setDropdownBodyElement,
|
|
611
|
-
},
|
|
612
|
-
childrenCount > 1 ? children[1] : children,
|
|
613
|
-
)
|
|
614
|
-
: null,
|
|
615
|
-
),
|
|
616
|
-
);
|
|
476
|
+
isOpen ? (React.createElement("div", { className: clsx(BODY_CLASS_NAME, {
|
|
477
|
+
'calculating-position': !outOfBounds.hasLayout,
|
|
478
|
+
'has-items': hasItems,
|
|
479
|
+
'out-of-bounds-bottom': outOfBounds.bottom && !outOfBounds.top,
|
|
480
|
+
'out-of-bounds-left': outOfBounds.left && !outOfBounds.right,
|
|
481
|
+
'out-of-bounds-right': outOfBounds.right && !outOfBounds.left,
|
|
482
|
+
'out-of-bounds-top': outOfBounds.top && !outOfBounds.bottom,
|
|
483
|
+
}), ref: setDropdownBodyElement }, childrenCount > 1 ? children[1] : children)) : null)));
|
|
617
484
|
}
|
|
618
|
-
//# sourceMappingURL=Dropdown.js.map
|
|
485
|
+
//# sourceMappingURL=Dropdown.js.map
|