@@ -3,19 +3,18 @@ import React, {
33 ElementType ,
44 forwardRef ,
55 HTMLAttributes ,
6- ReactNode ,
76 RefObject ,
87 useEffect ,
98 useRef ,
109 useState ,
1110} from 'react'
1211import PropTypes from 'prop-types'
1312import classNames from 'classnames'
14- import { Manager } from 'react-popper'
1513
16- import { useForkedRef } from '../../hooks'
14+ import { useForkedRef , usePopper } from '../../hooks'
1715import { placementPropType } from '../../props'
1816import type { Placements } from '../../types'
17+ import { isRTL } from '../../utils'
1918
2019export type Directions = 'start' | 'end'
2120
@@ -60,8 +59,14 @@ export interface CDropdownProps extends HTMLAttributes<HTMLDivElement | HTMLLIEl
6059 * Sets a specified direction and location of the dropdown menu.
6160 */
6261 direction ?: 'center' | 'dropup' | 'dropup-center' | 'dropend' | 'dropstart'
62+ /**
63+ * Offset of the dropdown menu relative to its target.
64+ */
65+ offset ?: [ number , number ]
6366 /**
6467 * Callback fired when the component requests to be hidden.
68+ *
69+ * @since 4.9.0-beta.1
6570 */
6671 onHide ?: ( ) => void
6772 /**
@@ -94,17 +99,45 @@ export interface CDropdownProps extends HTMLAttributes<HTMLDivElement | HTMLLIEl
9499 visible ?: boolean
95100}
96101
97- const PopperManagerWrapper = ( { children, popper } : { children : ReactNode ; popper : boolean } ) => {
98- return popper ? < Manager > { children } </ Manager > : < > { children } </ >
99- }
100-
101102interface ContextProps extends CDropdownProps {
102103 // 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 >
104106 setVisible : React . Dispatch < React . SetStateAction < boolean | undefined > >
105107 portal : boolean
106108}
107109
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+
108141export const CDropdownContext = createContext ( { } as ContextProps )
109142
110143export const CDropdown = forwardRef < HTMLDivElement | HTMLLIElement , CDropdownProps > (
@@ -116,6 +149,7 @@ export const CDropdown = forwardRef<HTMLDivElement | HTMLLIElement, CDropdownPro
116149 className,
117150 dark,
118151 direction,
152+ offset = [ 0 , 2 ] ,
119153 onHide,
120154 onShow,
121155 placement = 'bottom-start' ,
@@ -129,9 +163,12 @@ export const CDropdown = forwardRef<HTMLDivElement | HTMLLIElement, CDropdownPro
129163 ref ,
130164 ) => {
131165 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 )
133169 const forkedRef = useForkedRef ( ref , dropdownRef )
134170 const [ _visible , setVisible ] = useState ( visible )
171+ const { initPopper, destroyPopper } = usePopper ( )
135172
136173 const Component = variant === 'nav-item' ? 'li' : component
137174
@@ -142,52 +179,100 @@ export const CDropdown = forwardRef<HTMLDivElement | HTMLLIElement, CDropdownPro
142179
143180 const contextValues = {
144181 alignment,
145- autoClose,
146182 dark,
147- direction : direction ,
148183 dropdownToggleRef,
149- placement : placement ,
184+ dropdownMenuRef ,
150185 popper,
151- portal : portal ,
186+ portal,
152187 variant,
153188 visible : _visible ,
154189 setVisible,
155190 }
156191
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+
157204 useEffect ( ( ) => {
158205 setVisible ( visible )
159206 } , [ visible ] )
160207
161208 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+ }
164222 } , [ _visible ] )
165223
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+
166253 return (
167254 < 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+ ) }
191276 </ CDropdownContext . Provider >
192277 )
193278 } ,
@@ -214,6 +299,7 @@ CDropdown.propTypes = {
214299 component : PropTypes . elementType ,
215300 dark : PropTypes . bool ,
216301 direction : PropTypes . oneOf ( [ 'center' , 'dropup' , 'dropup-center' , 'dropend' , 'dropstart' ] ) ,
302+ offset : PropTypes . any , // TODO: find good proptype
217303 onHide : PropTypes . func ,
218304 onShow : PropTypes . func ,
219305 placement : placementPropType ,
0 commit comments