Skip to content

Commit b06b99d

Browse files
committed
feat(CDropdown): add reference prop for custom positioning targets
1 parent 9a8af53 commit b06b99d

File tree

4 files changed

+94
-16
lines changed

4 files changed

+94
-16
lines changed

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

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import type { Placements } from '../../types'
2121
import { getNextActiveElement, isRTL } from '../../utils'
2222

2323
import type { Alignments, Directions } from './types'
24-
import { getPlacement } from './utils'
24+
import { getPlacement, getReferenceElement } from './utils'
2525
import { CFocusTrap } from '../focus-trap'
2626

2727
export interface CDropdownProps extends HTMLAttributes<HTMLDivElement | HTMLLIElement> {
@@ -161,6 +161,27 @@ export interface CDropdownProps extends HTMLAttributes<HTMLDivElement | HTMLLIEl
161161
*/
162162
portal?: boolean
163163

164+
/**
165+
* Sets the reference element for positioning the React Dropdown Menu.
166+
* - `toggle` - The React Dropdown Toggle button (default).
167+
* - `parent` - The React Dropdown wrapper element.
168+
* - `HTMLElement` - A custom HTML element.
169+
* - `React.RefObject` - A custom reference element.
170+
*
171+
* @example
172+
* // Use the parent element as reference for positioning
173+
* <CDropdown reference="parent">
174+
* <CDropdownToggle>Toggle dropdown</CDropdownToggle>
175+
* <CDropdownMenu>
176+
* <CDropdownItem>Action</CDropdownItem>
177+
* <CDropdownItem>Another Action</CDropdownItem>
178+
* </CDropdownMenu>
179+
* </CDropdown>
180+
*
181+
* @since 5.9.0
182+
*/
183+
reference?: 'parent' | 'toggle' | HTMLElement | React.RefObject<HTMLElement | null>
184+
164185
/**
165186
* Defines the visual variant of the React Dropdown
166187
*/
@@ -204,6 +225,7 @@ export const CDropdown: PolymorphicRefForwardingComponent<'div', CDropdownProps>
204225
popper = true,
205226
popperConfig,
206227
portal = false,
228+
reference = 'toggle',
207229
variant = 'btn-group',
208230
visible = false,
209231
...rest
@@ -255,12 +277,12 @@ export const CDropdown: PolymorphicRefForwardingComponent<'div', CDropdownProps>
255277
}, [visible])
256278

257279
useEffect(() => {
258-
const toggleElement = dropdownToggleElement
280+
const referenceElement = getReferenceElement(reference, dropdownToggleElement, dropdownRef)
259281
const menuElement = dropdownMenuRef.current
260-
if (allowPopperUse && menuElement && toggleElement && _visible) {
261-
initPopper(toggleElement, menuElement, computedPopperConfig)
282+
if (allowPopperUse && menuElement && referenceElement && _visible) {
283+
initPopper(referenceElement, menuElement, computedPopperConfig)
262284
}
263-
}, [dropdownToggleElement])
285+
}, [dropdownToggleElement, reference])
264286

