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