@acusti/dropdown 0.38.2 → 0.38.4

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