@acusti/dropdown 0.44.0 → 0.44.1
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 +25 -3
- package/dist/Dropdown.js +466 -333
- package/dist/Dropdown.js.flow +59 -59
- package/dist/Dropdown.test.js +80 -47
- package/dist/helpers.d.ts +47 -32
- package/dist/helpers.js +35 -36
- package/dist/helpers.js.flow +37 -37
- package/dist/styles.d.ts +14 -13
- package/dist/styles.js +1 -1
- package/dist/styles.js.flow +12 -12
- package/package.json +5 -5
- package/src/Dropdown.tsx +8 -8
- package/src/helpers.ts +1 -1
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,80 +138,98 @@ 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
|
-
|
|
141
|
+
const handleSubmitItem = useCallback(
|
|
142
|
+
(event) => {
|
|
143
|
+
var _a, _b, _c;
|
|
144
|
+
const eventTarget = event.target;
|
|
145
|
+
if (isOpenRef.current && !keepOpenOnSubmitRef.current) {
|
|
146
|
+
const keepOpen = eventTarget.closest('[data-ukt-keep-open]');
|
|
147
|
+
// Don’t close dropdown if event occurs w/in data-ukt-keep-open element
|
|
148
|
+
if (
|
|
149
|
+
!(keepOpen === null || keepOpen === void 0
|
|
150
|
+
? void 0
|
|
151
|
+
: keepOpen.dataset.uktKeepOpen) ||
|
|
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
|
+
}
|
|
110
158
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
let itemLabel = (_b = element === null || element === void 0 ? void 0 : element.innerText) !== null && _b !== void 0 ? _b : '';
|
|
124
|
-
if (inputElementRef.current) {
|
|
125
|
-
if (!element) {
|
|
126
|
-
itemLabel = inputElementRef.current.value;
|
|
159
|
+
if (!hasItemsRef.current) return;
|
|
160
|
+
const element = getActiveItemElement(dropdownElementRef.current);
|
|
161
|
+
if (!element && !allowCreateRef.current) {
|
|
162
|
+
// If not allowEmpty, don’t allow submitting an empty item
|
|
163
|
+
if (!allowEmptyRef.current) return;
|
|
164
|
+
// If we have an input element as trigger & the user didn’t clear the text, do nothing
|
|
165
|
+
if (
|
|
166
|
+
(_a = inputElementRef.current) === null || _a === void 0
|
|
167
|
+
? void 0
|
|
168
|
+
: _a.value
|
|
169
|
+
)
|
|
170
|
+
return;
|
|
127
171
|
}
|
|
128
|
-
|
|
129
|
-
|
|
172
|
+
let itemLabel =
|
|
173
|
+
(_b =
|
|
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
|
+
}
|
|
130
191
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
192
|
+
const nextValue =
|
|
193
|
+
(_c =
|
|
194
|
+
element === null || element === void 0
|
|
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
|
+
});
|
|
134
208
|
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (valueRef.current && valueRef.current === nextValue)
|
|
139
|
-
return;
|
|
140
|
-
if (onSubmitItemRef.current) {
|
|
141
|
-
onSubmitItemRef.current({
|
|
142
|
-
element,
|
|
143
|
-
event,
|
|
144
|
-
label: itemLabel,
|
|
145
|
-
value: nextValue,
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
}, [closeDropdown]);
|
|
209
|
+
},
|
|
210
|
+
[closeDropdown],
|
|
211
|
+
);
|
|
149
212
|
const handleMouseMove = useCallback(({ clientX, clientY }) => {
|
|
150
213
|
currentInputMethodRef.current = 'mouse';
|
|
151
214
|
const initialPosition = mouseDownPositionRef.current;
|
|
152
|
-
if (!initialPosition)
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
Math.abs(initialPosition.clientY - clientY) < 12
|
|
215
|
+
if (!initialPosition) return;
|
|
216
|
+
if (
|
|
217
|
+
Math.abs(initialPosition.clientX - clientX) < 12 &&
|
|
218
|
+
Math.abs(initialPosition.clientY - clientY) < 12
|
|
219
|
+
) {
|
|
156
220
|
return;
|
|
157
221
|
}
|
|
158
222
|
setIsOpening(false);
|
|
159
223
|
}, []);
|
|
160
224
|
const handleMouseOver = useCallback((event) => {
|
|
161
|
-
if (!hasItemsRef.current)
|
|
162
|
-
return;
|
|
225
|
+
if (!hasItemsRef.current) return;
|
|
163
226
|
// If user isn’t currently using the mouse to navigate the dropdown, do nothing
|
|
164
|
-
if (currentInputMethodRef.current !== 'mouse')
|
|
165
|
-
return;
|
|
227
|
+
if (currentInputMethodRef.current !== 'mouse') return;
|
|
166
228
|
// Ensure we have the dropdown root HTMLElement
|
|
167
229
|
const dropdownElement = dropdownElementRef.current;
|
|
168
|
-
if (!dropdownElement)
|
|
169
|
-
return;
|
|
230
|
+
if (!dropdownElement) return;
|
|
170
231
|
const itemElements = getItemElements(dropdownElement);
|
|
171
|
-
if (!itemElements)
|
|
172
|
-
return;
|
|
232
|
+
if (!itemElements) return;
|
|
173
233
|
const eventTarget = event.target;
|
|
174
234
|
const item = eventTarget.closest(ITEM_SELECTOR);
|
|
175
235
|
const element = item !== null && item !== void 0 ? item : eventTarget;
|
|
@@ -181,11 +241,9 @@ export default function Dropdown({ allowCreate, allowEmpty = true, children, cla
|
|
|
181
241
|
}
|
|
182
242
|
}, []);
|
|
183
243
|
const handleMouseOut = useCallback((event) => {
|
|
184
|
-
if (!hasItemsRef.current)
|
|
185
|
-
return;
|
|
244
|
+
if (!hasItemsRef.current) return;
|
|
186
245
|
const activeItem = getActiveItemElement(dropdownElementRef.current);
|
|
187
|
-
if (!activeItem)
|
|
188
|
-
return;
|
|
246
|
+
if (!activeItem) return;
|
|
189
247
|
const eventRelatedTarget = event.relatedTarget;
|
|
190
248
|
if (activeItem !== event.target || activeItem.contains(eventRelatedTarget)) {
|
|
191
249
|
return;
|
|
@@ -193,296 +251,371 @@ export default function Dropdown({ allowCreate, allowEmpty = true, children, cla
|
|
|
193
251
|
// If user moused out of activeItem (not into a descendant), it’s no longer active
|
|
194
252
|
delete activeItem.dataset.uktActive;
|
|
195
253
|
}, []);
|
|
196
|
-
const handleMouseDown = useCallback(
|
|
197
|
-
|
|
198
|
-
onMouseDown(event);
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
},
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}
|
|
219
|
-
const eventTarget = event.target;
|
|
220
|
-
// If click was outside dropdown body, don’t trigger submit
|
|
221
|
-
if (!eventTarget.closest(BODY_SELECTOR)) {
|
|
222
|
-
// Don’t close dropdown if isOpening or search input is focused
|
|
223
|
-
if (!isOpeningRef.current &&
|
|
224
|
-
inputElementRef.current !== eventTarget.ownerDocument.activeElement) {
|
|
225
|
-
closeDropdown();
|
|
226
|
-
}
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
// If dropdown has no items and click was within dropdown body, do nothing
|
|
230
|
-
if (!hasItemsRef.current)
|
|
231
|
-
return;
|
|
232
|
-
handleSubmitItem(event);
|
|
233
|
-
}, [closeDropdown, handleSubmitItem, onMouseUp]);
|
|
234
|
-
const handleKeyDown = useCallback((event) => {
|
|
235
|
-
const { altKey, ctrlKey, key, metaKey } = event;
|
|
236
|
-
const eventTarget = event.target;
|
|
237
|
-
const dropdownElement = dropdownElementRef.current;
|
|
238
|
-
if (!dropdownElement)
|
|
239
|
-
return;
|
|
240
|
-
const onEventHandled = () => {
|
|
241
|
-
event.stopPropagation();
|
|
242
|
-
event.preventDefault();
|
|
243
|
-
currentInputMethodRef.current = 'keyboard';
|
|
244
|
-
};
|
|
245
|
-
const isEventTargetingDropdown = dropdownElement.contains(eventTarget);
|
|
246
|
-
if (!isOpenRef.current) {
|
|
247
|
-
// If dropdown is closed, don’t handle key events if event target isn’t within dropdown
|
|
248
|
-
if (!isEventTargetingDropdown)
|
|
254
|
+
const handleMouseDown = useCallback(
|
|
255
|
+
(event) => {
|
|
256
|
+
if (onMouseDown) onMouseDown(event);
|
|
257
|
+
if (isOpenRef.current) return;
|
|
258
|
+
setIsOpen(true);
|
|
259
|
+
setIsOpening(true);
|
|
260
|
+
mouseDownPositionRef.current = {
|
|
261
|
+
clientX: event.clientX,
|
|
262
|
+
clientY: event.clientY,
|
|
263
|
+
};
|
|
264
|
+
isOpeningTimerRef.current = setTimeout(() => {
|
|
265
|
+
setIsOpening(false);
|
|
266
|
+
isOpeningTimerRef.current = null;
|
|
267
|
+
}, 1000);
|
|
268
|
+
},
|
|
269
|
+
[onMouseDown],
|
|
270
|
+
);
|
|
271
|
+
const handleMouseUp = useCallback(
|
|
272
|
+
(event) => {
|
|
273
|
+
if (onMouseUp) onMouseUp(event);
|
|
274
|
+
// If dropdown is still opening or isn’t open or is closing, do nothing
|
|
275
|
+
if (isOpeningRef.current || !isOpenRef.current || closingTimerRef.current) {
|
|
249
276
|
return;
|
|
250
|
-
// Open the dropdown on spacebar, enter, or if isSearchable and user hits the ↑/↓ arrows
|
|
251
|
-
if (key === ' ' ||
|
|
252
|
-
key === 'Enter' ||
|
|
253
|
-
(hasItemsRef.current && (key === 'ArrowUp' || key === 'ArrowDown'))) {
|
|
254
|
-
onEventHandled();
|
|
255
|
-
setIsOpen(true);
|
|
256
|
-
}
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
const isTargetUsingKeyEvents = isEventTargetUsingKeyEvent(event);
|
|
260
|
-
// If dropdown isOpen + hasItems & eventTargetNotUsingKeyEvents, handle characters
|
|
261
|
-
if (hasItemsRef.current && !isTargetUsingKeyEvents) {
|
|
262
|
-
let isEditingCharacters = !ctrlKey && !metaKey && /^[A-Za-z0-9]$/.test(key);
|
|
263
|
-
// User could also be editing characters if there are already characters entered
|
|
264
|
-
// and they are hitting delete or spacebar
|
|
265
|
-
if (!isEditingCharacters && enteredCharactersRef.current) {
|
|
266
|
-
isEditingCharacters = key === ' ' || key === 'Backspace';
|
|
267
277
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
278
|
+
const eventTarget = event.target;
|
|
279
|
+
// If click was outside dropdown body, don’t trigger submit
|
|
280
|
+
if (!eventTarget.closest(BODY_SELECTOR)) {
|
|
281
|
+
// Don’t close dropdown if isOpening or search input is focused
|
|
282
|
+
if (
|
|
283
|
+
!isOpeningRef.current &&
|
|
284
|
+
inputElementRef.current !== eventTarget.ownerDocument.activeElement
|
|
285
|
+
) {
|
|
286
|
+
closeDropdown();
|
|
275
287
|
}
|
|
276
|
-
setActiveItem({
|
|
277
|
-
dropdownElement,
|
|
278
|
-
// If props.allowCreate, only override the input’s value with an
|
|
279
|
-
// exact text match so user can enter a value not in items
|
|
280
|
-
isExactMatch: allowCreateRef.current,
|
|
281
|
-
text: enteredCharactersRef.current,
|
|
282
|
-
});
|
|
283
|
-
if (clearEnteredCharactersTimerRef.current) {
|
|
284
|
-
clearTimeout(clearEnteredCharactersTimerRef.current);
|
|
285
|
-
}
|
|
286
|
-
clearEnteredCharactersTimerRef.current = setTimeout(() => {
|
|
287
|
-
enteredCharactersRef.current = '';
|
|
288
|
-
clearEnteredCharactersTimerRef.current = null;
|
|
289
|
-
}, 1500);
|
|
290
288
|
return;
|
|
291
289
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
if (key === 'Enter' || (key === ' ' && !inputElementRef.current)) {
|
|
295
|
-
onEventHandled();
|
|
290
|
+
// If dropdown has no items and click was within dropdown body, do nothing
|
|
291
|
+
if (!hasItemsRef.current) return;
|
|
296
292
|
handleSubmitItem(event);
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
293
|
+
},
|
|
294
|
+
[closeDropdown, handleSubmitItem, onMouseUp],
|
|
295
|
+
);
|
|
296
|
+
const handleKeyDown = useCallback(
|
|
297
|
+
(event) => {
|
|
298
|
+
const { altKey, ctrlKey, key, metaKey } = event;
|
|
299
|
+
const eventTarget = event.target;
|
|
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);
|
|
317
319
|
}
|
|
318
320
|
return;
|
|
319
321
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
322
|
+
const isTargetUsingKeyEvents = isEventTargetUsingKeyEvent(event);
|
|
323
|
+
// If dropdown isOpen + hasItems & eventTargetNotUsingKeyEvents, handle characters
|
|
324
|
+
if (hasItemsRef.current && !isTargetUsingKeyEvents) {
|
|
325
|
+
let isEditingCharacters =
|
|
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';
|
|
325
331
|
}
|
|
326
|
-
|
|
327
|
-
|
|
332
|
+
if (isEditingCharacters) {
|
|
333
|
+
onEventHandled();
|
|
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;
|
|
328
357
|
}
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
}, [closeDropdown, handleSubmitItem]);
|
|
333
|
-
useKeyboardEvents({ ignoreUsedKeyboardEvents: false, onKeyDown: handleKeyDown });
|
|
334
|
-
const cleanupEventListenersRef = useRef(noop);
|
|
335
|
-
const handleRef = useCallback((ref) => {
|
|
336
|
-
dropdownElementRef.current = ref;
|
|
337
|
-
if (!ref) {
|
|
338
|
-
// If component was unmounted, cleanup handlers
|
|
339
|
-
cleanupEventListenersRef.current();
|
|
340
|
-
cleanupEventListenersRef.current = noop;
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
const { ownerDocument } = ref;
|
|
344
|
-
let inputElement = inputElementRef.current;
|
|
345
|
-
// Check if trigger from props is a textual input or textarea element
|
|
346
|
-
if (isTriggerFromProps && !inputElement && ref.firstElementChild) {
|
|
347
|
-
if (ref.firstElementChild.matches(TEXT_INPUT_SELECTOR)) {
|
|
348
|
-
inputElement = ref.firstElementChild;
|
|
349
|
-
}
|
|
350
|
-
else {
|
|
351
|
-
inputElement =
|
|
352
|
-
ref.firstElementChild.querySelector(TEXT_INPUT_SELECTOR);
|
|
353
358
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
if (dropdownElementRef.current &&
|
|
359
|
-
!dropdownElementRef.current.contains(eventTarget)) {
|
|
360
|
-
// Close dropdown on an outside click
|
|
361
|
-
closeDropdown();
|
|
362
|
-
}
|
|
363
|
-
};
|
|
364
|
-
const handleGlobalMouseUp = ({ target }) => {
|
|
365
|
-
var _a;
|
|
366
|
-
if (!isOpenRef.current || closingTimerRef.current)
|
|
359
|
+
// If dropdown isOpen, handle submitting the value
|
|
360
|
+
if (key === 'Enter' || (key === ' ' && !inputElementRef.current)) {
|
|
361
|
+
onEventHandled();
|
|
362
|
+
handleSubmitItem(event);
|
|
367
363
|
return;
|
|
368
|
-
|
|
369
|
-
if
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
364
|
+
}
|
|
365
|
+
// If dropdown isOpen, handle closing it on escape or spacebar if !hasItems
|
|
366
|
+
if (
|
|
367
|
+
key === 'Escape' ||
|
|
368
|
+
(isEventTargetingDropdown && key === ' ' && !hasItemsRef.current)
|
|
369
|
+
) {
|
|
370
|
+
// Close dropdown if hasItems or event target not using key events
|
|
371
|
+
if (hasItemsRef.current || !isTargetUsingKeyEvents) {
|
|
372
|
+
closeDropdown();
|
|
374
373
|
}
|
|
375
374
|
return;
|
|
376
375
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
376
|
+
// Handle ↑/↓ arrows
|
|
377
|
+
if (hasItemsRef.current) {
|
|
378
|
+
if (key === 'ArrowUp') {
|
|
379
|
+
onEventHandled();
|
|
380
|
+
if (altKey || metaKey) {
|
|
381
|
+
setActiveItem({ dropdownElement, index: 0 });
|
|
382
|
+
} else {
|
|
383
|
+
setActiveItem({ dropdownElement, indexAddend: -1 });
|
|
384
|
+
}
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (key === 'ArrowDown') {
|
|
388
|
+
onEventHandled();
|
|
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;
|
|
396
|
+
}
|
|
381
397
|
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
398
|
+
},
|
|
399
|
+
[closeDropdown, handleSubmitItem],
|
|
400
|
+
);
|
|
401
|
+
useKeyboardEvents({ ignoreUsedKeyboardEvents: false, onKeyDown: handleKeyDown });
|
|
402
|
+
const cleanupEventListenersRef = useRef(noop);
|
|
403
|
+
const handleRef = useCallback(
|
|
404
|
+
(ref) => {
|
|
405
|
+
dropdownElementRef.current = ref;
|
|
406
|
+
if (!ref) {
|
|
407
|
+
// If component was unmounted, cleanup handlers
|
|
408
|
+
cleanupEventListenersRef.current();
|
|
409
|
+
cleanupEventListenersRef.current = noop;
|
|
392
410
|
return;
|
|
393
411
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
if (isOpenOnMount) {
|
|
406
|
-
ref.focus();
|
|
407
|
-
}
|
|
408
|
-
const handleInput = (event) => {
|
|
409
|
-
const dropdownElement = dropdownElementRef.current;
|
|
410
|
-
if (!dropdownElement)
|
|
411
|
-
return;
|
|
412
|
-
if (!isOpenRef.current)
|
|
413
|
-
setIsOpen(true);
|
|
414
|
-
const input = event.target;
|
|
415
|
-
const isDeleting = enteredCharactersRef.current.length > input.value.length;
|
|
416
|
-
enteredCharactersRef.current = input.value;
|
|
417
|
-
// When deleting text, if there’s already an active item and
|
|
418
|
-
// input isn’t empty, preserve the active item, else update it
|
|
419
|
-
if (isDeleting &&
|
|
420
|
-
input.value.length &&
|
|
421
|
-
getActiveItemElement(dropdownElement)) {
|
|
422
|
-
return;
|
|
412
|
+
const { ownerDocument } = ref;
|
|
413
|
+
let inputElement = inputElementRef.current;
|
|
414
|
+
// Check if trigger from props is a textual input or textarea element
|
|
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;
|
|
423
423
|
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
424
|
+
const handleGlobalMouseDown = ({ target }) => {
|
|
425
|
+
const eventTarget = target;
|
|
426
|
+
if (
|
|
427
|
+
dropdownElementRef.current &&
|
|
428
|
+
!dropdownElementRef.current.contains(eventTarget)
|
|
429
|
+
) {
|
|
430
|
+
// Close dropdown on an outside click
|
|
431
|
+
closeDropdown();
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
const handleGlobalMouseUp = ({ target }) => {
|
|
435
|
+
var _a;
|
|
436
|
+
if (!isOpenRef.current || closingTimerRef.current) return;
|
|
437
|
+
// If still isOpening (gets set false 1s after open triggers), set it to false onMouseUp
|
|
438
|
+
if (isOpeningRef.current) {
|
|
439
|
+
setIsOpening(false);
|
|
440
|
+
if (isOpeningTimerRef.current) {
|
|
441
|
+
clearTimeout(isOpeningTimerRef.current);
|
|
442
|
+
isOpeningTimerRef.current = null;
|
|
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;
|
|
467
|
+
}
|
|
468
|
+
closeDropdown();
|
|
469
|
+
};
|
|
470
|
+
document.addEventListener('focusin', handleGlobalFocusIn);
|
|
471
|
+
document.addEventListener('mousedown', handleGlobalMouseDown);
|
|
472
|
+
document.addEventListener('mouseup', handleGlobalMouseUp);
|
|
439
473
|
if (ownerDocument !== document) {
|
|
440
|
-
ownerDocument.
|
|
441
|
-
ownerDocument.
|
|
442
|
-
ownerDocument.
|
|
474
|
+
ownerDocument.addEventListener('focusin', handleGlobalFocusIn);
|
|
475
|
+
ownerDocument.addEventListener('mousedown', handleGlobalMouseDown);
|
|
476
|
+
ownerDocument.addEventListener('mouseup', handleGlobalMouseUp);
|
|
443
477
|
}
|
|
478
|
+
// If dropdown should be open on mount, focus it
|
|
479
|
+
if (isOpenOnMount) {
|
|
480
|
+
ref.focus();
|
|
481
|
+
}
|
|
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
|
+
};
|
|
444
507
|
if (inputElement) {
|
|
445
|
-
inputElement.
|
|
508
|
+
inputElement.addEventListener('input', handleInput);
|
|
446
509
|
}
|
|
447
|
-
|
|
448
|
-
|
|
510
|
+
cleanupEventListenersRef.current = () => {
|
|
511
|
+
document.removeEventListener('focusin', handleGlobalFocusIn);
|
|
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
|
+
);
|
|
449
526
|
if (!isTriggerFromProps) {
|
|
450
527
|
if (isSearchable) {
|
|
451
|
-
trigger =
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
528
|
+
trigger = React.createElement(InputText, {
|
|
529
|
+
autoComplete: 'off',
|
|
530
|
+
className: TRIGGER_CLASS_NAME,
|
|
531
|
+
disabled: disabled,
|
|
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
|
+
);
|
|
455
547
|
}
|
|
456
548
|
}
|
|
457
549
|
if (label) {
|
|
458
|
-
trigger =
|
|
459
|
-
|
|
460
|
-
|
|
550
|
+
trigger = React.createElement(
|
|
551
|
+
'label',
|
|
552
|
+
{ className: LABEL_CLASS_NAME },
|
|
553
|
+
React.createElement('div', { className: LABEL_TEXT_CLASS_NAME }, label),
|
|
554
|
+
trigger,
|
|
555
|
+
);
|
|
461
556
|
}
|
|
462
|
-
const style = useMemo(
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
557
|
+
const style = useMemo(
|
|
558
|
+
() =>
|
|
559
|
+
Object.assign(
|
|
560
|
+
Object.assign(
|
|
561
|
+
Object.assign({}, styleFromProps),
|
|
562
|
+
outOfBounds.maxHeight != null && outOfBounds.maxHeight > 0
|
|
563
|
+
? {
|
|
564
|
+
[BODY_MAX_HEIGHT_VAR]: `calc(${outOfBounds.maxHeight}px - var(--uktdd-body-buffer))`,
|
|
565
|
+
}
|
|
566
|
+
: null,
|
|
567
|
+
),
|
|
568
|
+
outOfBounds.maxWidth != null && outOfBounds.maxWidth > 0
|
|
569
|
+
? {
|
|
570
|
+
[BODY_MAX_WIDTH_VAR]: `calc(${outOfBounds.maxWidth}px - var(--uktdd-body-buffer))`,
|
|
571
|
+
}
|
|
572
|
+
: null,
|
|
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
|
+
},
|
|
478
597
|
trigger,
|
|
479
|
-
isOpen
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
598
|
+
isOpen
|
|
599
|
+
? React.createElement(
|
|
600
|
+
'div',
|
|
601
|
+
{
|
|
602
|
+
className: clsx(BODY_CLASS_NAME, {
|
|
603
|
+
'calculating-position': !outOfBounds.hasLayout,
|
|
604
|
+
'has-items': hasItems,
|
|
605
|
+
'out-of-bounds-bottom':
|
|
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
|
+
);
|
|
487
620
|
}
|
|
488
|
-
//# sourceMappingURL=Dropdown.js.map
|
|
621
|
+
//# sourceMappingURL=Dropdown.js.map
|