Skip to content

Commit 386bb8d

Browse files
committed
feat(CDropdown): add the offset property; rebuild the component, and improve the syntax
1 parent c64daa4 commit 386bb8d

File tree

3 files changed

+171
-191
lines changed

3 files changed

+171
-191
lines changed

packages/coreui-react/src/components/dropdown/CDropdown.tsx

Lines changed: 124 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -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'
1211
import PropTypes from 'prop-types'
1312
import classNames from 'classnames'
14-
import { Manager } from 'react-popper'
1513

16-
import { useForkedRef } from '../../hooks'
14+
import { useForkedRef, usePopper } from '../../hooks'
1715
import { placementPropType } from '../../props'
1816
import type { Placements } from '../../types'
17+
import { isRTL } from '../../utils'
1918

2019
export 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-
101102
interface 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+
108141
export const CDropdownContext = createContext({} as ContextProps)
109142

110143
export 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

Comments
 (0)