4
4
* MIT License
5
5
*/
6
6
7
+ /**
8
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
9
+ *
10
+ * This source code is licensed under the MIT license found in the
11
+ * LICENSE file in the root directory of this source tree.
12
+ *
13
+ */
14
+
15
+ import type { JSX } from 'react' ;
16
+
17
+ import { isDOMNode } from 'lexical' ;
18
+ import * as React from 'react' ;
7
19
import {
8
20
ReactNode ,
9
21
useCallback ,
@@ -12,15 +24,16 @@ import {
12
24
useRef ,
13
25
useState ,
14
26
} from 'react' ;
15
- import * as React from 'react' ;
16
27
import { createPortal } from 'react-dom' ;
17
28
18
29
type DropDownContextType = {
19
- registerItem : ( ref : React . RefObject < HTMLButtonElement > ) => void ;
30
+ registerItem : ( ref : React . RefObject < null | HTMLButtonElement > ) => void ;
20
31
} ;
21
32
22
33
const DropDownContext = React . createContext < DropDownContextType | null > ( null ) ;
23
34
35
+ const dropDownPadding = 4 ;
36
+
24
37
export function DropDownItem ( {
25
38
children,
26
39
className,
@@ -32,7 +45,7 @@ export function DropDownItem({
32
45
onClick : ( event : React . MouseEvent < HTMLButtonElement > ) => void ;
33
46
title ?: string ;
34
47
} ) {
35
- const ref = useRef < HTMLButtonElement > ( null ) ;
48
+ const ref = useRef < null | HTMLButtonElement > ( null ) ;
36
49
37
50
const dropDownContext = React . useContext ( DropDownContext ) ;
38
51
@@ -49,13 +62,18 @@ export function DropDownItem({
49
62
} , [ ref , registerItem ] ) ;
50
63
51
64
return (
52
- < button className = { className } onClick = { onClick } ref = { ref } title = { title } >
65
+ < button
66
+ className = { className }
67
+ onClick = { onClick }
68
+ ref = { ref }
69
+ title = { title }
70
+ type = "button" >
53
71
{ children }
54
72
</ button >
55
73
) ;
56
74
}
57
75
58
- function DropDownItems ( {
76
+ export function DropDownItems ( {
59
77
children,
60
78
dropDownRef,
61
79
onClose,
@@ -64,51 +82,66 @@ function DropDownItems({
64
82
dropDownRef : React . Ref < HTMLDivElement > ;
65
83
onClose : ( ) => void ;
66
84
} ) {
67
- const [ items , setItems ] = useState < React . RefObject < HTMLButtonElement > [ ] > ( ) ;
85
+ const [ items , setItems ] =
86
+ useState < React . RefObject < null | HTMLButtonElement > [ ] > ( ) ;
68
87
const [ highlightedItem , setHighlightedItem ] =
69
- useState < React . RefObject < HTMLButtonElement > > ( ) ;
88
+ useState < React . RefObject < null | HTMLButtonElement > > ( ) ;
89
+
70
90
const registerItem = useCallback (
71
- ( itemRef : React . RefObject < HTMLButtonElement > ) => {
72
- setItems ( prev => ( prev ? [ ...prev , itemRef ] : [ itemRef ] ) ) ;
91
+ ( itemRef : React . RefObject < null | HTMLButtonElement > ) => {
92
+ setItems ( ( prev ) => ( prev ? [ ...prev , itemRef ] : [ itemRef ] ) ) ;
73
93
} ,
74
94
[ setItems ] ,
75
95
) ;
96
+
76
97
const handleKeyDown = ( event : React . KeyboardEvent < HTMLDivElement > ) => {
77
- if ( ! items ) return ;
98
+ if ( ! items ) {
99
+ return ;
100
+ }
101
+
78
102
const key = event . key ;
103
+
79
104
if ( [ 'Escape' , 'ArrowUp' , 'ArrowDown' , 'Tab' ] . includes ( key ) ) {
80
105
event . preventDefault ( ) ;
81
106
}
82
107
83
108
if ( key === 'Escape' || key === 'Tab' ) {
84
109
onClose ( ) ;
85
110
} else if ( key === 'ArrowUp' ) {
86
- setHighlightedItem ( prev => {
87
- if ( ! prev ) return items [ 0 ] ;
111
+ setHighlightedItem ( ( prev ) => {
112
+ if ( ! prev ) {
113
+ return items [ 0 ] ;
114
+ }
88
115
const index = items . indexOf ( prev ) - 1 ;
89
116
return items [ index === - 1 ? items . length - 1 : index ] ;
90
117
} ) ;
91
118
} else if ( key === 'ArrowDown' ) {
92
- setHighlightedItem ( prev => {
93
- if ( ! prev ) return items [ 0 ] ;
119
+ setHighlightedItem ( ( prev ) => {
120
+ if ( ! prev ) {
121
+ return items [ 0 ] ;
122
+ }
94
123
return items [ items . indexOf ( prev ) + 1 ] ;
95
124
} ) ;
96
125
}
97
126
} ;
127
+
98
128
const contextValue = useMemo (
99
129
( ) => ( {
100
130
registerItem,
101
131
} ) ,
102
132
[ registerItem ] ,
103
133
) ;
134
+
104
135
useEffect ( ( ) => {
105
136
if ( items && ! highlightedItem ) {
106
137
setHighlightedItem ( items [ 0 ] ) ;
107
138
}
139
+
108
140
if ( highlightedItem && highlightedItem . current ) {
109
141
highlightedItem . current . focus ( ) ;
110
142
}
111
143
} , [ items , highlightedItem ] ) ;
144
+
112
145
return (
113
146
< DropDownContext . Provider value = { contextValue } >
114
147
< div className = "dropdown" ref = { dropDownRef } onKeyDown = { handleKeyDown } >
@@ -118,21 +151,23 @@ function DropDownItems({
118
151
) ;
119
152
}
120
153
121
- export const DropDown = ( {
154
+ export function DropDown ( {
155
+ disabled = false ,
122
156
buttonLabel,
123
157
buttonAriaLabel,
124
158
buttonClassName,
125
159
buttonIconClassName,
126
160
children,
127
161
stopCloseOnClickSelf,
128
162
} : {
163
+ disabled ?: boolean ;
129
164
buttonAriaLabel ?: string ;
130
165
buttonClassName : string ;
131
166
buttonIconClassName ?: string ;
132
167
buttonLabel ?: string ;
133
168
children : ReactNode ;
134
169
stopCloseOnClickSelf ?: boolean ;
135
- } ) : JSX . Element => {
170
+ } ) : JSX . Element {
136
171
const dropDownRef = useRef < HTMLDivElement > ( null ) ;
137
172
const buttonRef = useRef < HTMLButtonElement > ( null ) ;
138
173
const [ showDropDown , setShowDropDown ] = useState ( false ) ;
@@ -149,8 +184,8 @@ export const DropDown = ({
149
184
const dropDown = dropDownRef . current ;
150
185
151
186
if ( showDropDown && button !== null && dropDown !== null ) {
152
- const { top, left } = button . getBoundingClientRect ( ) ;
153
- dropDown . style . top = `${ top + 40 } px` ;
187
+ const { top, left} = button . getBoundingClientRect ( ) ;
188
+ dropDown . style . top = `${ top + button . offsetHeight + dropDownPadding } px` ;
154
189
dropDown . style . left = `${ Math . min (
155
190
left ,
156
191
window . innerWidth - dropDown . offsetWidth - 20 ,
@@ -164,14 +199,15 @@ export const DropDown = ({
164
199
if ( button !== null && showDropDown ) {
165
200
const handle = ( event : MouseEvent ) => {
166
201
const target = event . target ;
202
+ if ( ! isDOMNode ( target ) ) {
203
+ return ;
204
+ }
167
205
if ( stopCloseOnClickSelf ) {
168
- if (
169
- dropDownRef . current &&
170
- dropDownRef . current . contains ( target as Node )
171
- )
206
+ if ( dropDownRef . current && dropDownRef . current . contains ( target ) ) {
172
207
return ;
208
+ }
173
209
}
174
- if ( ! button . contains ( target as Node ) ) {
210
+ if ( ! button . contains ( target ) ) {
175
211
setShowDropDown ( false ) ;
176
212
}
177
213
} ;
@@ -183,14 +219,37 @@ export const DropDown = ({
183
219
}
184
220
} , [ dropDownRef , buttonRef , showDropDown , stopCloseOnClickSelf ] ) ;
185
221
222
+ useEffect ( ( ) => {
223
+ const handleButtonPositionUpdate = ( ) => {
224
+ if ( showDropDown ) {
225
+ const button = buttonRef . current ;
226
+ const dropDown = dropDownRef . current ;
227
+ if ( button !== null && dropDown !== null ) {
228
+ const { top} = button . getBoundingClientRect ( ) ;
229
+ const newPosition = top + button . offsetHeight + dropDownPadding ;
230
+ if ( newPosition !== dropDown . getBoundingClientRect ( ) . top ) {
231
+ dropDown . style . top = `${ newPosition } px` ;
232
+ }
233
+ }
234
+ }
235
+ } ;
236
+
237
+ document . addEventListener ( 'scroll' , handleButtonPositionUpdate ) ;
238
+
239
+ return ( ) => {
240
+ document . removeEventListener ( 'scroll' , handleButtonPositionUpdate ) ;
241
+ } ;
242
+ } , [ buttonRef , dropDownRef , showDropDown ] ) ;
243
+
186
244
return (
187
245
< >
188
246
< button
247
+ type = "button"
248
+ disabled = { disabled }
189
249
aria-label = { buttonAriaLabel || buttonLabel }
190
250
className = { buttonClassName }
191
251
onClick = { ( ) => setShowDropDown ( ! showDropDown ) }
192
- ref = { buttonRef }
193
- >
252
+ ref = { buttonRef } >
194
253
{ buttonIconClassName && < span className = { buttonIconClassName } /> }
195
254
{ buttonLabel && (
196
255
< span className = "text dropdown-button-text" > { buttonLabel } </ span >
@@ -207,6 +266,6 @@ export const DropDown = ({
207
266
) }
208
267
</ >
209
268
) ;
210
- } ;
269
+ }
211
270
212
271
export default DropDown ;
0 commit comments