@acusti/dropdown 0.43.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 -330
- package/dist/Dropdown.js.flow +59 -59
- package/dist/Dropdown.js.map +1 -1
- package/dist/Dropdown.test.js +81 -48
- package/dist/Dropdown.test.js.map +1 -1
- package/dist/helpers.d.ts +47 -32
- package/dist/helpers.js +37 -39
- package/dist/helpers.js.flow +37 -37
- package/dist/helpers.js.map +1 -1
- package/dist/styles.d.ts +14 -13
- package/dist/styles.js +1 -1
- package/dist/styles.js.flow +12 -12
- package/package.json +15 -15
- package/src/Dropdown.test.tsx +1 -1
- package/src/Dropdown.tsx +20 -17
- package/src/helpers.ts +4 -5
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,78 +138,99 @@ 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
|
+
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
|
+
}
|
|
111
158
|
}
|
|
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;
|
|
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;
|
|
128
171
|
}
|
|
129
|
-
|
|
130
|
-
|
|
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
|
+
}
|
|
131
191
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
+
});
|
|
135
208
|
}
|
|
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]);
|
|
209
|
+
},
|
|
210
|
+
[closeDropdown],
|
|
211
|
+
);
|
|
145
212
|
const handleMouseMove = useCallback(({ clientX, clientY }) => {
|
|
146
213
|
currentInputMethodRef.current = 'mouse';
|
|
147
214
|
const initialPosition = mouseDownPositionRef.current;
|
|
148
|
-
if (!initialPosition)
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
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
|
+
) {
|
|
152
220
|
return;
|
|
153
221
|
}
|
|
154
222
|
setIsOpening(false);
|
|
155
223
|
}, []);
|
|
156
224
|
const handleMouseOver = useCallback((event) => {
|
|
157
|
-
if (!hasItemsRef.current)
|
|
158
|
-
return;
|
|
225
|
+
if (!hasItemsRef.current) return;
|
|
159
226
|
// If user isn’t currently using the mouse to navigate the dropdown, do nothing
|
|
160
|
-
if (currentInputMethodRef.current !== 'mouse')
|
|
161
|
-
return;
|
|
227
|
+
if (currentInputMethodRef.current !== 'mouse') return;
|
|
162
228
|
// Ensure we have the dropdown root HTMLElement
|
|
163
229
|
const dropdownElement = dropdownElementRef.current;
|
|
164
|
-
if (!dropdownElement)
|
|
165
|
-
return;
|
|
230
|
+
if (!dropdownElement) return;
|
|
166
231
|
const itemElements = getItemElements(dropdownElement);
|
|
167
|
-
if (!itemElements)
|
|
168
|
-
return;
|
|
232
|
+
if (!itemElements) return;
|
|
169
233
|
const eventTarget = event.target;
|
|
170
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
|
171
234
|
const item = eventTarget.closest(ITEM_SELECTOR);
|
|
172
235
|
const element = item !== null && item !== void 0 ? item : eventTarget;
|
|
173
236
|
for (const itemElement of itemElements) {
|
|
@@ -178,11 +241,9 @@ export default function Dropdown({ allowCreate, allowEmpty = true, children, cla
|
|
|
178
241
|
}
|
|
179
242
|
}, []);
|
|
180
243
|
const handleMouseOut = useCallback((event) => {
|
|
181
|
-
if (!hasItemsRef.current)
|
|
182
|
-
return;
|
|
244
|
+
if (!hasItemsRef.current) return;
|
|
183
245
|
const activeItem = getActiveItemElement(dropdownElementRef.current);
|
|
184
|
-
if (!activeItem)
|
|
185
|
-
return;
|
|
246
|
+
if (!activeItem) return;
|
|
186
247
|
const eventRelatedTarget = event.relatedTarget;
|
|
187
248
|
if (activeItem !== event.target || activeItem.contains(eventRelatedTarget)) {
|
|
188
249
|
return;
|
|
@@ -190,296 +251,371 @@ export default function Dropdown({ allowCreate, allowEmpty = true, children, cla
|
|
|
190
251
|
// If user moused out of activeItem (not into a descendant), it’s no longer active
|
|
191
252
|
delete activeItem.dataset.uktActive;
|
|
192
253
|
}, []);
|
|
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)
|
|
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) {
|
|
246
276
|
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
|
-
}
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
const isTargetUsingKeyEvents = isEventTargetUsingKeyEvent(event);
|
|
257
|
-
// If dropdown isOpen + hasItems & eventTargetNotUsingKeyEvents, handle characters
|
|
258
|
-
if (hasItemsRef.current && !isTargetUsingKeyEvents) {
|
|
259
|
-
let isEditingCharacters = !ctrlKey && !metaKey && /^[A-Za-z0-9]$/.test(key);
|
|
260
|
-
// User could also be editing characters if there are already characters entered
|
|
261
|
-
// and they are hitting delete or spacebar
|
|
262
|
-
if (!isEditingCharacters && enteredCharactersRef.current) {
|
|
263
|
-
isEditingCharacters = key === ' ' || key === 'Backspace';
|
|
264
277
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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();
|
|
272
287
|
}
|
|
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
288
|
return;
|
|
288
289
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
if (key === 'Enter' || (key === ' ' && !inputElementRef.current)) {
|
|
292
|
-
onEventHandled();
|
|
290
|
+
// If dropdown has no items and click was within dropdown body, do nothing
|
|
291
|
+
if (!hasItemsRef.current) return;
|
|
293
292
|
handleSubmitItem(event);
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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);
|
|
314
319
|
}
|
|
315
320
|
return;
|
|
316
321
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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';
|
|
322
331
|
}
|
|
323
|
-
|
|
324
|
-
|
|
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;
|
|
325
357
|
}
|
|
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
|
-
}
|
|
347
|
-
else {
|
|
348
|
-
inputElement =
|
|
349
|
-
ref.firstElementChild.querySelector(TEXT_INPUT_SELECTOR);
|
|
350
358
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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)
|
|
359
|
+
// If dropdown isOpen, handle submitting the value
|
|
360
|
+
if (key === 'Enter' || (key === ' ' && !inputElementRef.current)) {
|
|
361
|
+
onEventHandled();
|
|
362
|
+
handleSubmitItem(event);
|
|
364
363
|
return;
|
|
365
|
-
|
|
366
|
-
if
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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();
|
|
371
373
|
}
|
|
372
374
|
return;
|
|
373
375
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
+
}
|
|
378
397
|
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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;
|
|
389
410
|
return;
|
|
390
411
|
}
|
|
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;
|
|
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;
|
|
420
423
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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);
|
|
436
473
|
if (ownerDocument !== document) {
|
|
437
|
-
ownerDocument.
|
|
438
|
-
ownerDocument.
|
|
439
|
-
ownerDocument.
|
|
474
|
+
ownerDocument.addEventListener('focusin', handleGlobalFocusIn);
|
|
475
|
+
ownerDocument.addEventListener('mousedown', handleGlobalMouseDown);
|
|
476
|
+
ownerDocument.addEventListener('mouseup', handleGlobalMouseUp);
|
|
440
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
|
+
};
|
|
441
507
|
if (inputElement) {
|
|
442
|
-
inputElement.
|
|
508
|
+
inputElement.addEventListener('input', handleInput);
|
|
443
509
|
}
|
|
444
|
-
|
|
445
|
-
|
|
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
|
+
);
|
|
446
526
|
if (!isTriggerFromProps) {
|
|
447
527
|
if (isSearchable) {
|
|
448
|
-
trigger =
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
+
);
|
|
452
547
|
}
|
|
453
548
|
}
|
|
454
549
|
if (label) {
|
|
455
|
-
trigger =
|
|
456
|
-
|
|
457
|
-
|
|
550
|
+
trigger = React.createElement(
|
|
551
|
+
'label',
|
|
552
|
+
{ className: LABEL_CLASS_NAME },
|
|
553
|
+
React.createElement('div', { className: LABEL_TEXT_CLASS_NAME }, label),
|
|
554
|
+
trigger,
|
|
555
|
+
);
|
|
458
556
|
}
|
|
459
|
-
const style = useMemo(
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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
|
+
},
|
|
475
597
|
trigger,
|
|
476
|
-
isOpen
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
+
);
|
|
484
620
|
}
|
|
485
|
-
//# sourceMappingURL=Dropdown.js.map
|
|
621
|
+
//# sourceMappingURL=Dropdown.js.map
|