@acusti/dropdown 0.46.0 → 0.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/Dropdown.tsx DELETED
@@ -1,733 +0,0 @@
1
- /* eslint-disable jsx-a11y/click-events-have-key-events, jsx-a11y/mouse-events-have-key-events, jsx-a11y/no-static-element-interactions */
2
- import { Style } from '@acusti/styling';
3
- import useIsOutOfBounds from '@acusti/use-is-out-of-bounds';
4
- import useKeyboardEvents, {
5
- isEventTargetUsingKeyEvent,
6
- } from '@acusti/use-keyboard-events';
7
- import clsx from 'clsx';
8
- import * as React from 'react';
9
-
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';
27
-
28
- export type Item = {
29
- element: HTMLElement | null;
30
- event: Event | React.SyntheticEvent<HTMLElement>;
31
- label: string;
32
- value: string;
33
- };
34
-
35
- export type Props = {
36
- /**
37
- * Boolean indicating if the user can submit a value not already in the
38
- * dropdown.
39
- */
40
- allowCreate?: boolean;
41
- /**
42
- * Boolean indicating if the user can submit an empty value (i.e. clear
43
- * the value). Defaults to true.
44
- */
45
- allowEmpty?: boolean;
46
- /**
47
- * Can take a single React element or exactly two renderable children.
48
- */
49
- children: ChildrenTuple | React.JSX.Element;
50
- className?: string;
51
- disabled?: boolean;
52
- /**
53
- * Group identifier string links dropdowns together into a menu
54
- * (like macOS top menubar).
55
- */
56
- group?: string;
57
- hasItems?: boolean;
58
- isOpenOnMount?: boolean;
59
- isSearchable?: boolean;
60
- keepOpenOnSubmit?: boolean;
61
- label?: string;
62
- /**
63
- * Only usable in conjunction with {isSearchable: true}.
64
- * Used as search input’s name.
65
- */
66
- name?: string;
67
- onClick?: (event: React.MouseEvent<HTMLElement>) => unknown;
68
- onClose?: () => unknown;
69
- onMouseDown?: (event: React.MouseEvent<HTMLElement>) => unknown;
70
- onMouseUp?: (event: React.MouseEvent<HTMLElement>) => unknown;
71
- onOpen?: () => unknown;
72
- onSubmitItem?: (payload: Item) => void;
73
- /**
74
- * Only usable in conjunction with {isSearchable: true}.
75
- * Used as search input’s placeholder.
76
- */
77
- placeholder?: string;
78
- style?: React.CSSProperties;
79
- /**
80
- * Only usable in conjunction with {isSearchable: true}.
81
- * Used as search input’s tabIndex.
82
- */
83
- tabIndex?: number;
84
- /**
85
- * Used as search input’s value if props.isSearchable === true
86
- * Used to determine if value has changed to avoid triggering onSubmitItem if not
87
- */
88
- value?: string;
89
- };
90
-
91
- type ChildrenTuple = [React.ReactNode, React.ReactNode];
92
-
93
- type MousePosition = { clientX: number; clientY: number };
94
-
95
- type TimeoutID = ReturnType<typeof setTimeout>;
96
-
97
- const { Children, Fragment, useCallback, useEffect, useMemo, useRef, useState } = React;
98
-
99
- const noop = () => {}; // eslint-disable-line @typescript-eslint/no-empty-function
100
-
101
- const CHILDREN_ERROR =
102
- '@acusti/dropdown requires either 1 child (the dropdown body) or 2 children: the dropdown trigger and the dropdown body.';
103
- const TEXT_INPUT_SELECTOR =
104
- 'input:not([type=radio]):not([type=checkbox]):not([type=range]),textarea';
105
-
106
- export default function Dropdown({
107
- allowCreate,
108
- allowEmpty = true,
109
- children,
110
- className,
111
- disabled,
112
- hasItems = true,
113
- isOpenOnMount,
114
- isSearchable,
115
- keepOpenOnSubmit = !hasItems,
116
- label,
117
- name,
118
- onClick,
119
- onClose,
120
- onMouseDown,
121
- onMouseUp,
122
- onOpen,
123
- onSubmitItem,
124
- placeholder,
125
- style: styleFromProps,
126
- tabIndex,
127
- value,
128
- }: Props) {
129
- const childrenCount = Children.count(children);
130
- if (childrenCount !== 1 && childrenCount !== 2) {
131
- if (childrenCount === 0) {
132
- throw new Error(CHILDREN_ERROR + ' Received no children.');
133
- }
134
- console.error(`${CHILDREN_ERROR} Received ${childrenCount} children.`);
135
- }
136
-
137
- let trigger: React.ReactNode;
138
- if (childrenCount > 1) {
139
- trigger = (children as ChildrenTuple)[0];
140
- }
141
- const isTriggerFromProps = React.isValidElement(trigger);
142
- const [isOpen, setIsOpen] = useState<boolean>(isOpenOnMount ?? false);
143
- const [isOpening, setIsOpening] = useState<boolean>(!isOpenOnMount);
144
- const [dropdownBodyElement, setDropdownBodyElement] = useState<HTMLDivElement | null>(
145
- null,
146
- );
147
-
148
- const dropdownElementRef = useRef<HTMLDivElement | null>(null);
149
- const inputElementRef = useRef<HTMLInputElement | null>(null);
150
- const closingTimerRef = useRef<null | TimeoutID>(null);
151
- const isOpeningTimerRef = useRef<null | TimeoutID>(null);
152
- const currentInputMethodRef = useRef<'keyboard' | 'mouse'>('mouse');
153
- const clearEnteredCharactersTimerRef = useRef<null | TimeoutID>(null);
154
- const enteredCharactersRef = useRef<string>('');
155
- const mouseDownPositionRef = useRef<MousePosition | null>(null);
156
- const outOfBounds = useIsOutOfBounds(dropdownBodyElement);
157
-
158
- const setDropdownOpenRef = useRef(() => setIsOpen(true));
159
- const allowCreateRef = useRef(allowCreate);
160
- const allowEmptyRef = useRef(allowEmpty);
161
- const hasItemsRef = useRef(hasItems);
162
- const isOpenRef = useRef(isOpen);
163
- const isOpeningRef = useRef(isOpening);
164
- const keepOpenOnSubmitRef = useRef(keepOpenOnSubmit);
165
- const onCloseRef = useRef(onClose);
166
- const onOpenRef = useRef(onOpen);
167
- const onSubmitItemRef = useRef(onSubmitItem);
168
- const valueRef = useRef(value);
169
-
170
- useEffect(() => {
171
- allowCreateRef.current = allowCreate;
172
- allowEmptyRef.current = allowEmpty;
173
- hasItemsRef.current = hasItems;
174
- isOpenRef.current = isOpen;
175
- isOpeningRef.current = isOpening;
176
- keepOpenOnSubmitRef.current = keepOpenOnSubmit;
177
- onCloseRef.current = onClose;
178
- onOpenRef.current = onOpen;
179
- onSubmitItemRef.current = onSubmitItem;
180
- valueRef.current = value;
181
- }, [
182
- allowCreate,
183
- allowEmpty,
184
- hasItems,
185
- isOpen,
186
- isOpening,
187
- keepOpenOnSubmit,
188
- onClose,
189
- onOpen,
190
- onSubmitItem,
191
- value,
192
- ]);
193
-
194
- const isMountedRef = useRef(false);
195
-
196
- useEffect(() => {
197
- if (!isMountedRef.current) {
198
- isMountedRef.current = true;
199
- // If isOpenOnMount, trigger onOpen right away
200
- if (isOpenRef.current && onOpenRef.current) {
201
- onOpenRef.current();
202
- }
203
- return;
204
- }
205
-
206
- if (isOpen && onOpenRef.current) {
207
- onOpenRef.current();
208
- } else if (!isOpen && onCloseRef.current) {
209
- onCloseRef.current();
210
- }
211
- }, [isOpen]);
212
-
213
- const closeDropdown = useCallback(() => {
214
- setIsOpen(false);
215
- setIsOpening(false);
216
- mouseDownPositionRef.current = null;
217
- if (closingTimerRef.current) {
218
- clearTimeout(closingTimerRef.current);
219
- closingTimerRef.current = null;
220
- }
221
- }, []);
222
-
223
- const handleSubmitItem = useCallback(
224
- (event: Event | React.SyntheticEvent<HTMLElement>) => {
225
- const eventTarget = event.target as HTMLElement;
226
- if (isOpenRef.current && !keepOpenOnSubmitRef.current) {
227
- const keepOpen = eventTarget.closest(
228
- '[data-ukt-keep-open]',
229
- ) as HTMLElement | null;
230
- // Don’t close dropdown if event occurs w/in data-ukt-keep-open element
231
- if (
232
- !keepOpen?.dataset.uktKeepOpen ||
233
- keepOpen.dataset.uktKeepOpen === 'false'
234
- ) {
235
- // A short timeout before closing is better UX when user selects an item so dropdown
236
- // doesn’t close before expected. It also enables using <Link />s in the dropdown body.
237
- closingTimerRef.current = setTimeout(closeDropdown, 90);
238
- }
239
- }
240
-
241
- if (!hasItemsRef.current) return;
242
-
243
- const element = getActiveItemElement(dropdownElementRef.current);
244
- if (!element && !allowCreateRef.current) {
245
- // If not allowEmpty, don’t allow submitting an empty item
246
- if (!allowEmptyRef.current) return;
247
- // If we have an input element as trigger & the user didn’t clear the text, do nothing
248
- if (inputElementRef.current?.value) return;
249
- }
250
-
251
- let itemLabel = element?.innerText ?? '';
252
- if (inputElementRef.current) {
253
- if (!element) {
254
- itemLabel = inputElementRef.current.value;
255
- } else {
256
- inputElementRef.current.value = itemLabel;
257
- }
258
-
259
- if (
260
- inputElementRef.current ===
261
- inputElementRef.current.ownerDocument.activeElement
262
- ) {
263
- inputElementRef.current.blur();
264
- }
265
- }
266
-
267
- const nextValue = element?.dataset.uktValue ?? itemLabel;
268
- // If parent is controlling Dropdown via props.value and nextValue is the same, do nothing
269
- if (valueRef.current && valueRef.current === nextValue) return;
270
-
271
- if (onSubmitItemRef.current) {
272
- onSubmitItemRef.current({
273
- element,
274
- event,
275
- label: itemLabel,
276
- value: nextValue,
277
- });
278
- }
279
- },
280
- [closeDropdown],
281
- );
282
-
283
- const handleMouseMove = useCallback(
284
- ({ clientX, clientY }: React.MouseEvent<HTMLElement>) => {
285
- currentInputMethodRef.current = 'mouse';
286
- const initialPosition = mouseDownPositionRef.current;
287
- if (!initialPosition) return;
288
- if (
289
- Math.abs(initialPosition.clientX - clientX) < 12 &&
290
- Math.abs(initialPosition.clientY - clientY) < 12
291
- ) {
292
- return;
293
- }
294
- setIsOpening(false);
295
- },
296
- [],
297
- );
298
-
299
- const handleMouseOver = useCallback((event: React.MouseEvent<HTMLElement>) => {
300
- if (!hasItemsRef.current) return;
301
-
302
- // If user isn’t currently using the mouse to navigate the dropdown, do nothing
303
- if (currentInputMethodRef.current !== 'mouse') return;
304
-
305
- // Ensure we have the dropdown root HTMLElement
306
- const dropdownElement = dropdownElementRef.current;
307
- if (!dropdownElement) return;
308
-
309
- const itemElements = getItemElements(dropdownElement);
310
- if (!itemElements) return;
311
-
312
- const eventTarget = event.target as HTMLElement;
313
- const item = eventTarget.closest(ITEM_SELECTOR) as HTMLElement | null;
314
- const element = item ?? eventTarget;
315
- for (const itemElement of itemElements) {
316
- if (itemElement === element) {
317
- setActiveItem({ dropdownElement, element });
318
- return;
319
- }
320
- }
321
- }, []);
322
-
323
- const handleMouseOut = useCallback((event: React.MouseEvent<HTMLElement>) => {
324
- if (!hasItemsRef.current) return;
325
- const activeItem = getActiveItemElement(dropdownElementRef.current);
326
- if (!activeItem) return;
327
- const eventRelatedTarget = event.relatedTarget as HTMLElement;
328
- if (activeItem !== event.target || activeItem.contains(eventRelatedTarget)) {
329
- return;
330
- }
331
- // If user moused out of activeItem (not into a descendant), it’s no longer active
332
- delete activeItem.dataset.uktActive;
333
- }, []);
334
-
335
- const handleMouseDown = useCallback(
336
- (event: React.MouseEvent<HTMLElement>) => {
337
- if (onMouseDown) onMouseDown(event);
338
- if (isOpenRef.current) return;
339
-
340
- setIsOpen(true);
341
- setIsOpening(true);
342
- mouseDownPositionRef.current = {
343
- clientX: event.clientX,
344
- clientY: event.clientY,
345
- };
346
- isOpeningTimerRef.current = setTimeout(() => {
347
- setIsOpening(false);
348
- isOpeningTimerRef.current = null;
349
- }, 1000);
350
- },
351
- [onMouseDown],
352
- );
353
-
354
- const handleMouseUp = useCallback(
355
- (event: React.MouseEvent<HTMLElement>) => {
356
- if (onMouseUp) onMouseUp(event);
357
- // If dropdown is still opening or isn’t open or is closing, do nothing
358
- if (isOpeningRef.current || !isOpenRef.current || closingTimerRef.current) {
359
- return;
360
- }
361
-
362
- const eventTarget = event.target as HTMLElement;
363
- // If click was outside dropdown body, don’t trigger submit
364
- if (!eventTarget.closest(BODY_SELECTOR)) {
365
- // Don’t close dropdown if isOpening or search input is focused
366
- if (
367
- !isOpeningRef.current &&
368
- inputElementRef.current !== eventTarget.ownerDocument.activeElement
369
- ) {
370
- closeDropdown();
371
- }
372
- return;
373
- }
374
-
375
- // If dropdown has no items and click was within dropdown body, do nothing
376
- if (!hasItemsRef.current) return;
377
-
378
- handleSubmitItem(event);
379
- },
380
- [closeDropdown, handleSubmitItem, onMouseUp],
381
- );
382
-
383
- const handleKeyDown = useCallback(
384
- (event: KeyboardEvent) => {
385
- const { altKey, ctrlKey, key, metaKey } = event;
386
- const eventTarget = event.target as HTMLElement;
387
- const dropdownElement = dropdownElementRef.current;
388
- if (!dropdownElement) return;
389
-
390
- const onEventHandled = () => {
391
- event.stopPropagation();
392
- event.preventDefault();
393
- currentInputMethodRef.current = 'keyboard';
394
- };
395
-
396
- const isEventTargetingDropdown = dropdownElement.contains(eventTarget);
397
-
398
- if (!isOpenRef.current) {
399
- // If dropdown is closed, don’t handle key events if event target isn’t within dropdown
400
- if (!isEventTargetingDropdown) return;
401
- // Open the dropdown on spacebar, enter, or if isSearchable and user hits the ↑/↓ arrows
402
- if (
403
- key === ' ' ||
404
- key === 'Enter' ||
405
- (hasItemsRef.current && (key === 'ArrowUp' || key === 'ArrowDown'))
406
- ) {
407
- onEventHandled();
408
- setIsOpen(true);
409
- }
410
- return;
411
- }
412
-
413
- const isTargetUsingKeyEvents = isEventTargetUsingKeyEvent(event);
414
-
415
- // If dropdown isOpen + hasItems & eventTargetNotUsingKeyEvents, handle characters
416
- if (hasItemsRef.current && !isTargetUsingKeyEvents) {
417
- let isEditingCharacters =
418
- !ctrlKey && !metaKey && /^[A-Za-z0-9]$/.test(key);
419
- // User could also be editing characters if there are already characters entered
420
- // and they are hitting delete or spacebar
421
- if (!isEditingCharacters && enteredCharactersRef.current) {
422
- isEditingCharacters = key === ' ' || key === 'Backspace';
423
- }
424
-
425
- if (isEditingCharacters) {
426
- onEventHandled();
427
- if (key === 'Backspace') {
428
- enteredCharactersRef.current = enteredCharactersRef.current.slice(
429
- 0,
430
- -1,
431
- );
432
- } else {
433
- enteredCharactersRef.current += key;
434
- }
435
-
436
- setActiveItem({
437
- dropdownElement,
438
- // If props.allowCreate, only override the input’s value with an
439
- // exact text match so user can enter a value not in items
440
- isExactMatch: allowCreateRef.current,
441
- text: enteredCharactersRef.current,
442
- });
443
-
444
- if (clearEnteredCharactersTimerRef.current) {
445
- clearTimeout(clearEnteredCharactersTimerRef.current);
446
- }
447
-
448
- clearEnteredCharactersTimerRef.current = setTimeout(() => {
449
- enteredCharactersRef.current = '';
450
- clearEnteredCharactersTimerRef.current = null;
451
- }, 1500);
452
-
453
- return;
454
- }
455
- }
456
-
457
- // If dropdown isOpen, handle submitting the value
458
- if (key === 'Enter' || (key === ' ' && !inputElementRef.current)) {
459
- onEventHandled();
460
- handleSubmitItem(event);
461
- return;
462
- }
463
-
464
- // If dropdown isOpen, handle closing it on escape or spacebar if !hasItems
465
- if (
466
- key === 'Escape' ||
467
- (isEventTargetingDropdown && key === ' ' && !hasItemsRef.current)
468
- ) {
469
- // Close dropdown if hasItems or event target not using key events
470
- if (hasItemsRef.current || !isTargetUsingKeyEvents) {
471
- closeDropdown();
472
- }
473
- return;
474
- }
475
-
476
- // Handle ↑/↓ arrows
477
- if (hasItemsRef.current) {
478
- if (key === 'ArrowUp') {
479
- onEventHandled();
480
- if (altKey || metaKey) {
481
- setActiveItem({ dropdownElement, index: 0 });
482
- } else {
483
- setActiveItem({ dropdownElement, indexAddend: -1 });
484
- }
485
- return;
486
- }
487
- if (key === 'ArrowDown') {
488
- onEventHandled();
489
- if (altKey || metaKey) {
490
- // Using a negative index counts back from the end
491
- setActiveItem({ dropdownElement, index: -1 });
492
- } else {
493
- setActiveItem({ dropdownElement, indexAddend: 1 });
494
- }
495
- return;
496
- }
497
- }
498
- },
499
- [closeDropdown, handleSubmitItem],
500
- );
501
-
502
- useKeyboardEvents({ ignoreUsedKeyboardEvents: false, onKeyDown: handleKeyDown });
503
-
504
- const cleanupEventListenersRef = useRef<() => void>(noop);
505
-
506
- const handleRef = useCallback(
507
- (ref: HTMLDivElement | null) => {
508
- dropdownElementRef.current = ref;
509
- if (!ref) {
510
- // If component was unmounted, cleanup handlers
511
- cleanupEventListenersRef.current();
512
- cleanupEventListenersRef.current = noop;
513
- return;
514
- }
515
-
516
- const { ownerDocument } = ref;
517
- let inputElement = inputElementRef.current;
518
- // Check if trigger from props is a textual input or textarea element
519
- if (isTriggerFromProps && !inputElement && ref.firstElementChild) {
520
- if (ref.firstElementChild.matches(TEXT_INPUT_SELECTOR)) {
521
- inputElement = ref.firstElementChild as HTMLInputElement;
522
- } else {
523
- inputElement =
524
- ref.firstElementChild.querySelector(TEXT_INPUT_SELECTOR);
525
- }
526
- inputElementRef.current = inputElement;
527
- }
528
-
529
- const handleGlobalMouseDown = ({ target }: MouseEvent) => {
530
- const eventTarget = target as HTMLElement;
531
- if (
532
- dropdownElementRef.current &&
533
- !dropdownElementRef.current.contains(eventTarget)
534
- ) {
535
- // Close dropdown on an outside click
536
- closeDropdown();
537
- }
538
- };
539
-
540
- const handleGlobalMouseUp = ({ target }: MouseEvent) => {
541
- if (!isOpenRef.current || closingTimerRef.current) return;
542
-
543
- // If still isOpening (gets set false 1s after open triggers), set it to false onMouseUp
544
- if (isOpeningRef.current) {
545
- setIsOpening(false);
546
- if (isOpeningTimerRef.current) {
547
- clearTimeout(isOpeningTimerRef.current);
548
- isOpeningTimerRef.current = null;
549
- }
550
- return;
551
- }
552
-
553
- const eventTarget = target as HTMLElement;
554
- // Only handle mouseup events from outside the dropdown here
555
- if (!dropdownElementRef.current?.contains(eventTarget)) {
556
- closeDropdown();
557
- }
558
- };
559
-
560
- // Close dropdown if any element is focused outside of this dropdown
561
- const handleGlobalFocusIn = ({ target }: Event) => {
562
- if (!isOpenRef.current) return;
563
-
564
- const eventTarget = target as HTMLElement;
565
- // If focused element is a descendant or a parent of the dropdown, do nothing
566
- if (
567
- !dropdownElementRef.current ||
568
- dropdownElementRef.current.contains(eventTarget) ||
569
- eventTarget.contains(dropdownElementRef.current)
570
- ) {
571
- return;
572
- }
573
-
574
- closeDropdown();
575
- };
576
-
577
- document.addEventListener('focusin', handleGlobalFocusIn);
578
- document.addEventListener('mousedown', handleGlobalMouseDown);
579
- document.addEventListener('mouseup', handleGlobalMouseUp);
580
-
581
- if (ownerDocument !== document) {
582
- ownerDocument.addEventListener('focusin', handleGlobalFocusIn);
583
- ownerDocument.addEventListener('mousedown', handleGlobalMouseDown);
584
- ownerDocument.addEventListener('mouseup', handleGlobalMouseUp);
585
- }
586
-
587
- // If dropdown should be open on mount, focus it
588
- if (isOpenOnMount) {
589
- ref.focus();
590
- }
591
-
592
- const handleInput = (event: Event) => {
593
- const dropdownElement = dropdownElementRef.current;
594
- if (!dropdownElement) return;
595
-
596
- if (!isOpenRef.current) setIsOpen(true);
597
-
598
- const input = event.target as HTMLInputElement;
599
- const isDeleting =
600
- enteredCharactersRef.current.length > input.value.length;
601
- enteredCharactersRef.current = input.value;
602
- // When deleting text, if there’s already an active item and
603
- // input isn’t empty, preserve the active item, else update it
604
- if (
605
- isDeleting &&
606
- input.value.length &&
607
- getActiveItemElement(dropdownElement)
608
- ) {
609
- return;
610
- }
611
-
612
- setActiveItem({
613
- dropdownElement,
614
- // If props.allowCreate, only override the input’s value with an
615
- // exact text match so user can enter a value not in items
616
- isExactMatch: allowCreateRef.current,
617
- text: enteredCharactersRef.current,
618
- });
619
- };
620
-
621
- if (inputElement) {
622
- inputElement.addEventListener('input', handleInput);
623
- }
624
-
625
- cleanupEventListenersRef.current = () => {
626
- document.removeEventListener('focusin', handleGlobalFocusIn);
627
- document.removeEventListener('mousedown', handleGlobalMouseDown);
628
- document.removeEventListener('mouseup', handleGlobalMouseUp);
629
-
630
- if (ownerDocument !== document) {
631
- ownerDocument.removeEventListener('focusin', handleGlobalFocusIn);
632
- ownerDocument.removeEventListener('mousedown', handleGlobalMouseDown);
633
- ownerDocument.removeEventListener('mouseup', handleGlobalMouseUp);
634
- }
635
-
636
- if (inputElement) {
637
- inputElement.removeEventListener('input', handleInput);
638
- }
639
- };
640
- },
641
- [closeDropdown, isOpenOnMount, isTriggerFromProps],
642
- );
643
-
644
- if (!isTriggerFromProps) {
645
- if (isSearchable) {
646
- trigger = (
647
- <input
648
- autoComplete="off"
649
- className={TRIGGER_CLASS_NAME}
650
- defaultValue={value ?? ''}
651
- disabled={disabled}
652
- name={name}
653
- onFocus={setDropdownOpenRef.current}
654
- placeholder={placeholder}
655
- ref={inputElementRef}
656
- tabIndex={tabIndex}
657
- type="text"
658
- />
659
- );
660
- } else {
661
- trigger = (
662
- <button className={TRIGGER_CLASS_NAME} tabIndex={0}>
663
- {trigger}
664
- </button>
665
- );
666
- }
667
- }
668
-
669
- if (label) {
670
- trigger = (
671
- <label className={LABEL_CLASS_NAME}>
672
- <div className={LABEL_TEXT_CLASS_NAME}>{label}</div>
673
- {trigger}
674
- </label>
675
- );
676
- }
677
-
678
- const style = useMemo<React.CSSProperties>(
679
- () => ({
680
- ...styleFromProps,
681
- ...(outOfBounds.maxHeight != null && outOfBounds.maxHeight > 0
682
- ? {
683
- [BODY_MAX_HEIGHT_VAR]: `calc(${outOfBounds.maxHeight}px - var(--uktdd-body-buffer))`,
684
- }
685
- : null),
686
- ...(outOfBounds.maxWidth != null && outOfBounds.maxWidth > 0
687
- ? {
688
- [BODY_MAX_WIDTH_VAR]: `calc(${outOfBounds.maxWidth}px - var(--uktdd-body-buffer))`,
689
- }
690
- : null),
691
- }),
692
- [outOfBounds.maxHeight, outOfBounds.maxWidth, styleFromProps],
693
- );
694
-
695
- return (
696
- <Fragment>
697
- <Style href="@acusti/dropdown/Dropdown">{STYLES}</Style>
698
- <div
699
- className={clsx(ROOT_CLASS_NAME, className, {
700
- disabled,
701
- 'is-open': isOpen,
702
- 'is-searchable': isSearchable,
703
- })}
704
- onClick={onClick}
705
- onMouseDown={handleMouseDown}
706
- onMouseMove={handleMouseMove}
707
- onMouseOut={handleMouseOut}
708
- onMouseOver={handleMouseOver}
709
- onMouseUp={handleMouseUp}
710
- ref={handleRef}
711
- style={style}
712
- >
713
- {trigger}
714
- {isOpen ? (
715
- <div
716
- className={clsx(BODY_CLASS_NAME, {
717
- 'calculating-position': !outOfBounds.hasLayout,
718
- 'has-items': hasItems,
719
- 'out-of-bounds-bottom':
720
- outOfBounds.bottom && !outOfBounds.top,
721
- 'out-of-bounds-left': outOfBounds.left && !outOfBounds.right,
722
- 'out-of-bounds-right': outOfBounds.right && !outOfBounds.left,
723
- 'out-of-bounds-top': outOfBounds.top && !outOfBounds.bottom,
724
- })}
725
- ref={setDropdownBodyElement}
726
- >
727
- {childrenCount > 1 ? (children as ChildrenTuple)[1] : children}
728
- </div>
729
- ) : null}
730
- </div>
731
- </Fragment>
732
- );
733
- }