1
1
import React from 'react'
2
- import styled , { css } from 'styled-components'
2
+ import styled , { css , keyframes } from 'styled-components'
3
+
4
+ const fadeIn = keyframes `
5
+ from {
6
+ opacity : 0 ;
7
+ transform : translateY (-10px );
8
+ }
9
+ to {
10
+ opacity : 1 ;
11
+ transform : translateY (0 );
12
+ }
13
+ `
14
+
15
+ const itemEntrance = keyframes `
16
+ from {
17
+ opacity : 0 ;
18
+ transform : translateX (-10px );
19
+ }
20
+ to {
21
+ opacity : 1 ;
22
+ transform : translateX (0 );
23
+ }
24
+ `
3
25
4
26
const Wrapper = styled . div < { $visible : boolean , $anchor : 'top' | 'bottom' } > `
5
27
opacity: 0;
6
- transition: transform .2s ease, opacity .2s ;
28
+ transition: transform .3s cubic-bezier(0.34, 1.56, 0.64, 1), opacity .3s ease ;
7
29
position: absolute;
8
30
visibility: hidden;
9
31
z-index: 1000;
@@ -12,33 +34,59 @@ const Wrapper = styled.div<{$visible: boolean, $anchor: 'top' | 'bottom'}>`
12
34
min-width: 100%;
13
35
white-space: nowrap;
14
36
min-width: max-content;
37
+ filter: drop-shadow(0 10px 20px rgba(0, 0, 0, 0.2));
38
+
15
39
${ ( props ) => props . $visible && css `
16
40
opacity : 1 ;
17
41
transform : translateY (0px )!important ;
18
42
visibility : visible;
43
+ animation : ${ fadeIn } 0.3s cubic-bezier (0.34 , 1.56 , 0.64 , 1 );
19
44
` }
45
+
20
46
${ ( props ) => props . $anchor === 'top' && css `
21
47
top : 100% ;
22
48
margin-top : 10px ;
23
49
transform : translateY (-10px );
24
50
` }
51
+
25
52
${ ( props ) => props . $anchor === 'bottom' && css `
26
53
bottom : 100% ;
27
54
margin-bottom : 10px ;
28
55
transform : translateY (10px );
29
56
` }
57
+
30
58
& > div {
31
59
display: grid;
32
- background: #15151f;
33
- border-radius: 10px;
60
+ background: rgba(21, 21, 31, 0.95);
61
+ backdrop-filter: blur(10px);
62
+ border-radius: 12px;
34
63
overflow: hidden;
35
- padding: 5px;
36
- gap: 5px;
64
+ padding: 8px;
65
+ gap: 6px;
66
+ border: 1px solid rgba(255, 255, 255, 0.1);
67
+
68
+ & > * {
69
+ opacity: 0;
70
+ animation: ${ itemEntrance } 0.3s forwards;
71
+
72
+ ${ props => props . $visible && Array . from ( { length : 10 } ) . map ( ( _ , i ) => css `
73
+ & : nth-child (${ i + 1 } ) {
74
+ animation-delay : ${ 0.05 * ( i + 1 ) } s;
75
+ }
76
+ ` ) }
77
+ }
37
78
}
38
79
`
39
80
40
81
export function Dropdown ( { visible, children, anchor : _anchor } : React . PropsWithChildren < { visible : boolean , anchor ?: 'bottom' | 'top' } > ) {
41
82
const ref = React . useRef < HTMLDivElement > ( null ! )
83
+ const [ mounted , setMounted ] = React . useState ( false )
84
+
85
+ React . useEffect ( ( ) => {
86
+ if ( visible && ! mounted ) {
87
+ setMounted ( true )
88
+ }
89
+ } , [ visible ] )
42
90
43
91
const anchor = React . useMemo (
44
92
( ) => {
@@ -48,6 +96,24 @@ export function Dropdown({ visible, children, anchor: _anchor }: React.PropsWith
48
96
}
49
97
, [ children , visible , _anchor ] ,
50
98
)
99
+
100
+ // Close dropdown when clicking outside
101
+ React . useEffect ( ( ) => {
102
+ if ( ! visible ) return
103
+
104
+ const handleClickOutside = ( event : MouseEvent ) => {
105
+ if ( ref . current && ! ref . current . contains ( event . target as Node ) ) {
106
+ // We can't directly modify the visible prop, but we can dispatch a custom event
107
+ // that the parent component can listen for
108
+ ref . current . dispatchEvent ( new CustomEvent ( 'dropdown-close' , { bubbles : true } ) )
109
+ }
110
+ }
111
+
112
+ document . addEventListener ( 'mousedown' , handleClickOutside )
113
+ return ( ) => {
114
+ document . removeEventListener ( 'mousedown' , handleClickOutside )
115
+ }
116
+ } , [ visible ] )
51
117
52
118
return (
53
119
< Wrapper
@@ -56,7 +122,7 @@ export function Dropdown({ visible, children, anchor: _anchor }: React.PropsWith
56
122
$visible = { visible }
57
123
>
58
124
< div >
59
- { children }
125
+ { mounted && children }
60
126
</ div >
61
127
</ Wrapper >
62
128
)
0 commit comments