265287
useEffect(() => {
266288
if (pendingKeyDownEvent !== null) {
@@ -272,21 +294,21 @@ export const CDropdown: PolymorphicRefForwardingComponent<'div', CDropdownProps>
272294
const handleHide = useCallback(() => {
273295
setVisible(false)
274296

275-
const toggleElement = dropdownToggleElement
276297
const menuElement = dropdownMenuRef.current
298+
const toggleElement = dropdownToggleElement
277299

278300
if (allowPopperUse) {
279301
destroyPopper()
280302
}
281303

282-
toggleElement?.removeEventListener('keydown', handleKeydown)
283304
menuElement?.removeEventListener('keydown', handleKeydown)
305+
toggleElement?.removeEventListener('keydown', handleKeydown)
284306

285307
window.removeEventListener('click', handleClick)
286308
window.removeEventListener('keyup', handleKeyup)
287309

288310
onHide?.()
289-
}, [dropdownToggleElement, allowPopperUse, destroyPopper, onHide])
311+
}, [allowPopperUse, dropdownToggleElement, destroyPopper, onHide])
290312

291313
const handleKeydown = useCallback((event: KeyboardEvent) => {
292314
if (!dropdownMenuRef.current) {
@@ -316,7 +338,7 @@ export const CDropdown: PolymorphicRefForwardingComponent<'div', CDropdownProps>
316338
dropdownToggleElement?.focus()
317339
}
318340
},
319-
[autoClose, handleHide]
341+
[autoClose, dropdownToggleElement, handleHide]
320342
)
321343

322344
const handleClick = useCallback(
@@ -357,14 +379,15 @@ export const CDropdown: PolymorphicRefForwardingComponent<'div', CDropdownProps>
357379

358380
const handleShow = useCallback(
359381
(event?: KeyboardEvent) => {
360-
const toggleElement = dropdownToggleElement
361382
const menuElement = dropdownMenuRef.current
383+
const referenceElement = getReferenceElement(reference, dropdownToggleElement, dropdownRef)
384+
const toggleElement = dropdownToggleElement
362385

363-
if (toggleElement && menuElement) {
386+
if (menuElement && referenceElement && toggleElement) {
364387
setVisible(true)
365388

366389
if (allowPopperUse) {
367-
initPopper(toggleElement, menuElement, computedPopperConfig)
390+
initPopper(referenceElement, menuElement, computedPopperConfig)
368391
}
369392

370393
toggleElement.focus()
@@ -382,13 +405,14 @@ export const CDropdown: PolymorphicRefForwardingComponent<'div', CDropdownProps>
382405
}
383406
},
384407
[
385-
dropdownToggleElement,
386408
allowPopperUse,
387-
initPopper,
388409
computedPopperConfig,
410+
dropdownToggleElement,
411+
reference,
389412
handleClick,
390413
handleKeydown,
391414
handleKeyup,
415+
initPopper,
392416
onShow,
393417
]
394418
)

packages/coreui-react/src/components/dropdown/utils.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import React from 'react'
12
import type { Placement } from '@popperjs/core'
23
import type { Placements } from '../../types'
34
import type { Alignments, Breakpoints } from './types'
@@ -49,3 +50,23 @@ export const getPlacement = (
4950

5051
return _placement
5152
}
53+
54+
export const getReferenceElement = (
55+
reference: 'parent' | 'toggle' | React.RefObject<HTMLElement | null> | HTMLElement,
56+
dropdownToggleElement: HTMLElement | null,
57+
dropdownRef: React.RefObject<HTMLElement | null>
58+
): HTMLElement | null => {
59+
if (reference === 'parent') {
60+
return dropdownRef.current
61+
}
62+
63+
if (reference instanceof HTMLElement) {
64+
return reference
65+
}
66+
67+
if (reference instanceof Object && 'current' in reference) {
68+
return reference.current
69+
}
70+
71+
return dropdownToggleElement
72+
}

packages/docs/content/api/CDropdown.api.mdx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,30 @@ const myContainer = document.getElementById('my-container')
190190
<p>Renders the React Dropdown Menu using a React Portal, allowing it to escape the DOM hierarchy for improved positioning.</p>
191191
</td>
192192
</tr>
193+
<tr id="cdropdown-reference">
194+
<td className="text-primary fw-semibold">reference<a href="#cdropdown-reference" aria-label="CDropdown reference permalink" className="anchor-link after">#</a><span className="badge bg-success">5.9.0+</span></td>
195+
<td><code>{`toggle`}</code></td>
196+
<td><code>{`HTMLElement`}</code>, <code>{`'parent'`}</code>, <code>{`'toggle'`}</code>, <code>{`RefObject\<HTMLElement>`}</code></td>
197+
</tr>
198+
<tr>
199+
<td colSpan="3">
200+
<p>Sets the reference element for positioning the React Dropdown Menu.</p>
201+
<ul>
202+
<li><code>{`toggle`}</code> - The React Dropdown Toggle button (default).</li>
203+
<li><code>{`parent`}</code> - The React Dropdown wrapper element.</li>
204+
<li><code>{`HTMLElement`}</code> - A custom HTML element.</li>
205+
<li><code>{`React.RefObject`}</code> - A custom reference element.</li>
206+
</ul>
207+
<JSXDocs code={`// Use the parent element as reference for positioning
208+
<CDropdown reference="parent">
209+
<CDropdownToggle>Toggle dropdown</CDropdownToggle>
210+
<CDropdownMenu>
211+
<CDropdownItem>Action</CDropdownItem>
212+
<CDropdownItem>Another Action</CDropdownItem>
213+
</CDropdownMenu>
214+
</CDropdown>`} />
215+
</td>
216+
</tr>
193217
<tr id="cdropdown-variant">
194218
<td className="text-primary fw-semibold">variant<a href="#cdropdown-variant" aria-label="CDropdown variant permalink" className="anchor-link after">#</a></td>
195219
<td><code>{`btn-group`}</code></td>

packages/docs/content/components/dropdown/examples/DropdownOptionsExample.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react'
2-
import { CDropdown, CDropdownItem, CDropdownMenu, CDropdownToggle } from '@coreui/react'
2+
import { CButton, CDropdown, CDropdownItem, CDropdownMenu, CDropdownToggle } from '@coreui/react'
33

44
export const DropdownOptionsExample = () => {
55
return (
@@ -12,7 +12,7 @@ export const DropdownOptionsExample = () => {
1212
<CDropdownItem href="#">Something else here</CDropdownItem>
1313
</CDropdownMenu>
1414
</CDropdown>
15-
<CDropdown portal variant="input-group">
15+
<CDropdown portal>
1616
<CDropdownToggle color="secondary" aria-controls="dropdownMenuInPortal">
1717
Portal
1818
</CDropdownToggle>
@@ -22,6 +22,15 @@ export const DropdownOptionsExample = () => {
2222
<CDropdownItem href="#">Something else here</CDropdownItem>
2323
</CDropdownMenu>
2424
</CDropdown>
25+
<CDropdown reference="parent">
26+
<CButton color="secondary">Reference</CButton>
27+
<CDropdownToggle color="secondary" split />
28+
<CDropdownMenu>
29+
<CDropdownItem href="#">Action</CDropdownItem>
30+
<CDropdownItem href="#">Another action</CDropdownItem>
31+
<CDropdownItem href="#">Something else here</CDropdownItem>
32+
</CDropdownMenu>
33+
</CDropdown>
2534
</div>
2635
)
2736
}

0 commit comments

Comments
 (0)