@acusti/dropdown 0.38.2 → 0.38.4
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 +23 -1
- package/dist/Dropdown.js +461 -328
- package/dist/Dropdown.test.js +80 -47
- package/dist/helpers.d.ts +47 -32
- package/dist/helpers.js +35 -36
- package/dist/styles.d.ts +14 -13
- package/dist/styles.js +1 -1
- package/package.json +2 -2
package/dist/Dropdown.js
CHANGED
|
@@ -2,16 +2,57 @@
|
|
|
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, {
|
|
5
|
+
import useKeyboardEvents, {
|
|
6
|
+
isEventTargetUsingKeyEvent,
|
|
7
|
+
} from '@acusti/use-keyboard-events';
|
|
6
8
|
import clsx from 'clsx';
|
|
7
9
|
import * as React from 'react';
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
+
import {
|
|
11
|
+
getActiveItemElement,
|
|
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';
|
|
10
27
|
const { Children, Fragment, useCallback, useEffect, useMemo, useRef, useState } = React;
|
|
11
|
-
const noop = () => {
|
|
12
|
-
const CHILDREN_ERROR =
|
|
13
|
-
|
|
14
|
-
|
|
28
|
+
const noop = () => {}; // eslint-disable-line @typescript-eslint/no-empty-function
|
|
29
|
+
const CHILDREN_ERROR =
|
|
30
|
+
'@acusti/dropdown requires either 1 child (the dropdown body) or 2 children: the dropdown trigger and the dropdown body.';
|
|
31
|
+
const TEXT_INPUT_SELECTOR =
|
|
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
|
+
}) {
|
|
15
56
|
const childrenCount = Children.count(children);
|
|
16
57
|
if (childrenCount !== 1 && childrenCount !== 2) {
|
|
17
58
|
if (childrenCount === 0) {
|
|
@@ -24,7 +65,9 @@ export default function Dropdown({ allowCreate, allowEmpty = true, children, cla
|
|
|
24
65
|
trigger = children[0];
|
|
25
66
|
}
|
|
26
67
|
const isTriggerFromProps = React.isValidElement(trigger);
|
|
27
|
-
const [isOpen, setIsOpen] = useState(
|
|
68
|
+
const [isOpen, setIsOpen] = useState(
|
|
69
|
+
isOpenOnMount !== null && isOpenOnMount !== void 0 ? isOpenOnMount : false,
|
|
70
|
+
);
|
|
28
71
|
const [isOpening, setIsOpening] = useState(!isOpenOnMount);
|
|
29
72
|
const [dropdownBodyElement, setDropdownBodyElement] = useState(null);
|
|
30
73
|
const dropdownElementRef = useRef(null);
|
|
@@ -82,8 +125,7 @@ export default function Dropdown({ allowCreate, allowEmpty = true, children, cla
|
|
|
82
125
|
}
|
|
83
126
|
if (isOpen && onOpenRef.current) {
|
|
84
127
|
onOpenRef.current();
|
|
85
|
-
}
|
|
86
|
-
else if (!isOpen && onCloseRef.current) {
|
|
128
|
+
} else if (!isOpen && onCloseRef.current) {
|
|
87
129
|
onCloseRef.current();
|
|
88
130
|
}
|
|
89
131
|
}, [isOpen]);
|
|
@@ -96,76 +138,94 @@ export default function Dropdown({ allowCreate, allowEmpty = true, children, cla
|
|
|
96
138
|
closingTimerRef.current = null;
|
|
97
139
|
}
|
|
98
140
|
}, []);
|
|
99
|
-
const handleSubmitItem = useCallback(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
141
|
+
const handleSubmitItem = useCallback(
|
|
142
|
+
(event) => {
|
|
143
|
+
var _a, _b, _c;
|
|
144
|
+
const eventTarget = event.target;
|
|
145
|
+
if (isOpenRef.current && !keepOpenOnSubmitRef.current) {
|
|
146
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
|
147
|
+
const keepOpen = eventTarget.closest('[data-ukt-keep-open]');
|
|
148
|
+
// Don’t close dropdown if event occurs w/in data-ukt-keep-open element
|
|
149
|
+
if (
|
|
150
|
+
!(keepOpen === null || keepOpen === void 0
|
|
151
|
+
? void 0
|
|
152
|
+
: keepOpen.dataset.uktKeepOpen) ||
|
|
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
|
+
}
|
|
111
159
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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;
|
|
160
|
+
if (!hasItemsRef.current) return;
|
|
161
|
+
const element = getActiveItemElement(dropdownElementRef.current);
|
|
162
|
+
if (!element && !allowCreateRef.current) {
|
|
163
|
+
// If not allowEmpty, don’t allow submitting an empty item
|
|
164
|
+
if (!allowEmptyRef.current) return;
|
|
165
|
+
// If we have an input element as trigger & the user didn’t clear the text, do nothing
|
|
166
|
+
if (
|
|
167
|
+
(_a = inputElementRef.current) === null || _a === void 0
|
|
168
|
+
? void 0
|
|
169
|
+
: _a.value
|
|
170
|
+
)
|
|
171
|
+
return;
|
|
128
172
|
}
|
|
129
|
-
|
|
130
|
-
|
|
173
|
+
let label =
|
|
174
|
+
(_b =
|
|
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
|
+
}
|
|
131
192
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
193
|
+
const nextValue =
|
|
194
|
+
(_c =
|
|
195
|
+
element === null || element === void 0
|
|
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 });
|
|
135
204
|
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (valueRef.current && valueRef.current === nextValue)
|
|
140
|
-
return;
|
|
141
|
-
if (onSubmitItemRef.current) {
|
|
142
|
-
onSubmitItemRef.current({ element, event, label, value: nextValue });
|
|
143
|
-
}
|
|
144
|
-
}, [closeDropdown]);
|
|
205
|
+
},
|
|
206
|
+
[closeDropdown],
|
|
207
|
+
);
|
|
145
208
|
const handleMouseMove = useCallback(({ clientX, clientY }) => {
|
|
146
209
|
currentInputMethodRef.current = 'mouse';
|
|
147
210
|
const initialPosition = mouseDownPositionRef.current;
|
|
148
|
-
if (!initialPosition)
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
Math.abs(initialPosition.clientY - clientY) < 12
|
|
211
|
+
if (!initialPosition) return;
|
|
212
|
+
if (
|
|
213
|
+
Math.abs(initialPosition.clientX - clientX) < 12 &&
|
|
214
|
+
Math.abs(initialPosition.clientY - clientY) < 12
|
|
215
|
+
) {
|
|
152
216
|
return;
|
|
153
217
|
}
|
|
154
218
|
setIsOpening(false);
|
|
155
219
|
}, []);
|
|
156
220
|
const handleMouseOver = useCallback((event) => {
|
|
157
|
-
if (!hasItemsRef.current)
|
|
158
|
-
return;
|
|
221
|
+
if (!hasItemsRef.current) return;
|
|
159
222
|
// If user isn’t currently using the mouse to navigate the dropdown, do nothing
|
|
160
|
-
if (currentInputMethodRef.current !== 'mouse')
|
|
161
|
-
return;
|
|
223
|
+
if (currentInputMethodRef.current !== 'mouse') return;
|
|
162
224
|
// Ensure we have the dropdown root HTMLElement
|
|
163
225
|
const dropdownElement = dropdownElementRef.current;
|
|
164
|
-
if (!dropdownElement)
|
|
165
|
-
return;
|
|
226
|
+
if (!dropdownElement) return;
|
|
166
227
|
const itemElements = getItemElements(dropdownElement);
|
|
167
|
-
if (!itemElements)
|
|
168
|
-
return;
|
|
228
|
+
if (!itemElements) return;
|
|
169
229
|
const eventTarget = event.target;
|
|
170
230
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
|
171
231
|
const item = eventTarget.closest(ITEM_SELECTOR);
|
|
@@ -178,11 +238,9 @@ export default function Dropdown({ allowCreate, allowEmpty = true, children, cla
|
|
|
178
238
|
}
|
|
179
239
|
}, []);
|
|
180
240
|
const handleMouseOut = useCallback((event) => {
|
|
181
|
-
if (!hasItemsRef.current)
|
|
182
|
-
return;
|
|
241
|
+
if (!hasItemsRef.current) return;
|
|
183
242
|
const activeItem = getActiveItemElement(dropdownElementRef.current);
|
|
184
|
-
if (!activeItem)
|
|
185
|
-
return;
|
|
243
|
+
if (!activeItem) return;
|
|
186
244
|
const eventRelatedTarget = event.relatedTarget;
|
|
187
245
|
if (activeItem !== event.target || activeItem.contains(eventRelatedTarget)) {
|
|
188
246
|
return;
|
|
@@ -190,296 +248,371 @@ export default function Dropdown({ allowCreate, allowEmpty = true, children, cla
|
|
|
190
248
|
// If user moused out of activeItem (not into a descendant), it’s no longer active
|
|
191
249
|
delete activeItem.dataset.uktActive;
|
|
192
250
|
}, []);
|
|
193
|
-
const handleMouseDown = useCallback(
|
|
194
|
-
|
|
195
|
-
onMouseDown(event);
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
},
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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();
|
|
223
|
-
}
|
|
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)
|
|
251
|
+
const handleMouseDown = useCallback(
|
|
252
|
+
(event) => {
|
|
253
|
+
if (onMouseDown) onMouseDown(event);
|
|
254
|
+
if (isOpenRef.current) return;
|
|
255
|
+
setIsOpen(true);
|
|
256
|
+
setIsOpening(true);
|
|
257
|
+
mouseDownPositionRef.current = {
|
|
258
|
+
clientX: event.clientX,
|
|
259
|
+
clientY: event.clientY,
|
|
260
|
+
};
|
|
261
|
+
isOpeningTimerRef.current = setTimeout(() => {
|
|
262
|
+
setIsOpening(false);
|
|
263
|
+
isOpeningTimerRef.current = null;
|
|
264
|
+
}, 1000);
|
|
265
|
+
},
|
|
266
|
+
[onMouseDown],
|
|
267
|
+
);
|
|
268
|
+
const handleMouseUp = useCallback(
|
|
269
|
+
(event) => {
|
|
270
|
+
if (onMouseUp) onMouseUp(event);
|
|
271
|
+
// If dropdown is still opening or isn’t open or is closing, do nothing
|
|
272
|
+
if (isOpeningRef.current || !isOpenRef.current || closingTimerRef.current) {
|
|
246
273
|
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);
|
|
253
274
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
isEditingCharacters = key === ' ' || key === 'Backspace';
|
|
264
|
-
}
|
|
265
|
-
if (isEditingCharacters) {
|
|
266
|
-
onEventHandled();
|
|
267
|
-
if (key === 'Backspace') {
|
|
268
|
-
enteredCharactersRef.current = enteredCharactersRef.current.slice(0, -1);
|
|
275
|
+
const eventTarget = event.target;
|
|
276
|
+
// If click was outside dropdown body, don’t trigger submit
|
|
277
|
+
if (!eventTarget.closest(BODY_SELECTOR)) {
|
|
278
|
+
// Don’t close dropdown if isOpening or search input is focused
|
|
279
|
+
if (
|
|
280
|
+
!isOpeningRef.current &&
|
|
281
|
+
inputElementRef.current !== eventTarget.ownerDocument.activeElement
|
|
282
|
+
) {
|
|
283
|
+
closeDropdown();
|
|
269
284
|
}
|
|
270
|
-
else {
|
|
271
|
-
enteredCharactersRef.current += key;
|
|
272
|
-
}
|
|
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);
|
|
287
285
|
return;
|
|
288
286
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
if (key === 'Enter' || (key === ' ' && !inputElementRef.current)) {
|
|
292
|
-
onEventHandled();
|
|
287
|
+
// If dropdown has no items and click was within dropdown body, do nothing
|
|
288
|
+
if (!hasItemsRef.current) return;
|
|
293
289
|
handleSubmitItem(event);
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
290
|
+
},
|
|
291
|
+
[closeDropdown, handleSubmitItem, onMouseUp],
|
|
292
|
+
);
|
|
293
|
+
const handleKeyDown = useCallback(
|
|
294
|
+
(event) => {
|
|
295
|
+
const { altKey, ctrlKey, key, metaKey } = event;
|
|
296
|
+
const eventTarget = event.target;
|
|
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);
|
|
314
316
|
}
|
|
315
317
|
return;
|
|
316
318
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
319
|
+
const isTargetUsingKeyEvents = isEventTargetUsingKeyEvent(event);
|
|
320
|
+
// If dropdown isOpen + hasItems & eventTargetNotUsingKeyEvents, handle characters
|
|
321
|
+
if (hasItemsRef.current && !isTargetUsingKeyEvents) {
|
|
322
|
+
let isEditingCharacters =
|
|
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';
|
|
322
328
|
}
|
|
323
|
-
|
|
324
|
-
|
|
329
|
+
if (isEditingCharacters) {
|
|
330
|
+
onEventHandled();
|
|
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;
|
|
325
354
|
}
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
}, [closeDropdown, handleSubmitItem]);
|
|
330
|
-
useKeyboardEvents({ ignoreUsedKeyboardEvents: false, onKeyDown: handleKeyDown });
|
|
331
|
-
const cleanupEventListenersRef = useRef(noop);
|
|
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;
|
|
346
355
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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)
|
|
356
|
+
// If dropdown isOpen, handle submitting the value
|
|
357
|
+
if (key === 'Enter' || (key === ' ' && !inputElementRef.current)) {
|
|
358
|
+
onEventHandled();
|
|
359
|
+
handleSubmitItem(event);
|
|
364
360
|
return;
|
|
365
|
-
|
|
366
|
-
if
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
361
|
+
}
|
|
362
|
+
// If dropdown isOpen, handle closing it on escape or spacebar if !hasItems
|
|
363
|
+
if (
|
|
364
|
+
key === 'Escape' ||
|
|
365
|
+
(isEventTargetingDropdown && key === ' ' && !hasItemsRef.current)
|
|
366
|
+
) {
|
|
367
|
+
// Close dropdown if hasItems or event target not using key events
|
|
368
|
+
if (hasItemsRef.current || !isTargetUsingKeyEvents) {
|
|
369
|
+
closeDropdown();
|
|
371
370
|
}
|
|
372
371
|
return;
|
|
373
372
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
373
|
+
// Handle ↑/↓ arrows
|
|
374
|
+
if (hasItemsRef.current) {
|
|
375
|
+
if (key === 'ArrowUp') {
|
|
376
|
+
onEventHandled();
|
|
377
|
+
if (altKey || metaKey) {
|
|
378
|
+
setActiveItem({ dropdownElement, index: 0 });
|
|
379
|
+
} else {
|
|
380
|
+
setActiveItem({ dropdownElement, indexAddend: -1 });
|
|
381
|
+
}
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (key === 'ArrowDown') {
|
|
385
|
+
onEventHandled();
|
|
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;
|
|
393
|
+
}
|
|
378
394
|
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
395
|
+
},
|
|
396
|
+
[closeDropdown, handleSubmitItem],
|
|
397
|
+
);
|
|
398
|
+
useKeyboardEvents({ ignoreUsedKeyboardEvents: false, onKeyDown: handleKeyDown });
|
|
399
|
+
const cleanupEventListenersRef = useRef(noop);
|
|
400
|
+
const handleRef = useCallback(
|
|
401
|
+
(ref) => {
|
|
402
|
+
dropdownElementRef.current = ref;
|
|
403
|
+
if (!ref) {
|
|
404
|
+
// If component was unmounted, cleanup handlers
|
|
405
|
+
cleanupEventListenersRef.current();
|
|
406
|
+
cleanupEventListenersRef.current = noop;
|
|
389
407
|
return;
|
|
390
408
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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;
|
|
409
|
+
const { ownerDocument } = ref;
|
|
410
|
+
let inputElement = inputElementRef.current;
|
|
411
|
+
// Check if trigger from props is a textual input or textarea element
|
|
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;
|
|
420
420
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
421
|
+
const handleGlobalMouseDown = ({ target }) => {
|
|
422
|
+
const eventTarget = target;
|
|
423
|
+
if (
|
|
424
|
+
dropdownElementRef.current &&
|
|
425
|
+
!dropdownElementRef.current.contains(eventTarget)
|
|
426
|
+
) {
|
|
427
|
+
// Close dropdown on an outside click
|
|
428
|
+
closeDropdown();
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
const handleGlobalMouseUp = ({ target }) => {
|
|
432
|
+
var _a;
|
|
433
|
+
if (!isOpenRef.current || closingTimerRef.current) return;
|
|
434
|
+
// If still isOpening (gets set false 1s after open triggers), set it to false onMouseUp
|
|
435
|
+
if (isOpeningRef.current) {
|
|
436
|
+
setIsOpening(false);
|
|
437
|
+
if (isOpeningTimerRef.current) {
|
|
438
|
+
clearTimeout(isOpeningTimerRef.current);
|
|
439
|
+
isOpeningTimerRef.current = null;
|
|
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;
|
|
464
|
+
}
|
|
465
|
+
closeDropdown();
|
|
466
|
+
};
|
|
467
|
+
document.addEventListener('focusin', handleGlobalFocusIn);
|
|
468
|
+
document.addEventListener('mousedown', handleGlobalMouseDown);
|
|
469
|
+
document.addEventListener('mouseup', handleGlobalMouseUp);
|
|
436
470
|
if (ownerDocument !== document) {
|
|
437
|
-
ownerDocument.
|
|
438
|
-
ownerDocument.
|
|
439
|
-
ownerDocument.
|
|
471
|
+
ownerDocument.addEventListener('focusin', handleGlobalFocusIn);
|
|
472
|
+
ownerDocument.addEventListener('mousedown', handleGlobalMouseDown);
|
|
473
|
+
ownerDocument.addEventListener('mouseup', handleGlobalMouseUp);
|
|
440
474
|
}
|
|
475
|
+
// If dropdown should be open on mount, focus it
|
|
476
|
+
if (isOpenOnMount) {
|
|
477
|
+
ref.focus();
|
|
478
|
+
}
|
|
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
|
+
};
|
|
441
504
|
if (inputElement) {
|
|
442
|
-
inputElement.
|
|
505
|
+
inputElement.addEventListener('input', handleInput);
|
|
443
506
|
}
|
|
444
|
-
|
|
445
|
-
|
|
507
|
+
cleanupEventListenersRef.current = () => {
|
|
508
|
+
document.removeEventListener('focusin', handleGlobalFocusIn);
|
|
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
|
+
);
|
|
446
523
|
if (!isTriggerFromProps) {
|
|
447
524
|
if (isSearchable) {
|
|
448
|
-
trigger =
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
525
|
+
trigger = React.createElement(InputText, {
|
|
526
|
+
autoComplete: 'off',
|
|
527
|
+
className: TRIGGER_CLASS_NAME,
|
|
528
|
+
disabled: disabled,
|
|
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
|
+
);
|
|
452
544
|
}
|
|
453
545
|
}
|
|
454
546
|
if (label) {
|
|
455
|
-
trigger =
|
|
456
|
-
|
|
457
|
-
|
|
547
|
+
trigger = React.createElement(
|
|
548
|
+
'label',
|
|
549
|
+
{ className: LABEL_CLASS_NAME },
|
|
550
|
+
React.createElement('div', { className: LABEL_TEXT_CLASS_NAME }, label),
|
|
551
|
+
trigger,
|
|
552
|
+
);
|
|
458
553
|
}
|
|
459
|
-
const style = useMemo(
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
554
|
+
const style = useMemo(
|
|
555
|
+
() =>
|
|
556
|
+
Object.assign(
|
|
557
|
+
Object.assign(
|
|
558
|
+
Object.assign({}, styleFromProps),
|
|
559
|
+
outOfBounds.maxHeight
|
|
560
|
+
? {
|
|
561
|
+
[BODY_MAX_HEIGHT_VAR]: `calc(${outOfBounds.maxHeight}px - var(--uktdd-body-buffer))`,
|
|
562
|
+
}
|
|
563
|
+
: null,
|
|
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,
|
|
469
576
|
React.createElement(Style, null, STYLES),
|
|
470
|
-
React.createElement(
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
577
|
+
React.createElement(
|
|
578
|
+
'div',
|
|
579
|
+
{
|
|
580
|
+
className: clsx(ROOT_CLASS_NAME, className, {
|
|
581
|
+
disabled,
|
|
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
|
+
},
|
|
475
594
|
trigger,
|
|
476
|
-
isOpen
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
595
|
+
isOpen
|
|
596
|
+
? React.createElement(
|
|
597
|
+
'div',
|
|
598
|
+
{
|
|
599
|
+
className: clsx(BODY_CLASS_NAME, {
|
|
600
|
+
'calculating-position': !outOfBounds.hasLayout,
|
|
601
|
+
'has-items': hasItems,
|
|
602
|
+
'out-of-bounds-bottom':
|
|
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
|
+
);
|
|
484
617
|
}
|
|
485
|
-
//# sourceMappingURL=Dropdown.js.map
|
|
618
|
+
//# sourceMappingURL=Dropdown.js.map
|