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