@acusti/dropdown 0.44.0 → 0.44.1

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