@acusti/dropdown 0.24.0 → 0.26.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 +319 -420
- package/dist/Dropdown.js.flow +43 -43
- package/dist/helpers.d.ts +32 -47
- package/dist/helpers.js +36 -35
- package/dist/helpers.js.flow +36 -36
- package/dist/styles.d.ts +1 -1
- package/dist/styles.js +1 -1
- package/dist/styles.js.flow +1 -1
- package/package.json +2 -2
package/dist/Dropdown.js
CHANGED
|
@@ -5,45 +5,12 @@ 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
|
-
|
|
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';
|
|
8
|
+
import { BODY_CLASS_NAME, BODY_SELECTOR, LABEL_CLASS_NAME, LABEL_TEXT_CLASS_NAME, ROOT_CLASS_NAME, STYLES, TRIGGER_CLASS_NAME, } from './styles.js';
|
|
9
|
+
import { getActiveItemElement, getItemElements, ITEM_SELECTOR, KEY_EVENT_ELEMENTS, setActiveItem, } from './helpers.js';
|
|
24
10
|
const { Children, Fragment, useCallback, useEffect, useRef, useState } = React;
|
|
25
|
-
const noop = () => {};
|
|
26
|
-
const CHILDREN_ERROR =
|
|
27
|
-
|
|
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
|
-
}) => {
|
|
11
|
+
const noop = () => { };
|
|
12
|
+
const CHILDREN_ERROR = '@acusti/dropdown requires either 1 child (the dropdown body) or 2 children: the dropdown trigger and the dropdown body.';
|
|
13
|
+
const Dropdown = ({ allowEmpty = true, children, className, disabled, hasItems = true, isOpenOnMount, isSearchable, keepOpenOnSubmit = !hasItems, label, name, onClick, onMouseDown, onMouseUp, onSubmitItem, placeholder, tabIndex, value, }) => {
|
|
47
14
|
const childrenCount = Children.count(children);
|
|
48
15
|
if (childrenCount !== 1 && childrenCount !== 2) {
|
|
49
16
|
if (childrenCount === 0) {
|
|
@@ -101,84 +68,71 @@ const Dropdown = ({
|
|
|
101
68
|
closingTimerRef.current = null;
|
|
102
69
|
}
|
|
103
70
|
}, []);
|
|
104
|
-
const handleSubmitItem = useCallback(
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
}
|
|
121
|
-
}
|
|
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;
|
|
134
|
-
}
|
|
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
|
-
}
|
|
71
|
+
const handleSubmitItem = useCallback((event) => {
|
|
72
|
+
var _a;
|
|
73
|
+
const eventTarget = event.target;
|
|
74
|
+
if (isOpenRef.current && !keepOpenOnSubmitRef.current) {
|
|
75
|
+
const keepOpen = eventTarget.closest('[data-ukt-keep-open]');
|
|
76
|
+
// Don’t close dropdown if event occurs w/in data-ukt-keep-open element
|
|
77
|
+
if (!(keepOpen === null || keepOpen === void 0 ? void 0 : keepOpen.dataset.uktKeepOpen) ||
|
|
78
|
+
keepOpen.dataset.uktKeepOpen === 'false') {
|
|
79
|
+
// A short timeout before closing is better UX when user selects an item so dropdown
|
|
80
|
+
// doesn’t close before expected. It also enables using <Link />s in the dropdown body.
|
|
81
|
+
closingTimerRef.current = setTimeout(closeDropdown, 90);
|
|
152
82
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
83
|
+
}
|
|
84
|
+
if (!hasItemsRef.current)
|
|
85
|
+
return;
|
|
86
|
+
const nextElement = getActiveItemElement(dropdownElementRef.current);
|
|
87
|
+
if (!nextElement) {
|
|
88
|
+
// If not allowEmpty, don’t allow submitting an empty item
|
|
89
|
+
if (!allowEmptyRef.current)
|
|
90
|
+
return;
|
|
91
|
+
// If we have an input element as trigger & the user didn’t clear the text, do nothing
|
|
92
|
+
if ((_a = inputElementRef.current) === null || _a === void 0 ? void 0 : _a.value)
|
|
93
|
+
return;
|
|
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();
|
|
157
103
|
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
|
|
104
|
+
}
|
|
105
|
+
// If parent is controlling Dropdown via props.value and nextValue is the same, do nothing
|
|
106
|
+
if (valueRef.current && valueRef.current === nextValue)
|
|
107
|
+
return;
|
|
108
|
+
if (onSubmitItemRef.current) {
|
|
109
|
+
onSubmitItemRef.current(nextItem);
|
|
110
|
+
}
|
|
111
|
+
}, [closeDropdown]);
|
|
161
112
|
const handleMouseMove = useCallback(({ clientX, clientY }) => {
|
|
162
113
|
currentInputMethodRef.current = 'mouse';
|
|
163
114
|
const initialPosition = mouseDownPositionRef.current;
|
|
164
|
-
if (!initialPosition)
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
Math.abs(initialPosition.clientY - clientY) < 12
|
|
168
|
-
) {
|
|
115
|
+
if (!initialPosition)
|
|
116
|
+
return;
|
|
117
|
+
if (Math.abs(initialPosition.clientX - clientX) < 12 &&
|
|
118
|
+
Math.abs(initialPosition.clientY - clientY) < 12) {
|
|
169
119
|
return;
|
|
170
120
|
}
|
|
171
121
|
setIsOpening(false);
|
|
172
122
|
}, []);
|
|
173
123
|
const handleMouseOver = useCallback((event) => {
|
|
174
|
-
if (!hasItemsRef.current)
|
|
124
|
+
if (!hasItemsRef.current)
|
|
125
|
+
return;
|
|
175
126
|
// If user isn’t currently using the mouse to navigate the dropdown, do nothing
|
|
176
|
-
if (currentInputMethodRef.current !== 'mouse')
|
|
127
|
+
if (currentInputMethodRef.current !== 'mouse')
|
|
128
|
+
return;
|
|
177
129
|
// Ensure we have the dropdown root HTMLElement
|
|
178
130
|
const dropdownElement = dropdownElementRef.current;
|
|
179
|
-
if (!dropdownElement)
|
|
131
|
+
if (!dropdownElement)
|
|
132
|
+
return;
|
|
180
133
|
const itemElements = getItemElements(dropdownElement);
|
|
181
|
-
if (!itemElements)
|
|
134
|
+
if (!itemElements)
|
|
135
|
+
return;
|
|
182
136
|
const eventTarget = event.target;
|
|
183
137
|
const item = eventTarget.closest(ITEM_SELECTOR);
|
|
184
138
|
const element = item || eventTarget;
|
|
@@ -193,9 +147,11 @@ const Dropdown = ({
|
|
|
193
147
|
}
|
|
194
148
|
}, []);
|
|
195
149
|
const handleMouseOut = useCallback((event) => {
|
|
196
|
-
if (!hasItemsRef.current)
|
|
150
|
+
if (!hasItemsRef.current)
|
|
151
|
+
return;
|
|
197
152
|
const activeItem = getActiveItemElement(dropdownElementRef.current);
|
|
198
|
-
if (!activeItem)
|
|
153
|
+
if (!activeItem)
|
|
154
|
+
return;
|
|
199
155
|
const eventRelatedTarget = event.relatedTarget;
|
|
200
156
|
if (activeItem !== event.target || activeItem.contains(eventRelatedTarget)) {
|
|
201
157
|
return;
|
|
@@ -203,359 +159,302 @@ const Dropdown = ({
|
|
|
203
159
|
// If user moused out of activeItem (not into a descendant), it’s no longer active
|
|
204
160
|
delete activeItem.dataset.uktActive;
|
|
205
161
|
}, []);
|
|
206
|
-
const handleMouseDown = useCallback(
|
|
207
|
-
(
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
},
|
|
221
|
-
|
|
222
|
-
)
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
) {
|
|
236
|
-
closeDropdown();
|
|
237
|
-
}
|
|
238
|
-
return;
|
|
162
|
+
const handleMouseDown = useCallback((event) => {
|
|
163
|
+
if (onMouseDown)
|
|
164
|
+
onMouseDown(event);
|
|
165
|
+
if (isOpenRef.current)
|
|
166
|
+
return;
|
|
167
|
+
setIsOpen(true);
|
|
168
|
+
setIsOpening(true);
|
|
169
|
+
mouseDownPositionRef.current = {
|
|
170
|
+
clientX: event.clientX,
|
|
171
|
+
clientY: event.clientY,
|
|
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();
|
|
239
191
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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]);
|
|
246
199
|
const cleanupEventListenersRef = useRef(noop);
|
|
247
|
-
const handleRef = useCallback(
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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) {
|
|
229
|
+
setIsOpening(false);
|
|
230
|
+
if (isOpeningTimerRef.current) {
|
|
231
|
+
clearTimeout(isOpeningTimerRef.current);
|
|
232
|
+
isOpeningTimerRef.current = null;
|
|
233
|
+
}
|
|
254
234
|
return;
|
|
255
235
|
}
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
inputElement = ref.firstElementChild.querySelector(
|
|
261
|
-
'input:not([type=radio]):not([type=checkbox]):not([type=range]),textarea',
|
|
262
|
-
);
|
|
263
|
-
inputElementRef.current = inputElement;
|
|
236
|
+
const eventTarget = target;
|
|
237
|
+
// Only handle mouseup events from outside the dropdown here
|
|
238
|
+
if (!((_a = dropdownElementRef.current) === null || _a === void 0 ? void 0 : _a.contains(eventTarget))) {
|
|
239
|
+
closeDropdown();
|
|
264
240
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
241
|
+
};
|
|
242
|
+
const handleGlobalKeyDown = (event) => {
|
|
243
|
+
const { altKey, ctrlKey, key, metaKey } = event;
|
|
244
|
+
const eventTarget = event.target;
|
|
245
|
+
const dropdownElement = dropdownElementRef.current;
|
|
246
|
+
if (!dropdownElement)
|
|
247
|
+
return;
|
|
248
|
+
const onEventHandled = () => {
|
|
249
|
+
event.stopPropagation();
|
|
250
|
+
event.preventDefault();
|
|
251
|
+
currentInputMethodRef.current = 'keyboard';
|
|
274
252
|
};
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
if
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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);
|
|
285
265
|
return;
|
|
286
266
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
// If dropdown isOpen, hasItems, and not isSearchable, handle entering characters
|
|
270
|
+
if (hasItemsRef.current && !inputElementRef.current) {
|
|
271
|
+
let isEditingCharacters = !ctrlKey && !metaKey && /^[A-Za-z0-9]$/.test(key);
|
|
272
|
+
// User could also be editing characters if there are already characters entered
|
|
273
|
+
// and they are hitting delete or spacebar
|
|
274
|
+
if (!isEditingCharacters && enteredCharactersRef.current) {
|
|
275
|
+
isEditingCharacters = key === ' ' || key === 'Backspace';
|
|
295
276
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
if (
|
|
313
|
-
|
|
314
|
-
key === 'Enter' ||
|
|
315
|
-
(hasItemsRef.current &&
|
|
316
|
-
(key === 'ArrowUp' || key === 'ArrowDown'))
|
|
317
|
-
) {
|
|
318
|
-
onEventHandled();
|
|
319
|
-
setIsOpen(true);
|
|
320
|
-
return;
|
|
277
|
+
if (isEditingCharacters) {
|
|
278
|
+
onEventHandled();
|
|
279
|
+
if (key === 'Backspace') {
|
|
280
|
+
enteredCharactersRef.current =
|
|
281
|
+
enteredCharactersRef.current.slice(0, -1);
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
enteredCharactersRef.current += key;
|
|
285
|
+
}
|
|
286
|
+
setActiveItem({
|
|
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);
|
|
321
295
|
}
|
|
296
|
+
clearEnteredCharactersTimerRef.current = setTimeout(() => {
|
|
297
|
+
enteredCharactersRef.current = '';
|
|
298
|
+
clearEnteredCharactersTimerRef.current = null;
|
|
299
|
+
}, 1500);
|
|
322
300
|
return;
|
|
323
301
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
302
|
+
}
|
|
303
|
+
// If dropdown isOpen, handle submitting the value
|
|
304
|
+
if (key === 'Enter' || (key === ' ' && !inputElementRef.current)) {
|
|
305
|
+
onEventHandled();
|
|
306
|
+
handleSubmitItem(event);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
// If dropdown isOpen, handle closing it on escape or spacebar if !hasItems
|
|
310
|
+
if (key === 'Escape' ||
|
|
311
|
+
(isEventTargetingDropdown && key === ' ' && !hasItemsRef.current)) {
|
|
312
|
+
// If there are no items & event target element uses key events, don’t close it
|
|
313
|
+
if (!hasItemsRef.current &&
|
|
314
|
+
(eventTarget.isContentEditable ||
|
|
315
|
+
KEY_EVENT_ELEMENTS.has(eventTarget.tagName))) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
closeDropdown();
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
// Handle ↑/↓ arrows
|
|
322
|
+
if (hasItemsRef.current) {
|
|
323
|
+
if (key === 'ArrowUp') {
|
|
324
|
+
onEventHandled();
|
|
325
|
+
if (altKey || metaKey) {
|
|
341
326
|
setActiveItem({
|
|
342
327
|
dropdownElement,
|
|
343
|
-
|
|
344
|
-
// with an exact text match so user can enter a value not in items
|
|
345
|
-
isExactMatch: isTriggerFromPropsRef.current,
|
|
346
|
-
text: enteredCharactersRef.current,
|
|
328
|
+
index: 0,
|
|
347
329
|
});
|
|
348
|
-
if (clearEnteredCharactersTimerRef.current) {
|
|
349
|
-
clearTimeout(clearEnteredCharactersTimerRef.current);
|
|
350
|
-
}
|
|
351
|
-
clearEnteredCharactersTimerRef.current = setTimeout(() => {
|
|
352
|
-
enteredCharactersRef.current = '';
|
|
353
|
-
clearEnteredCharactersTimerRef.current = null;
|
|
354
|
-
}, 1500);
|
|
355
|
-
return;
|
|
356
330
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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;
|
|
331
|
+
else {
|
|
332
|
+
setActiveItem({
|
|
333
|
+
dropdownElement,
|
|
334
|
+
indexAddend: -1,
|
|
335
|
+
});
|
|
376
336
|
}
|
|
377
|
-
closeDropdown();
|
|
378
337
|
return;
|
|
379
338
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
if (
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
});
|
|
389
|
-
} else {
|
|
390
|
-
setActiveItem({
|
|
391
|
-
dropdownElement,
|
|
392
|
-
indexAddend: -1,
|
|
393
|
-
});
|
|
394
|
-
}
|
|
395
|
-
return;
|
|
339
|
+
if (key === 'ArrowDown') {
|
|
340
|
+
onEventHandled();
|
|
341
|
+
if (altKey || metaKey) {
|
|
342
|
+
// Using a negative index counts back from the end
|
|
343
|
+
setActiveItem({
|
|
344
|
+
dropdownElement,
|
|
345
|
+
index: -1,
|
|
346
|
+
});
|
|
396
347
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
dropdownElement,
|
|
403
|
-
index: -1,
|
|
404
|
-
});
|
|
405
|
-
} else {
|
|
406
|
-
setActiveItem({
|
|
407
|
-
dropdownElement,
|
|
408
|
-
indexAddend: 1,
|
|
409
|
-
});
|
|
410
|
-
}
|
|
411
|
-
return;
|
|
348
|
+
else {
|
|
349
|
+
setActiveItem({
|
|
350
|
+
dropdownElement,
|
|
351
|
+
indexAddend: 1,
|
|
352
|
+
});
|
|
412
353
|
}
|
|
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
|
-
) {
|
|
425
354
|
return;
|
|
426
355
|
}
|
|
427
|
-
closeDropdown();
|
|
428
|
-
};
|
|
429
|
-
document.addEventListener('focusin', handleGlobalFocusIn);
|
|
430
|
-
document.addEventListener('keydown', handleGlobalKeyDown);
|
|
431
|
-
document.addEventListener('mousedown', handleGlobalMouseDown);
|
|
432
|
-
document.addEventListener('mouseup', handleGlobalMouseUp);
|
|
433
|
-
if (ownerDocument !== document) {
|
|
434
|
-
ownerDocument.addEventListener('focusin', handleGlobalFocusIn);
|
|
435
|
-
ownerDocument.addEventListener('keydown', handleGlobalKeyDown);
|
|
436
|
-
ownerDocument.addEventListener('mousedown', handleGlobalMouseDown);
|
|
437
|
-
ownerDocument.addEventListener('mouseup', handleGlobalMouseUp);
|
|
438
356
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
357
|
+
};
|
|
358
|
+
// Close dropdown if any element is focused outside of this dropdown
|
|
359
|
+
const handleGlobalFocusIn = ({ target }) => {
|
|
360
|
+
if (!isOpenRef.current)
|
|
361
|
+
return;
|
|
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);
|
|
413
|
+
if (ownerDocument !== document) {
|
|
414
|
+
ownerDocument.removeEventListener('focusin', handleGlobalFocusIn);
|
|
415
|
+
ownerDocument.removeEventListener('keydown', handleGlobalKeyDown);
|
|
416
|
+
ownerDocument.removeEventListener('mousedown', handleGlobalMouseDown);
|
|
417
|
+
ownerDocument.removeEventListener('mouseup', handleGlobalMouseUp);
|
|
442
418
|
}
|
|
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
|
-
};
|
|
461
419
|
if (inputElement) {
|
|
462
|
-
inputElement.
|
|
420
|
+
inputElement.removeEventListener('input', handleInput);
|
|
463
421
|
}
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
);
|
|
422
|
+
};
|
|
423
|
+
}, [closeDropdown, handleSubmitItem, isOpenOnMount, isTriggerFromProps]);
|
|
482
424
|
const handleTriggerFocus = useCallback(() => {
|
|
483
425
|
setIsOpen(true);
|
|
484
426
|
}, []);
|
|
485
427
|
if (!isTriggerFromProps) {
|
|
486
428
|
if (isSearchable) {
|
|
487
|
-
trigger = React.createElement(InputText, {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
-
);
|
|
429
|
+
trigger = (React.createElement(InputText, { className: TRIGGER_CLASS_NAME, disabled: disabled, initialValue: value || '', name: name, onFocus: handleTriggerFocus, placeholder: placeholder, ref: inputElementRef, selectTextOnFocus: true, tabIndex: tabIndex, type: "text" }));
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
trigger = (React.createElement("button", { className: TRIGGER_CLASS_NAME, tabIndex: 0 }, trigger));
|
|
505
433
|
}
|
|
506
434
|
}
|
|
507
435
|
if (label) {
|
|
508
|
-
trigger = React.createElement(
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
React.createElement('div', { className: LABEL_TEXT_CLASS_NAME }, label),
|
|
512
|
-
trigger,
|
|
513
|
-
);
|
|
436
|
+
trigger = (React.createElement("label", { className: LABEL_CLASS_NAME },
|
|
437
|
+
React.createElement("div", { className: LABEL_TEXT_CLASS_NAME }, label),
|
|
438
|
+
trigger));
|
|
514
439
|
}
|
|
515
|
-
return React.createElement(
|
|
516
|
-
Fragment,
|
|
517
|
-
null,
|
|
440
|
+
return (React.createElement(Fragment, null,
|
|
518
441
|
React.createElement(Style, null, STYLES),
|
|
519
|
-
React.createElement(
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
-
},
|
|
442
|
+
React.createElement("div", { className: clsx(ROOT_CLASS_NAME, className, {
|
|
443
|
+
disabled,
|
|
444
|
+
'is-open': isOpen,
|
|
445
|
+
'is-searchable': isSearchable,
|
|
446
|
+
}), onClick: onClick, onMouseDown: handleMouseDown, onMouseUp: handleMouseUp, onMouseMove: handleMouseMove, onMouseOut: handleMouseOut, onMouseOver: handleMouseOver, ref: handleRef, tabIndex: isSearchable || inputElementRef.current || !isTriggerFromProps
|
|
447
|
+
? undefined
|
|
448
|
+
: 0 },
|
|
539
449
|
trigger,
|
|
540
|
-
isOpen
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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
|
-
);
|
|
450
|
+
isOpen ? (React.createElement("div", { className: clsx(BODY_CLASS_NAME, {
|
|
451
|
+
'calculating-position': !outOfBounds.hasLayout,
|
|
452
|
+
'has-items': hasItems,
|
|
453
|
+
'out-of-bounds-bottom': outOfBounds.bottom,
|
|
454
|
+
'out-of-bounds-left': outOfBounds.left,
|
|
455
|
+
'out-of-bounds-right': outOfBounds.right,
|
|
456
|
+
'out-of-bounds-top': outOfBounds.top,
|
|
457
|
+
}), ref: setDropdownBodyElement }, children[1] || children[0] || children)) : null)));
|
|
559
458
|
};
|
|
560
459
|
export default Dropdown;
|
|
561
|
-
//# sourceMappingURL=Dropdown.js.map
|
|
460
|
+
//# sourceMappingURL=Dropdown.js.map
|
package/dist/Dropdown.js.flow
CHANGED
|
@@ -5,58 +5,58 @@
|
|
|
5
5
|
* @flow
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import * as React from
|
|
8
|
+
import * as React from "react";
|
|
9
9
|
export type Item = {|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
element: HTMLElement | null,
|
|
11
|
+
value: string,
|
|
12
12
|
|};
|
|
13
13
|
export type Props = {|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Boolean indicating if the user can submit an empty value (i.e. clear the value); defaults to true
|
|
16
|
+
*/
|
|
17
|
+
allowEmpty?: boolean,
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Can take a single React element (e.g. ReactChild) or exactly two renderable children
|
|
21
|
+
*/
|
|
22
|
+
children: React.Element<any> | [React.Node, React.Node],
|
|
23
|
+
className?: string,
|
|
24
|
+
disabled?: boolean,
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Group identifier string links dropdowns together into a menu (like macOS top menubar)
|
|
28
|
+
*/
|
|
29
|
+
group?: string,
|
|
30
|
+
hasItems?: boolean,
|
|
31
|
+
isOpenOnMount?: boolean,
|
|
32
|
+
isSearchable?: boolean,
|
|
33
|
+
keepOpenOnSubmit?: boolean,
|
|
34
|
+
label?: string,
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Only usable in conjunction with {isSearchable: true}; used as search input’s name
|
|
38
|
+
*/
|
|
39
|
+
name?: string,
|
|
40
|
+
onClick?: (event: SyntheticMouseEvent<HTMLElement>) => mixed,
|
|
41
|
+
onMouseDown?: (event: SyntheticMouseEvent<HTMLElement>) => mixed,
|
|
42
|
+
onMouseUp?: (event: SyntheticMouseEvent<HTMLElement>) => mixed,
|
|
43
|
+
onSubmitItem?: (payload: Item) => void,
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Only usable in conjunction with {isSearchable: true}; used as search input’s placeholder
|
|
47
|
+
*/
|
|
48
|
+
placeholder?: string,
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Only usable in conjunction with {isSearchable: true}; used as search input’s tabIndex
|
|
52
|
+
*/
|
|
53
|
+
tabIndex?: number,
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Used as search input’s value if props.isSearchable === true
|
|
57
|
+
* Used to determine if value has changed to avoid triggering onSubmitItem if not
|
|
58
|
+
*/
|
|
59
|
+
value?: string,
|
|
60
60
|
|};
|
|
61
61
|
declare var Dropdown: React.StatelessFunctionalComponent<Props>;
|
|
62
62
|
declare export default typeof Dropdown;
|
package/dist/helpers.d.ts
CHANGED
|
@@ -1,48 +1,33 @@
|
|
|
1
|
-
export declare const ITEM_SELECTOR =
|
|
1
|
+
export declare const ITEM_SELECTOR = "[data-ukt-item], [data-ukt-value]";
|
|
2
2
|
export declare const KEY_EVENT_ELEMENTS: Set<string>;
|
|
3
|
-
export declare const getItemElements: (
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
dropdownElement: HTMLElement;
|
|
35
|
-
element?: null | undefined;
|
|
36
|
-
index?: null | undefined;
|
|
37
|
-
indexAddend: number;
|
|
38
|
-
isExactMatch?: null | undefined;
|
|
39
|
-
text?: null | undefined;
|
|
40
|
-
}
|
|
41
|
-
| {
|
|
42
|
-
dropdownElement: HTMLElement;
|
|
43
|
-
element?: null | undefined;
|
|
44
|
-
index?: null | undefined;
|
|
45
|
-
indexAddend?: null | undefined;
|
|
46
|
-
isExactMatch?: boolean | undefined;
|
|
47
|
-
text: string;
|
|
48
|
-
}) => void;
|
|
3
|
+
export declare const getItemElements: (dropdownElement: HTMLElement | null) => NodeListOf<Element> | HTMLCollection | null;
|
|
4
|
+
export declare const getActiveItemElement: (dropdownElement: HTMLElement | null) => HTMLElement | null;
|
|
5
|
+
export declare const setActiveItem: ({ dropdownElement, element, index, indexAddend, isExactMatch, text, }: {
|
|
6
|
+
dropdownElement: HTMLElement;
|
|
7
|
+
element: HTMLElement;
|
|
8
|
+
index?: null | undefined;
|
|
9
|
+
indexAddend?: null | undefined;
|
|
10
|
+
isExactMatch?: null | undefined;
|
|
11
|
+
text?: null | undefined;
|
|
12
|
+
} | {
|
|
13
|
+
dropdownElement: HTMLElement;
|
|
14
|
+
element?: null | undefined;
|
|
15
|
+
index: number;
|
|
16
|
+
indexAddend?: null | undefined;
|
|
17
|
+
isExactMatch?: null | undefined;
|
|
18
|
+
text?: null | undefined;
|
|
19
|
+
} | {
|
|
20
|
+
dropdownElement: HTMLElement;
|
|
21
|
+
element?: null | undefined;
|
|
22
|
+
index?: null | undefined;
|
|
23
|
+
indexAddend: number;
|
|
24
|
+
isExactMatch?: null | undefined;
|
|
25
|
+
text?: null | undefined;
|
|
26
|
+
} | {
|
|
27
|
+
dropdownElement: HTMLElement;
|
|
28
|
+
element?: null | undefined;
|
|
29
|
+
index?: null | undefined;
|
|
30
|
+
indexAddend?: null | undefined;
|
|
31
|
+
isExactMatch?: boolean | undefined;
|
|
32
|
+
text: string;
|
|
33
|
+
}) => void;
|
package/dist/helpers.js
CHANGED
|
@@ -3,16 +3,20 @@ import { BODY_SELECTOR } from './styles.js';
|
|
|
3
3
|
export const ITEM_SELECTOR = `[data-ukt-item], [data-ukt-value]`;
|
|
4
4
|
export const KEY_EVENT_ELEMENTS = new Set(['INPUT', 'TEXTAREA']);
|
|
5
5
|
export const getItemElements = (dropdownElement) => {
|
|
6
|
-
if (!dropdownElement)
|
|
6
|
+
if (!dropdownElement)
|
|
7
|
+
return null;
|
|
7
8
|
const bodyElement = dropdownElement.querySelector(BODY_SELECTOR);
|
|
8
|
-
if (!bodyElement)
|
|
9
|
+
if (!bodyElement)
|
|
10
|
+
return null;
|
|
9
11
|
let items = bodyElement.querySelectorAll(ITEM_SELECTOR);
|
|
10
|
-
if (items.length)
|
|
12
|
+
if (items.length)
|
|
13
|
+
return items;
|
|
11
14
|
// If no items found via [data-ukt-item] or [data-ukt-value] selector,
|
|
12
15
|
// use first instance of multiple children found
|
|
13
16
|
items = bodyElement.children;
|
|
14
17
|
while (items.length === 1) {
|
|
15
|
-
if (!items[0].children)
|
|
18
|
+
if (!items[0].children)
|
|
19
|
+
break;
|
|
16
20
|
items = items[0].children;
|
|
17
21
|
}
|
|
18
22
|
// If unable to find an element with more than one child, treat direct child as items
|
|
@@ -22,7 +26,8 @@ export const getItemElements = (dropdownElement) => {
|
|
|
22
26
|
return items;
|
|
23
27
|
};
|
|
24
28
|
export const getActiveItemElement = (dropdownElement) => {
|
|
25
|
-
if (!dropdownElement)
|
|
29
|
+
if (!dropdownElement)
|
|
30
|
+
return null;
|
|
26
31
|
return dropdownElement.querySelector('[data-ukt-active]');
|
|
27
32
|
};
|
|
28
33
|
const clearItemElementsState = (itemElements) => {
|
|
@@ -32,45 +37,40 @@ const clearItemElementsState = (itemElements) => {
|
|
|
32
37
|
}
|
|
33
38
|
});
|
|
34
39
|
};
|
|
35
|
-
export const setActiveItem = ({
|
|
36
|
-
dropdownElement,
|
|
37
|
-
element,
|
|
38
|
-
index,
|
|
39
|
-
indexAddend,
|
|
40
|
-
isExactMatch,
|
|
41
|
-
text,
|
|
42
|
-
}) => {
|
|
40
|
+
export const setActiveItem = ({ dropdownElement, element, index, indexAddend, isExactMatch, text, }) => {
|
|
43
41
|
const items = getItemElements(dropdownElement);
|
|
44
|
-
if (!items)
|
|
42
|
+
if (!items)
|
|
43
|
+
return;
|
|
45
44
|
const itemElements = Array.from(items);
|
|
46
|
-
if (!itemElements.length)
|
|
45
|
+
if (!itemElements.length)
|
|
46
|
+
return;
|
|
47
47
|
const lastIndex = itemElements.length - 1;
|
|
48
|
-
const currentActiveIndex = itemElements.findIndex((itemElement) =>
|
|
49
|
-
itemElement.hasAttribute('data-ukt-active'),
|
|
50
|
-
);
|
|
48
|
+
const currentActiveIndex = itemElements.findIndex((itemElement) => itemElement.hasAttribute('data-ukt-active'));
|
|
51
49
|
let nextActiveIndex = currentActiveIndex;
|
|
52
50
|
if (typeof index === 'number') {
|
|
53
51
|
// Negative index means count back from the end
|
|
54
52
|
nextActiveIndex = index < 0 ? itemElements.length + index : index;
|
|
55
53
|
}
|
|
56
54
|
if (element) {
|
|
57
|
-
nextActiveIndex = itemElements.findIndex(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
} else if (typeof indexAddend === 'number') {
|
|
55
|
+
nextActiveIndex = itemElements.findIndex((itemElement) => itemElement === element);
|
|
56
|
+
}
|
|
57
|
+
else if (typeof indexAddend === 'number') {
|
|
61
58
|
// If there’s no currentActiveIndex and we are handling -1, start at lastIndex
|
|
62
59
|
if (currentActiveIndex === -1 && indexAddend === -1) {
|
|
63
60
|
nextActiveIndex = lastIndex;
|
|
64
|
-
}
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
65
63
|
nextActiveIndex += indexAddend;
|
|
66
64
|
}
|
|
67
65
|
// Keep it within the bounds of the items list
|
|
68
66
|
if (nextActiveIndex < 0) {
|
|
69
67
|
nextActiveIndex = 0;
|
|
70
|
-
}
|
|
68
|
+
}
|
|
69
|
+
else if (nextActiveIndex > lastIndex) {
|
|
71
70
|
nextActiveIndex = lastIndex;
|
|
72
71
|
}
|
|
73
|
-
}
|
|
72
|
+
}
|
|
73
|
+
else if (typeof text === 'string') {
|
|
74
74
|
// If text is empty, clear existing active items and early return
|
|
75
75
|
if (!text) {
|
|
76
76
|
clearItemElementsState(itemElements);
|
|
@@ -79,19 +79,19 @@ export const setActiveItem = ({
|
|
|
79
79
|
const itemTexts = itemElements.map((itemElement) => itemElement.innerText);
|
|
80
80
|
if (isExactMatch) {
|
|
81
81
|
const textToCompare = text.toLowerCase();
|
|
82
|
-
nextActiveIndex = itemTexts.findIndex((itemText) =>
|
|
83
|
-
itemText.toLowerCase().startsWith(textToCompare),
|
|
84
|
-
);
|
|
82
|
+
nextActiveIndex = itemTexts.findIndex((itemText) => itemText.toLowerCase().startsWith(textToCompare));
|
|
85
83
|
// If isExactMatch is required and no exact match was found, clear active items
|
|
86
84
|
if (nextActiveIndex === -1) {
|
|
87
85
|
clearItemElementsState(itemElements);
|
|
88
86
|
}
|
|
89
|
-
}
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
90
89
|
const bestMatch = getBestMatch({ items: itemTexts, text });
|
|
91
90
|
nextActiveIndex = itemTexts.findIndex((text) => text === bestMatch);
|
|
92
91
|
}
|
|
93
92
|
}
|
|
94
|
-
if (nextActiveIndex === -1 || nextActiveIndex === currentActiveIndex)
|
|
93
|
+
if (nextActiveIndex === -1 || nextActiveIndex === currentActiveIndex)
|
|
94
|
+
return;
|
|
95
95
|
// Clear any existing active dropdown body item state
|
|
96
96
|
clearItemElementsState(itemElements);
|
|
97
97
|
const nextActiveItem = items[nextActiveIndex];
|
|
@@ -101,11 +101,11 @@ export const setActiveItem = ({
|
|
|
101
101
|
let { parentElement } = nextActiveItem;
|
|
102
102
|
let scrollableParent = null;
|
|
103
103
|
while (!scrollableParent && parentElement && parentElement !== dropdownElement) {
|
|
104
|
-
const isScrollable =
|
|
105
|
-
parentElement.scrollHeight > parentElement.clientHeight + 15;
|
|
104
|
+
const isScrollable = parentElement.scrollHeight > parentElement.clientHeight + 15;
|
|
106
105
|
if (isScrollable) {
|
|
107
106
|
scrollableParent = parentElement;
|
|
108
|
-
}
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
109
|
parentElement = parentElement.parentElement;
|
|
110
110
|
}
|
|
111
111
|
}
|
|
@@ -119,7 +119,8 @@ export const setActiveItem = ({
|
|
|
119
119
|
// Item isn’t fully visible; adjust scrollTop to put item within closest edge
|
|
120
120
|
if (isAboveTop) {
|
|
121
121
|
scrollTop -= parentRect.top - itemRect.top;
|
|
122
|
-
}
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
123
124
|
scrollTop += itemRect.bottom - parentRect.bottom;
|
|
124
125
|
}
|
|
125
126
|
scrollableParent.scrollTop = scrollTop;
|
|
@@ -127,4 +128,4 @@ export const setActiveItem = ({
|
|
|
127
128
|
}
|
|
128
129
|
}
|
|
129
130
|
};
|
|
130
|
-
//# sourceMappingURL=helpers.js.map
|
|
131
|
+
//# sourceMappingURL=helpers.js.map
|
package/dist/helpers.js.flow
CHANGED
|
@@ -5,46 +5,46 @@
|
|
|
5
5
|
* @flow
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
declare export var ITEM_SELECTOR:
|
|
8
|
+
declare export var ITEM_SELECTOR: "[data-ukt-item], [data-ukt-value]";
|
|
9
9
|
declare export var KEY_EVENT_ELEMENTS: Set<string>;
|
|
10
10
|
declare export var getItemElements: (
|
|
11
|
-
|
|
11
|
+
dropdownElement: HTMLElement | null
|
|
12
12
|
) => NodeListOf<Element> | HTMLCollection | null;
|
|
13
13
|
declare export var getActiveItemElement: (
|
|
14
|
-
|
|
14
|
+
dropdownElement: HTMLElement | null
|
|
15
15
|
) => HTMLElement | null;
|
|
16
16
|
declare export var setActiveItem: (
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
17
|
+
x:
|
|
18
|
+
| {|
|
|
19
|
+
dropdownElement: HTMLElement,
|
|
20
|
+
element: HTMLElement,
|
|
21
|
+
index?: null | void,
|
|
22
|
+
indexAddend?: null | void,
|
|
23
|
+
isExactMatch?: null | void,
|
|
24
|
+
text?: null | void,
|
|
25
|
+
|}
|
|
26
|
+
| {|
|
|
27
|
+
dropdownElement: HTMLElement,
|
|
28
|
+
element?: null | void,
|
|
29
|
+
index: number,
|
|
30
|
+
indexAddend?: null | void,
|
|
31
|
+
isExactMatch?: null | void,
|
|
32
|
+
text?: null | void,
|
|
33
|
+
|}
|
|
34
|
+
| {|
|
|
35
|
+
dropdownElement: HTMLElement,
|
|
36
|
+
element?: null | void,
|
|
37
|
+
index?: null | void,
|
|
38
|
+
indexAddend: number,
|
|
39
|
+
isExactMatch?: null | void,
|
|
40
|
+
text?: null | void,
|
|
41
|
+
|}
|
|
42
|
+
| {|
|
|
43
|
+
dropdownElement: HTMLElement,
|
|
44
|
+
element?: null | void,
|
|
45
|
+
index?: null | void,
|
|
46
|
+
indexAddend?: null | void,
|
|
47
|
+
isExactMatch?: boolean | void,
|
|
48
|
+
text: string,
|
|
49
|
+
|}
|
|
50
50
|
) => void;
|
package/dist/styles.d.ts
CHANGED
package/dist/styles.js
CHANGED
package/dist/styles.js.flow
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@acusti/dropdown",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"description": "React component that renders a dropdown with a trigger and supports searching, keyboard access, and more",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"typescript": "^4.9.3"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@acusti/input-text": "^1.0
|
|
46
|
+
"@acusti/input-text": "^1.2.0",
|
|
47
47
|
"@acusti/matchmaking": "^0.4.0",
|
|
48
48
|
"@acusti/styling": "^0.6.0",
|
|
49
49
|
"@acusti/use-is-out-of-bounds": "^0.6.0",
|