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