Skip to content

Commit 7c91c99

Browse files
committed
feat(CDropdown, CPopover, CTooltip): allow passing custom popper configuration
1 parent cb1ef1f commit 7c91c99

File tree

7 files changed

+506
-158
lines changed

7 files changed

+506
-158
lines changed

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

+106-26
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import React, {
1010
} from 'react'
1111
import PropTypes from 'prop-types'
1212
import classNames from 'classnames'
13+
import type { Options } from '@popperjs/core'
1314

1415
import { PolymorphicRefForwardingComponent } from '../../helpers'
1516
import { useForkedRef, usePopper } from '../../hooks'
@@ -22,77 +23,148 @@ import { getPlacement } from './utils'
2223

2324
export interface CDropdownProps extends HTMLAttributes<HTMLDivElement | HTMLLIElement> {
2425
/**
25-
* Set aligment of dropdown menu.
26+
* Specifies the alignment of the React Dropdown Menu within this React Dropdown.
27+
*
28+
* @example
29+
* // Align dropdown menu to the end on large devices, otherwise start
30+
* <CDropdown alignment={{ lg: 'end', xs: 'start' }}>
31+
* <CDropdownToggle>Toggle dropdown</CDropdownToggle>
32+
* <CDropdownMenu>
33+
* <CDropdownItem>Action</CDropdownItem>
34+
* <CDropdownItem>Another Action</CDropdownItem>
35+
* </CDropdownMenu>
36+
* </CDropdown>
2637
*
2738
* @type 'start' | 'end' | { xs: 'start' | 'end' } | { sm: 'start' | 'end' } | { md: 'start' | 'end' } | { lg: 'start' | 'end' } | { xl: 'start' | 'end'} | { xxl: 'start' | 'end'}
2839
*/
2940
alignment?: Alignments
41+
3042
/**
31-
* Component used for the root node. Either a string to use a HTML element or a component.
43+
* Determines the root node component (native HTML element or a custom React component) for the React Dropdown.
3244
*/
3345
as?: ElementType
46+
3447
/**
35-
* Configure the auto close behavior of the dropdown:
36-
* - `true` - the dropdown will be closed by clicking outside or inside the dropdown menu.
37-
* - `false` - the dropdown will be closed by clicking the toggle button and manually calling hide or toggle method. (Also will not be closed by pressing esc key)
38-
* - `'inside'` - the dropdown will be closed (only) by clicking inside the dropdown menu.
39-
* - `'outside'` - the dropdown will be closed (only) by clicking outside the dropdown menu.
48+
* Configures automatic closing behavior for the React Dropdown:
49+
* - `true` - Close on clicks inside or outside of the React Dropdown Menu.
50+
* - `false` - Disable auto-close; manually call `hide` or `toggle` (also not closed by `Escape`).
51+
* - `'inside'` - Close only when clicking inside the React Dropdown Menu.
52+
* - `'outside'` - Close only when clicking outside the React Dropdown Menu.
53+
*
54+
* @example
55+
* // Close only when user clicks outside of the menu
56+
* <CDropdown autoClose="outside" />
4057
*/
4158
autoClose?: 'inside' | 'outside' | boolean
59+
4260
/**
43-
* A string of all className you want applied to the base component.
61+
* Adds custom classes to the React Dropdown root element.
4462
*/
4563
className?: string
64+
4665
/**
47-
* Appends the react dropdown menu to a specific element. You can pass an HTML element or function that returns a single element. By default `document.body`.
66+
* Appends the React Dropdown Menu to a specific element. You can pass an HTML element or a function returning an element. Defaults to `document.body`.
67+
*
68+
* @example
69+
* // Append the menu to a custom container
70+
* const myContainer = document.getElementById('my-container')
71+
*
72+
* <CDropdown container={myContainer} />
4873
*
4974
* @since 4.11.0
5075
*/
5176
container?: DocumentFragment | Element | (() => DocumentFragment | Element | null) | null
77+
5278
/**
53-
* Sets a darker color scheme to match a dark navbar.
79+
* Applies a darker color scheme to the React Dropdown Menu, often used within dark navbars.
5480
*/
5581
dark?: boolean
82+
5683
/**
57-
* Sets a specified direction and location of the dropdown menu.
84+
* Specifies the direction of the React Dropdown.
5885
*/
5986
direction?: 'center' | 'dropup' | 'dropup-center' | 'dropend' | 'dropstart'
87+
6088
/**
61-
* Offset of the dropdown menu relative to its target.
89+
* Defines x and y offsets ([x, y]) for the React Dropdown Menu relative to its target.
90+
*
91+
* @example
92+
* // Offset the menu 10px in X and 5px in Y direction
93+
* <CDropdown offset={[10, 5]}>
94+
* ...
95+
* </CDropdown>
6296
*/
6397
offset?: [number, number]
98+
6499
/**
65-
* Callback fired when the component requests to be hidden.
100+
* Callback fired right before the React Dropdown becomes hidden.
66101
*
67102
* @since 4.9.0
68103
*/
69104
onHide?: () => void
105+
70106
/**
71-
* Callback fired when the component requests to be shown.
107+
* Callback fired immediately after the React Dropdown is displayed.
72108
*/
73109
onShow?: () => void
110+
74111
/**
75-
* Describes the placement of your component after Popper.js has applied all the modifiers that may have flipped or altered the originally provided placement property.
112+
* Determines the placement of the React Dropdown Menu after Popper.js modifiers.
76113
*
77-
* @type 'auto' | 'top-end' | 'top' | 'top-start' | 'bottom-end' | 'bottom' | 'bottom-start' | 'right-start' | 'right' | 'right-end' | 'left-start' | 'left' | 'left-end'
114+
* @type 'auto' | 'auto-start' | 'auto-end' | 'top-end' | 'top' | 'top-start' | 'bottom-end' | 'bottom' | 'bottom-start' | 'right-start' | 'right' | 'right-end' | 'left-start' | 'left' | 'left-end'
78115
*/
79116
placement?: Placements
117+
80118
/**
81-
* If you want to disable dynamic positioning set this property to `true`.
119+
* Enables or disables dynamic positioning via Popper.js for the React Dropdown Menu.
82120
*/
83121
popper?: boolean
122+
123+
/**
124+
* Provides a custom Popper.js configuration or a function that returns a modified Popper.js configuration for advanced positioning of the React Dropdown Menu. [Read more](https://popper.js.org/docs/v2/constructors/#options)
125+
*
126+
* @example
127+
* // Providing a custom popper config
128+
* <CDropdown
129+
* popperConfig={{
130+
* modifiers: [
131+
* {
132+
* name: 'flip',
133+
* options: { fallbackPlacements: ['bottom', 'top'] },
134+
* },
135+
* ],
136+
* }}
137+
* >...</CDropdown>
138+
*
139+
* @since 5.5.0
140+
*/
141+
popperConfig?: Partial<Options> | ((defaultPopperConfig: Partial<Options>) => Partial<Options>)
142+
84143
/**
85-
* Generates dropdown menu using createPortal.
144+
* Renders the React Dropdown Menu using a React Portal, allowing it to escape the DOM hierarchy for improved positioning.
86145
*
87146
* @since 4.8.0
88147
*/
89148
portal?: boolean
149+
90150
/**
91-
* Set the dropdown variant to an btn-group, dropdown, input-group, and nav-item.
151+
* Defines the visual variant of the React Dropdown
92152
*/
93153
variant?: 'btn-group' | 'dropdown' | 'input-group' | 'nav-item'
154+
94155
/**
95-
* Toggle the visibility of dropdown menu component.
156+
* Controls the visibility of the React Dropdown Menu:
157+
* - `true` - Visible
158+
* - `false` - Hidden
159+
*
160+
* @example
161+
* // Programmatically manage the dropdown visibility
162+
* const [visible, setVisible] = useState(false)
163+
*
164+
* <CDropdown visible={visible}>
165+
* ...
166+
* </CDropdown>
167+
*
96168
*/
97169
visible?: boolean
98170
}
@@ -126,12 +198,13 @@ export const CDropdown: PolymorphicRefForwardingComponent<'div', CDropdownProps>
126198
onShow,
127199
placement = 'bottom-start',
128200
popper = true,
201+
popperConfig,
129202
portal = false,
130203
variant = 'btn-group',
131204
visible = false,
132205
...rest
133206
},
134-
ref,
207+
ref
135208
) => {
136209
const dropdownRef = useRef<HTMLDivElement>(null)
137210
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -161,7 +234,7 @@ export const CDropdown: PolymorphicRefForwardingComponent<'div', CDropdownProps>
161234
setVisible,
162235
}
163236

