@acusti/dropdown 0.43.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,78 +138,99 @@ 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
+ 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
+ }
111
158
  }
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;
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;
128
171
  }
129
- else {
130
- inputElementRef.current.value = label;
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
+ }
131
191
  }
132
- if (inputElementRef.current ===
133
- inputElementRef.current.ownerDocument.activeElement) {
134
- 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
+ });
135
208
  }
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]);
209
+ },
210
+ [closeDropdown],
211
+ );
145
212
  const handleMouseMove = useCallback(({ clientX, clientY }) => {
146
213
  currentInputMethodRef.current = 'mouse';
147
214
  const initialPosition = mouseDownPositionRef.current;
148
- if (!initialPosition)
149
- return;
150
- if (Math.abs(initialPosition.clientX - clientX) < 12 &&
151
- 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
+ ) {
152
220
  return;
153
221
  }
154
222
  setIsOpening(false);
155
223
  }, []);
156
224
  const handleMouseOver = useCallback((event) => {
157
- if (!hasItemsRef.current)
158
- return;
225
+ if (!hasItemsRef.current) return;
159
226
  // If user isn’t currently using the mouse to navigate the dropdown, do nothing
160
- if (currentInputMethodRef.current !== 'mouse')
161
- return;
227
+ if (currentInputMethodRef.current !== 'mouse') return;
162
228
  // Ensure we have the dropdown root HTMLElement
163
229
  const dropdownElement = dropdownElementRef.current;
164
- if (!dropdownElement)
165
- return;
230
+ if (!dropdownElement) return;
166
231
  const itemElements = getItemElements(dropdownElement);
167
- if (!itemElements)
168
- return;
232
+ if (!itemElements) return;
169
233
  const eventTarget = event.target;
170
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
171
234
  const item = eventTarget.closest(ITEM_SELECTOR);
172
235
  const element = item !== null && item !== void 0 ? item : eventTarget;
173
236
  for (const itemElement of itemElements) {
@@ -178,11 +241,9 @@ export default function Dropdown({ allowCreate, allowEmpty = true, children, cla
178
241
  }
179
242
  }, []);
180
243
  const handleMouseOut = useCallback((event) => {
181
- if (!hasItemsRef.current)
182
- return;
244
+ if (!hasItemsRef.current) return;
183
245
  const activeItem = getActiveItemElement(dropdownElementRef.current);
184
- if (!activeItem)
185
- return;
246
+ if (!activeItem) return;
186
247
  const eventRelatedTarget = event.relatedTarget;
187
248
  if (activeItem !== event.target || activeItem.contains(eventRelatedTarget)) {
188
249
  return;
@@ -190,296 +251,371 @@ export default function Dropdown({ allowCreate, allowEmpty = true, children, cla
190
251
  // If user moused out of activeItem (not into a descendant), it’s no longer active
191
252
  delete activeItem.dataset.uktActive;
192
253
  }, []);
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)
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) {
246
276
  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
- }
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
277
  }
265
- if (isEditingCharacters) {
266
- onEventHandled();
267
- if (key === 'Backspace') {
268
- enteredCharactersRef.current = enteredCharactersRef.current.slice(0, -1);
269
- }
270
- else {
271
- 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();
272
287
  }
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
288
  return;
288
289
  }
289
- }
290
- // If dropdown isOpen, handle submitting the value
291
- if (key === 'Enter' || (key === ' ' && !inputElementRef.current)) {
292
- onEventHandled();
290
+ // If dropdown has no items and click was within dropdown body, do nothing
291
+ if (!hasItemsRef.current) return;
293
292
  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 });
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);
314
319
  }
315
320
  return;
316
321
  }
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 });
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';
322
331
  }
323
- else {
324
- 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;
325
357
  }
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
- }
347
- else {
348
- inputElement =
349
- ref.firstElementChild.querySelector(TEXT_INPUT_SELECTOR);
350
358
  }
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)
359
+ // If dropdown isOpen, handle submitting the value
360
+ if (key === 'Enter' || (key === ' ' && !inputElementRef.current)) {
361
+ onEventHandled();
362
+ handleSubmitItem(event);
364
363
  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;
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();
371
373
  }
372
374
  return;
373
375
  }
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();
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
+ }
378
397
  }
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)) {
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;
389
410
  return;
390
411
  }
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;
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;
420
423
  }
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);
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);
436
473
  if (ownerDocument !== document) {
437
- ownerDocument.removeEventListener('focusin', handleGlobalFocusIn);
438
- ownerDocument.removeEventListener('mousedown', handleGlobalMouseDown);
439
- ownerDocument.removeEventListener('mouseup', handleGlobalMouseUp);
474
+ ownerDocument.addEventListener('focusin', handleGlobalFocusIn);
475
+ ownerDocument.addEventListener('mousedown', handleGlobalMouseDown);
476
+ ownerDocument.addEventListener('mouseup', handleGlobalMouseUp);
440
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
+ };
441
507
  if (inputElement) {
442
- inputElement.removeEventListener('input', handleInput);
508
+ inputElement.addEventListener('input', handleInput);
443
509
  }
444
- };
445
- }, [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
+ );
446
526
  if (!isTriggerFromProps) {
447
527
  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));
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
+ );
452
547
  }
453
548
  }
454
549
  if (label) {
455
- trigger = (React.createElement("label", { className: LABEL_CLASS_NAME },
456
- React.createElement("div", { className: LABEL_TEXT_CLASS_NAME }, label),
457
- 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
+ );
458
556
  }
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,
469
- React.createElement(Style, { href: "@acusti/dropdown/Dropdown" }, 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 },
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
+ },
475
597
  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)));
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
+ );
484
620
  }
485
- //# sourceMappingURL=Dropdown.js.map
621
+ //# sourceMappingURL=Dropdown.js.map