|
1 | | -import * as React from 'react'; |
2 | 1 | import { cn } from '@/shared/lib'; |
| 2 | +import { useCallback, useEffect, useRef, useState } from 'react'; |
3 | 3 | import type { DropdownOption } from 'react-day-picker'; |
4 | 4 |
|
5 | 5 | export interface CustomDropdownProps { |
6 | 6 | value?: number; |
7 | 7 | onChange: (next: number) => void; |
8 | 8 | options: DropdownOption[]; |
9 | 9 | 'aria-label'?: string; |
| 10 | + className?: string; |
10 | 11 | } |
11 | 12 |
|
12 | 13 | export function CustomDropdown({ |
13 | 14 | value, |
14 | 15 | onChange, |
15 | 16 | options, |
16 | 17 | 'aria-label': ariaLabel, |
| 18 | + className, |
17 | 19 | }: CustomDropdownProps) { |
18 | | - const [open, setOpen] = React.useState(false); |
19 | | - const buttonRef = React.useRef<HTMLButtonElement>(null); |
20 | | - const listRef = React.useRef<HTMLUListElement>(null); |
| 20 | + const [open, setOpen] = useState(false); |
| 21 | + const buttonRef = useRef<HTMLButtonElement>(null); |
| 22 | + const listRef = useRef<HTMLUListElement>(null); |
21 | 23 |
|
22 | | - const selectedIndex = Math.max( |
23 | | - 0, |
24 | | - options.findIndex((o) => o.value === value), |
25 | | - ); |
26 | | - const [activeIndex, setActiveIndex] = React.useState( |
27 | | - selectedIndex === -1 ? 0 : selectedIndex, |
28 | | - ); |
| 24 | + const calcSelectedIndex = useCallback(() => { |
| 25 | + const i = options.findIndex((o) => o.value === value); |
| 26 | + return i >= 0 ? i : 0; |
| 27 | + }, [options, value]); |
29 | 28 |
|
30 | | - React.useEffect(() => { |
31 | | - if (selectedIndex >= 0) { |
32 | | - setActiveIndex(selectedIndex); |
33 | | - } else if (options.length > 0) { |
34 | | - setActiveIndex(0); |
35 | | - } else { |
36 | | - setActiveIndex(0); |
37 | | - } |
38 | | - }, [selectedIndex, options.length]); |
| 29 | + const [activeIndex, setActiveIndex] = useState<number>(calcSelectedIndex()); |
| 30 | + |
| 31 | + useEffect(() => { |
| 32 | + setActiveIndex(calcSelectedIndex()); |
| 33 | + }, [calcSelectedIndex]); |
39 | 34 |
|
40 | 35 | const label = |
41 | 36 | options.find((o) => o.value === value)?.label ?? options[0]?.label ?? ''; |
42 | 37 |
|
43 | | - React.useEffect(() => { |
| 38 | + useEffect(() => { |
44 | 39 | if (!open) return; |
45 | 40 |
|
46 | 41 | const handler = (e: Event) => { |
47 | | - const t = e.target as Node; |
| 42 | + const t = (e as Event).target as Node | null; |
| 43 | + if (!t) return; |
48 | 44 | if (!buttonRef.current?.contains(t) && !listRef.current?.contains(t)) { |
49 | 45 | setOpen(false); |
50 | 46 | } |
51 | 47 | }; |
52 | 48 |
|
53 | | - const hasPointer = |
| 49 | + const usePointer = |
54 | 50 | typeof window !== 'undefined' && 'PointerEvent' in window; |
55 | 51 |
|
56 | | - if (hasPointer) { |
57 | | - document.addEventListener('pointerdown', handler as EventListener, { |
58 | | - capture: true, |
59 | | - }); |
| 52 | + if (usePointer) { |
| 53 | + document.addEventListener('pointerdown', handler as EventListener, true); |
60 | 54 | return () => |
61 | 55 | document.removeEventListener( |
62 | 56 | 'pointerdown', |
63 | 57 | handler as EventListener, |
64 | | - { capture: true } as any, |
| 58 | + true, |
65 | 59 | ); |
66 | 60 | } else { |
67 | | - document.addEventListener('mousedown', handler as EventListener, { |
68 | | - capture: true, |
69 | | - }); |
70 | | - document.addEventListener('touchstart', handler as EventListener, { |
71 | | - capture: true, |
72 | | - }); |
| 61 | + document.addEventListener('mousedown', handler as EventListener, true); |
| 62 | + document.addEventListener('touchstart', handler as EventListener, true); |
73 | 63 | return () => { |
74 | 64 | document.removeEventListener( |
75 | 65 | 'mousedown', |
76 | 66 | handler as EventListener, |
77 | | - { capture: true } as any, |
| 67 | + true, |
78 | 68 | ); |
79 | 69 | document.removeEventListener( |
80 | 70 | 'touchstart', |
81 | 71 | handler as EventListener, |
82 | | - { capture: true } as any, |
| 72 | + true, |
83 | 73 | ); |
84 | 74 | }; |
85 | 75 | } |
@@ -122,7 +112,7 @@ export function CustomDropdown({ |
122 | 112 | }; |
123 | 113 |
|
124 | 114 | return ( |
125 | | - <div className='relative inline-block'> |
| 115 | + <div className={cn('relative inline-block', className)}> |
126 | 116 | <button |
127 | 117 | ref={buttonRef} |
128 | 118 | type='button' |
|
0 commit comments