@@ -3,19 +3,18 @@ import React, {
3
3
ElementType ,
4
4
forwardRef ,
5
5
HTMLAttributes ,
6
- ReactNode ,
7
6
RefObject ,
8
7
useEffect ,
9
8
useRef ,
10
9
useState ,
11
10
} from 'react'
12
11
import PropTypes from 'prop-types'
13
12
import classNames from 'classnames'
14
- import { Manager } from 'react-popper'
15
13
16
- import { useForkedRef } from '../../hooks'
14
+ import { useForkedRef , usePopper } from '../../hooks'
17
15
import { placementPropType } from '../../props'
18
16
import type { Placements } from '../../types'
17
+ import { isRTL } from '../../utils'
19
18
20
19
export type Directions = 'start' | 'end'
21
20
@@ -60,8 +59,14 @@ export interface CDropdownProps extends HTMLAttributes<HTMLDivElement | HTMLLIEl
60
59
* Sets a specified direction and location of the dropdown menu.
61
60
*/
62
61
direction ?: 'center' | 'dropup' | 'dropup-center' | 'dropend' | 'dropstart'
62
+ /**
63
+ * Offset of the dropdown menu relative to its target.
64
+ */
65
+ offset ?: [ number , number ]
63
66
/**
64
67
* Callback fired when the component requests to be hidden.
68
+ *
69
+ * @since 4.9.0-beta.1
65
70
*/
66
71
onHide ?: ( ) => void
67
72
/**
@@ -94,17 +99,45 @@ export interface CDropdownProps extends HTMLAttributes<HTMLDivElement | HTMLLIEl
94
99
visible ?: boolean
95
100
}
96
101
97
- const PopperManagerWrapper = ( { children, popper } : { children : ReactNode ; popper : boolean } ) => {
98
- return popper ? < Manager > { children } </ Manager > : < > { children } </ >
99
- }
100
-
101
102
interface ContextProps extends CDropdownProps {
102
103
// eslint-disable-next-line @typescript-eslint/no-explicit-any
103
- dropdownToggleRef : RefObject < any > | undefined
104
+ dropdownToggleRef : RefObject < any | undefined >
105
+ dropdownMenuRef : RefObject < HTMLDivElement | HTMLUListElement | undefined >
104
106
setVisible : React . Dispatch < React . SetStateAction < boolean | undefined > >
105
107
portal : boolean
106
108
}
107
109
110
+ const getPlacement = (
111
+ placement : Placements ,
112
+ direction : CDropdownProps [ 'direction' ] ,
113
+ alignment : CDropdownProps [ 'alignment' ] ,
114
+ isRTL : boolean ,
115
+ ) : Placements => {
116
+ let _placement = placement
117
+
118
+ if ( direction === 'dropup' ) {
119
+ _placement = isRTL ? 'top-end' : 'top-start'
120
+ }
121
+
122
+ if ( direction === 'dropup-center' ) {
123
+ _placement = 'top'
124
+ }
125
+
126
+ if ( direction === 'dropend' ) {
127
+ _placement = isRTL ? 'left-start' : 'right-start'
128
+ }
129
+
130
+ if ( direction === 'dropstart' ) {
131
+ _placement = isRTL ? 'right-start' : 'left-start'
132
+ }
133
+
134
+ if ( alignment === 'end' ) {
135
+ _placement = isRTL ? 'bottom-start' : 'bottom-end'
136
+ }
137
+
138
+ return _placement
139
+ }
140
+
108
141
export const CDropdownContext = createContext ( { } as ContextProps )
109
142
110
143
export const CDropdown = forwardRef < HTMLDivElement | HTMLLIElement , CDropdownProps > (
@@ -116,6 +149,7 @@ export const CDropdown = forwardRef<HTMLDivElement | HTMLLIElement, CDropdownPro
116
149
className,
117
150
dark,
118
151
direction,
152
+ offset = [ 0 , 2 ] ,
119
153
onHide,
120
154
onShow,
121
155
placement = 'bottom-start' ,
@@ -129,9 +163,12 @@ export const CDropdown = forwardRef<HTMLDivElement | HTMLLIElement, CDropdownPro
129
163
ref ,
130
164
) => {
131
165
const dropdownRef = useRef < HTMLDivElement > ( null )
132
- const dropdownToggleRef = useRef ( null )
166
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
167
+ const dropdownToggleRef = useRef < any > ( null )
168
+ const dropdownMenuRef = useRef < HTMLDivElement | HTMLUListElement > ( null )
133
169
const forkedRef = useForkedRef ( ref , dropdownRef )
134
170
const [ _visible , setVisible ] = useState ( visible )
171
+ const { initPopper, destroyPopper } = usePopper ( )
135
172
136
173
const Component = variant === 'nav-item' ? 'li' : component
137
174
@@ -142,52 +179,100 @@ export const CDropdown = forwardRef<HTMLDivElement | HTMLLIElement, CDropdownPro
142
179
143
180
const contextValues = {
144
181
alignment,
145
- autoClose,
146
182
dark,
147
- direction : direction ,
148
183
dropdownToggleRef,
149
- placement : placement ,
184
+ dropdownMenuRef ,
150
185
popper,
151
- portal : portal ,
186
+ portal,
152
187
variant,
153
188
visible : _visible ,
154
189
setVisible,
155
190
}
156
191
192
+ const popperConfig = {
193
+ modifiers : [
194
+ {
195
+ name : 'offset' ,
196
+ options : {
197
+ offset : offset ,
198
+ } ,
199
+ } ,
200
+ ] ,
201
+ placement : getPlacement ( placement , direction , alignment , isRTL ( dropdownMenuRef . current ) ) ,
202
+ }
203
+
157
204
useEffect ( ( ) => {
158
205
setVisible ( visible )
159
206
} , [ visible ] )
160
207
161
208
useEffect ( ( ) => {
162
- _visible && onShow && onShow ( )
163
- ! _visible && onHide && onHide ( )
209
+ if ( _visible && dropdownToggleRef . current && dropdownMenuRef . current ) {
210
+ popper && initPopper ( dropdownToggleRef . current , dropdownMenuRef . current , popperConfig )
211
+ window . addEventListener ( 'mouseup' , handleMouseUp )
212
+ window . addEventListener ( 'keyup' , handleKeyup )
213
+ onShow && onShow ( )
214
+ }
215
+
216
+ return ( ) => {
217
+ popper && destroyPopper ( )
218
+ window . removeEventListener ( 'mouseup' , handleMouseUp )
219
+ window . removeEventListener ( 'keyup' , handleKeyup )
220
+ onHide && onHide ( )
221
+ }
164
222
} , [ _visible ] )
165
223
224
+ const handleKeyup = ( event : KeyboardEvent ) => {
225
+ if ( autoClose === false ) {
226
+ return
227
+ }
228
+
229
+ if ( event . key === 'Escape' ) {
230
+ setVisible ( false )
231
+ }
232
+ }
233
+
234
+ const handleMouseUp = ( event : Event ) => {
235
+ if ( ! dropdownToggleRef . current || ! dropdownMenuRef . current ) {
236
+ return
237
+ }
238
+
239
+ if ( dropdownToggleRef . current . contains ( event . target as HTMLElement ) ) {
240
+ return
241
+ }
242
+
243
+ if (
244
+ autoClose === true ||
245
+ ( autoClose === 'inside' && dropdownMenuRef . current . contains ( event . target as HTMLElement ) ) ||
246
+ ( autoClose === 'outside' && ! dropdownMenuRef . current . contains ( event . target as HTMLElement ) )
247
+ ) {
248
+ setTimeout ( ( ) => setVisible ( false ) , 1 )
249
+ return
250
+ }
251
+ }
252
+
166
253
return (
167
254
< CDropdownContext . Provider value = { contextValues } >
168
- < PopperManagerWrapper popper = { popper } >
169
- { variant === 'input-group' ? (
170
- < > { children } </ >
171
- ) : (
172
- < Component
173
- className = { classNames (
174
- variant === 'nav-item' ? 'nav-item dropdown' : variant ,
175
- {
176
- 'dropdown-center' : direction === 'center' ,
177
- 'dropup dropup-center' : direction === 'dropup-center' ,
178
- [ `${ direction } ` ] :
179
- direction && direction !== 'center' && direction !== 'dropup-center' ,
180
- show : _visible ,
181
- } ,
182
- className ,
183
- ) }
184
- { ...rest }
185
- ref = { forkedRef }
186
- >
187
- { children }
188
- </ Component >
189
- ) }
190
- </ PopperManagerWrapper >
255
+ { variant === 'input-group' ? (
256
+ < > { children } </ >
257
+ ) : (
258
+ < Component
259
+ className = { classNames (
260
+ variant === 'nav-item' ? 'nav-item dropdown' : variant ,
261
+ {
262
+ 'dropdown-center' : direction === 'center' ,
263
+ 'dropup dropup-center' : direction === 'dropup-center' ,
264
+ [ `${ direction } ` ] :
265
+ direction && direction !== 'center' && direction !== 'dropup-center' ,
266
+ show : _visible ,
267
+ } ,
268
+ className ,
269
+ ) }
270
+ { ...rest }
271
+ ref = { forkedRef }
272
+ >
273
+ { children }
274
+ </ Component >
275
+ ) }
191
276
</ CDropdownContext . Provider >
192
277
)
193
278
} ,
@@ -214,6 +299,7 @@ CDropdown.propTypes = {
214
299
component : PropTypes . elementType ,
215
300
dark : PropTypes . bool ,
216
301
direction : PropTypes . oneOf ( [ 'center' , 'dropup' , 'dropup-center' , 'dropend' , 'dropstart' ] ) ,
302
+ offset : PropTypes . any , // TODO: find good proptype
217
303
onHide : PropTypes . func ,
218
304
onShow : PropTypes . func ,
219
305
placement : placementPropType ,
0 commit comments