Skip to content

Commit 1c15b39

Browse files
committed
feat(CConditionalPortal, CDropdown, CPopover, CTooltip): allow to append component to the specific element
1 parent 82e3ce6 commit 1c15b39

File tree

9 files changed

+139
-96
lines changed

9 files changed

+139
-96
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,54 @@
1-
import React, { FC, ReactNode } from 'react'
1+
import React, { FC, ReactNode, useEffect, useState } from 'react'
22
import { createPortal } from 'react-dom'
33
import PropTypes from 'prop-types'
44

5+
const getContainer = (container?: Element | (() => Element | null) | null) => {
6+
if (container) {
7+
return typeof container === 'function' ? container() : container
8+
}
9+
10+
return document.body
11+
}
12+
513
export interface CConditionalPortalProps {
614
/**
715
* @ignore
816
*/
917
children: ReactNode
18+
/**
19+
* An HTML element or function that returns a single element, with `document.body` as the default.
20+
*
21+
* @since v4.11.0
22+
*/
23+
container?: Element | (() => Element | null) | null
1024
/**
1125
* Render some children into a different part of the DOM
1226
*/
13-
portal: boolean
27+
portal: boolean | any
1428
}
1529

16-
export const CConditionalPortal: FC<CConditionalPortalProps> = ({ children, portal }) => {
17-
return typeof window !== 'undefined' && portal ? (
18-
createPortal(children, document.body)
30+
export const CConditionalPortal: FC<CConditionalPortalProps> = ({
31+
children,
32+
container,
33+
portal,
34+
}) => {
35+
const [_container, setContainer] = useState<ReturnType<typeof getContainer>>(null)
36+
37+
useEffect(() => {
38+
portal && setContainer(getContainer(container) || document.body)
39+
}, [container, portal])
40+
41+
return typeof window !== 'undefined' && portal && _container ? (
42+
createPortal(children, _container)
1943
) : (
2044
<>{children}</>
2145
)
2246
}
2347

2448
CConditionalPortal.propTypes = {
2549
children: PropTypes.node,
26-
portal: PropTypes.bool.isRequired,
50+
container: PropTypes.any, // HTMLElement
51+
portal: PropTypes.bool,
2752
}
2853

2954
CConditionalPortal.displayName = 'CConditionalPortal'

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

+8
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ export interface CDropdownProps extends HTMLAttributes<HTMLDivElement | HTMLLIEl
4242
* Component used for the root node. Either a string to use a HTML element or a component.
4343
*/
4444
component?: string | ElementType
45+
/**
46+
* 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`.
47+
*
48+
* @since v4.11.0
49+
*/
50+
container?: Element | (() => Element | null) | null
4551
/**
4652
* Sets a darker color scheme to match a dark navbar.
4753
*/
@@ -107,6 +113,7 @@ export const CDropdown = forwardRef<HTMLDivElement | HTMLLIElement, CDropdownPro
107113
alignment,
108114
autoClose = true,
109115
className,
116+
container,
110117
dark,
111118
direction,
112119
offset = [0, 2],
@@ -139,6 +146,7 @@ export const CDropdown = forwardRef<HTMLDivElement | HTMLLIElement, CDropdownPro
139146

140147
const contextValues = {
141148
alignment,
149+
container,
142150
dark,
143151
dropdownToggleRef,
144152
dropdownMenuRef,

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,13 @@ export interface CDropdownMenuProps extends HTMLAttributes<HTMLDivElement | HTML
2323

2424
export const CDropdownMenu = forwardRef<HTMLDivElement | HTMLUListElement, CDropdownMenuProps>(
2525
({ children, className, component: Component = 'ul', ...rest }, ref) => {
26-
const { alignment, dark, dropdownMenuRef, popper, portal, visible } =
26+
const { alignment, container, dark, dropdownMenuRef, popper, portal, visible } =
2727
useContext(CDropdownContext)
2828

2929
const forkedRef = useForkedRef(ref, dropdownMenuRef)
3030

3131
return (
32-
<CConditionalPortal portal={portal ?? false}>
32+
<CConditionalPortal container={container} portal={portal ?? false}>
3333
<Component
3434
className={classNames(
3535
'dropdown-menu',

packages/coreui-react/src/components/popover/CPopover.tsx

+47-40
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import React, { forwardRef, HTMLAttributes, ReactNode, useRef, useEffect, useState } from 'react'
2-
import { createPortal } from 'react-dom'
2+
// import { createPortal } from 'react-dom'
33
import classNames from 'classnames'
44
import PropTypes from 'prop-types'
55
import { Transition } from 'react-transition-group'
66

7+
import { CConditionalPortal } from '../conditional-portal'
78
import { useForkedRef, usePopper } from '../../hooks'
89
import { fallbackPlacementsPropType, triggerPropType } from '../../props'
910
import type { Placements, Triggers } from '../../types'
@@ -20,6 +21,12 @@ export interface CPopoverProps extends Omit<HTMLAttributes<HTMLDivElement>, 'tit
2021
* A string of all className you want applied to the component.
2122
*/
2223
className?: string
24+
/**
25+
* Appends the react popover to a specific element. You can pass an HTML element or function that returns a single element. By default `document.body`.
26+
*
27+
* @since v4.11.0
28+
*/
29+
container?: Element | (() => Element | null) | null
2330
/**
2431
* Content node for your component.
2532
*/
@@ -74,6 +81,7 @@ export const CPopover = forwardRef<HTMLDivElement, CPopoverProps>(
7481
children,
7582
animation = true,
7683
className,
84+
container,
7785
content,
7886
delay = 0,
7987
fallbackPlacements = ['top', 'right', 'bottom', 'left'],
@@ -160,45 +168,43 @@ export const CPopover = forwardRef<HTMLDivElement, CPopoverProps>(
160168
onMouseLeave: () => toggleVisible(false),
161169
}),
162170
})}
163-
{typeof window !== 'undefined' &&
164-
createPortal(
165-
<Transition
166-
in={_visible}
167-
mountOnEnter
168-
nodeRef={popoverRef}
169-
onEnter={onShow}
170-
onExit={onHide}
171-
timeout={{
172-
enter: 0,
173-
exit: popoverRef.current
174-
? getTransitionDurationFromElement(popoverRef.current) + 50
175-
: 200,
176-
}}
177-
unmountOnExit
178-
>
179-
{(state) => (
180-
<div
181-
className={classNames(
182-
'popover',
183-
'bs-popover-auto',
184-
{
185-
fade: animation,
186-
show: state === 'entered',
187-
},
188-
className,
189-
)}
190-
ref={forkedRef}
191-
role="tooltip"
192-
{...rest}
193-
>
194-
<div className="popover-arrow"></div>
195-
<div className="popover-header">{title}</div>
196-
<div className="popover-body">{content}</div>
197-
</div>
198-
)}
199-
</Transition>,
200-
document.body,
201-
)}
171+
<CConditionalPortal container={container} portal={true}>
172+
<Transition
173+
in={_visible}
174+
mountOnEnter
175+
nodeRef={popoverRef}
176+
onEnter={onShow}
177+
onExit={onHide}
178+
timeout={{
179+
enter: 0,
180+
exit: popoverRef.current
181+
? getTransitionDurationFromElement(popoverRef.current) + 50
182+
: 200,
183+
}}
184+
unmountOnExit
185+
>
186+
{(state) => (
187+
<div
188+
className={classNames(
189+
'popover',
190+
'bs-popover-auto',
191+
{
192+
fade: animation,
193+
show: state === 'entered',
194+
},
195+
className,
196+
)}
197+
ref={forkedRef}
198+
role="tooltip"
199+
{...rest}
200+
>
201+
<div className="popover-arrow"></div>
202+
<div className="popover-header">{title}</div>
203+
<div className="popover-body">{content}</div>
204+
</div>
205+
)}
206+
</Transition>
207+
</CConditionalPortal>
202208
</>
203209
)
204210
},
@@ -208,6 +214,7 @@ CPopover.propTypes = {
208214
animation: PropTypes.bool,
209215
children: PropTypes.node,
210216
className: PropTypes.string,
217+
container: PropTypes.any,
211218
content: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
212219
delay: PropTypes.oneOfType([
213220
PropTypes.number,

packages/coreui-react/src/components/tooltip/CTooltip.tsx

+46-47
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,9 @@
1-
import React, {
2-
forwardRef,
3-
HTMLAttributes,
4-
ReactNode,
5-
useRef,
6-
useEffect,
7-
useState,
8-
} from 'react'
9-
import { createPortal } from 'react-dom'
1+
import React, { forwardRef, HTMLAttributes, ReactNode, useRef, useEffect, useState } from 'react'
102
import classNames from 'classnames'
113
import PropTypes from 'prop-types'
124
import { Transition } from 'react-transition-group'
135

6+
import { CConditionalPortal } from '../conditional-portal'
147
import { useForkedRef, usePopper } from '../../hooks'
158
import { fallbackPlacementsPropType, triggerPropType } from '../../props'
169
import type { Placements, Triggers } from '../../types'
@@ -27,6 +20,12 @@ export interface CTooltipProps extends Omit<HTMLAttributes<HTMLDivElement>, 'con
2720
* A string of all className you want applied to the component.
2821
*/
2922
className?: string
23+
/**
24+
* Appends the react tooltip to a specific element. You can pass an HTML element or function that returns a single element. By default `document.body`.
25+
*
26+
* @since v4.11.0
27+
*/
28+
container?: Element | (() => Element | null) | null
3029
/**
3130
* Content node for your component.
3231
*/
@@ -77,6 +76,7 @@ export const CTooltip = forwardRef<HTMLDivElement, CTooltipProps>(
7776
children,
7877
animation = true,
7978
className,
79+
container,
8080
content,
8181
delay = 0,
8282
fallbackPlacements = ['top', 'right', 'bottom', 'left'],
@@ -162,44 +162,42 @@ export const CTooltip = forwardRef<HTMLDivElement, CTooltipProps>(
162162
onMouseLeave: () => toggleVisible(false),
163163
}),
164164
})}
165-
{typeof window !== 'undefined' &&
166-
createPortal(
167-
<Transition
168-
in={_visible}
169-
mountOnEnter
170-
nodeRef={tooltipRef}
171-
onEnter={onShow}
172-
onExit={onHide}
173-
timeout={{
174-
enter: 0,
175-
exit: tooltipRef.current
176-
? getTransitionDurationFromElement(tooltipRef.current) + 50
177-
: 200,
178-
}}
179-
unmountOnExit
180-
>
181-
{(state) => (
182-
<div
183-
className={classNames(
184-
'tooltip',
185-
'bs-tooltip-auto',
186-
{
187-
fade: animation,
188-
show: state === 'entered',
189-
},
190-
className,
191-
)}
192-
ref={forkedRef}
193-
role="tooltip"
194-
{...rest}
195-
>
196-
<div className="tooltip-arrow"></div>
197-
<div className="tooltip-inner">{content}</div>
198-
</div>
199-
)}
200-
</Transition>,
201-
document.body,
202-
)}
165+
<CConditionalPortal container={container} portal={true}>
166+
<Transition
167+
in={_visible}
168+
mountOnEnter
169+
nodeRef={tooltipRef}
170+
onEnter={onShow}
171+
onExit={onHide}
172+
timeout={{
173+
enter: 0,
174+
exit: tooltipRef.current
175+
? getTransitionDurationFromElement(tooltipRef.current) + 50
176+
: 200,
177+
}}
178+
unmountOnExit
179+
>
180+
{(state) => (
181+
<div
182+
className={classNames(
183+
'tooltip',
184+
'bs-tooltip-auto',
185+
{
186+
fade: animation,
187+
show: state === 'entered',
188+
},
189+
className,
190+
)}
191+
ref={forkedRef}
192+
role="tooltip"
193+
{...rest}
194+
>
195+
<div className="tooltip-arrow"></div>
196+
<div className="tooltip-inner">{content}</div>
197+
</div>
198+
)}
199+
</Transition>
200+
</CConditionalPortal>
203201
</>
204202
)
205203
},
@@ -208,6 +206,7 @@ export const CTooltip = forwardRef<HTMLDivElement, CTooltipProps>(
208206
CTooltip.propTypes = {
209207
animation: PropTypes.bool,
210208
children: PropTypes.node,
209+
container: PropTypes.any,
211210
content: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
212211
delay: PropTypes.oneOfType([
213212
PropTypes.number,

packages/docs/content/api/CConditionalPortal.api.mdx

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ import CConditionalPortal from '@coreui/react/src/components/conditional-portal/
77

88
| Property | Description | Type | Default |
99
| --- | --- | --- | --- |
10-
| **portal** | Render some children into a different part of the DOM | `boolean` | - |
10+
| **container** **_v4.11.0+_** | An HTML element or function that returns a single element, with `document.body` as the default. | `Element` \| `(() => Element)` | - |
11+
| **portal** | Render some children into a different part of the DOM | `any` | - |

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

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import CDropdown from '@coreui/react/src/components/dropdown/CDropdown'
1111
| **autoClose** | Configure the auto close behavior of the dropdown:<br/>- `true` - the dropdown will be closed by clicking outside or inside the dropdown menu.<br/>- `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)<br/>- `'inside'` - the dropdown will be closed (only) by clicking inside the dropdown menu.<br/>- `'outside'` - the dropdown will be closed (only) by clicking outside the dropdown menu. | `boolean` \| `'inside'` \| `'outside'` | true |
1212
| **className** | A string of all className you want applied to the base component. | `string` | - |
1313
| **component** | Component used for the root node. Either a string to use a HTML element or a component. | `string` \| `ComponentClass<any, any>` \| `FunctionComponent<any>` | div |
14+
| **container** **_v4.11.0+_** | 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`. | `Element` \| `(() => Element)` | - |
1415
| **dark** | Sets a darker color scheme to match a dark navbar. | `boolean` | - |
1516
| **direction** | Sets a specified direction and location of the dropdown menu. | `'center'` \| `'dropup'` \| `'dropup-center'` \| `'dropend'` \| `'dropstart'` | - |
1617
| **offset** | Offset of the dropdown menu relative to its target. | `[number, number]` | [0, 2] |

packages/docs/content/api/CPopover.api.mdx

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import CPopover from '@coreui/react/src/components/popover/CPopover'
99
| --- | --- | --- | --- |
1010
| **animation** **_4.9.0+_** | Apply a CSS fade transition to the popover. | `boolean` | true |
1111
| **className** | A string of all className you want applied to the component. | `string` | - |
12+
| **container** **_v4.11.0+_** | Appends the react popover to a specific element. You can pass an HTML element or function that returns a single element. By default `document.body`. | `Element` \| `(() => Element)` | - |
1213
| **content** | Content node for your component. | `ReactNode` | - |
1314
| **delay** **_4.9.0+_** | The delay for displaying and hiding the popover (in milliseconds). When a numerical value is provided, the delay applies to both the hide and show actions. The object structure for specifying the delay is as follows: delay: `{ 'show': 500, 'hide': 100 }`. | `number` \| `{ show: number; hide: number; }` | 0 |
1415
| **fallbackPlacements** **_4.9.0+_** | Specify the desired order of fallback placements by providing a list of placements as an array. The placements should be prioritized based on preference. | `Placements` \| `Placements[]` | ['top', 'right', 'bottom', 'left'] |

packages/docs/content/api/CTooltip.api.mdx

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import CTooltip from '@coreui/react/src/components/tooltip/CTooltip'
99
| --- | --- | --- | --- |
1010
| **animation** **_4.9.0+_** | Apply a CSS fade transition to the tooltip. | `boolean` | true |
1111
| **className** | A string of all className you want applied to the component. | `string` | - |
12+
| **container** **_v4.11.0+_** | Appends the react tooltip to a specific element. You can pass an HTML element or function that returns a single element. By default `document.body`. | `Element` \| `(() => Element)` | - |
1213
| **content** | Content node for your component. | `ReactNode` | - |
1314
| **delay** **_4.9.0+_** | The delay for displaying and hiding the tooltip (in milliseconds). When a numerical value is provided, the delay applies to both the hide and show actions. The object structure for specifying the delay is as follows: delay: `{ 'show': 500, 'hide': 100 }`. | `number` \| `{ show: number; hide: number; }` | 0 |
1415
| **fallbackPlacements** **_4.9.0+_** | Specify the desired order of fallback placements by providing a list of placements as an array. The placements should be prioritized based on preference. | `Placements` \| `Placements[]` | ['top', 'right', 'bottom', 'left'] |

0 commit comments

Comments
 (0)