164-
const popperConfig = {
237+
const defaultPopperConfig = {
165238
modifiers: [
166239
{
167240
name: 'offset',
@@ -173,14 +246,20 @@ export const CDropdown: PolymorphicRefForwardingComponent<'div', CDropdownProps>
173246
placement: getPlacement(placement, direction, alignment, isRTL(dropdownMenuRef.current)),
174247
}
175248

249+
const computedPopperConfig: Partial<Options> = {
250+
...defaultPopperConfig,
251+
...(typeof popperConfig === 'function' ? popperConfig(defaultPopperConfig) : popperConfig),
252+
}
253+
176254
useEffect(() => {
177255
setVisible(visible)
178256
}, [visible])
179257

180258
useEffect(() => {
181259
if (_visible && dropdownToggleRef.current && dropdownMenuRef.current) {
182260
dropdownToggleRef.current.focus()
183-
popper && initPopper(dropdownToggleRef.current, dropdownMenuRef.current, popperConfig)
261+
popper &&
262+
initPopper(dropdownToggleRef.current, dropdownMenuRef.current, computedPopperConfig)
184263
window.addEventListener('mouseup', handleMouseUp)
185264
window.addEventListener('keyup', handleKeyup)
186265
dropdownToggleRef.current.addEventListener('keydown', handleKeydown)
@@ -209,7 +288,7 @@ export const CDropdown: PolymorphicRefForwardingComponent<'div', CDropdownProps>
209288
event.preventDefault()
210289
const target = event.target as HTMLElement
211290
const items: HTMLElement[] = Array.from(
212-
dropdownMenuRef.current.querySelectorAll('.dropdown-item:not(.disabled):not(:disabled)'),
291+
dropdownMenuRef.current.querySelectorAll('.dropdown-item:not(.disabled):not(:disabled)')
213292
)
214293
getNextActiveElement(items, target, event.key === 'ArrowDown', true).focus()
215294
}
@@ -258,7 +337,7 @@ export const CDropdown: PolymorphicRefForwardingComponent<'div', CDropdownProps>
258337
[`${direction}`]:
259338
direction && direction !== 'center' && direction !== 'dropup-center',
260339
},
261-
className,
340+
className
262341
)}
263342
{...rest}
264343
ref={forkedRef}
@@ -268,7 +347,7 @@ export const CDropdown: PolymorphicRefForwardingComponent<'div', CDropdownProps>
268347
)}
269348
</CDropdownContext.Provider>
270349
)
271-
},
350+
}
272351
)
273352

274353
const alignmentDirection = PropTypes.oneOf<Directions>(['start', 'end'])
@@ -297,6 +376,7 @@ CDropdown.propTypes = {
297376
onShow: PropTypes.func,
298377
placement: placementPropType,
299378
popper: PropTypes.bool,
379+
popperConfig: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
300380
portal: PropTypes.bool,
301381
variant: PropTypes.oneOf(['btn-group', 'dropdown', 'input-group', 'nav-item']),
302382
visible: PropTypes.bool,

0 commit comments

Comments
 (0)