Skip to content

Commit

Permalink
feat(CConditionalPortal, CDropdown, CPopover, CTooltip): allow to app…
Browse files Browse the repository at this point in the history
…end component to the specific element
  • Loading branch information
mrholek committed Oct 24, 2023
1 parent 15bf9ca commit dd3106a
Show file tree
Hide file tree
Showing 10 changed files with 140 additions and 96 deletions.
Original file line number Diff line number Diff line change
@@ -1,29 +1,54 @@
import React, { FC, ReactNode } from 'react'
import React, { FC, ReactNode, useEffect, useState } from 'react'
import { createPortal } from 'react-dom'
import PropTypes from 'prop-types'

const getContainer = (container?: Element | (() => Element | null) | null) => {
if (container) {
return typeof container === 'function' ? container() : container
}

return document.body
}

export interface CConditionalPortalProps {
/**
* @ignore
*/
children: ReactNode
/**
* An HTML element or function that returns a single element, with `document.body` as the default.
*
* @since v4.11.0
*/
container?: Element | (() => Element | null) | null
/**
* Render some children into a different part of the DOM
*/
portal: boolean
portal: boolean | any
}

export const CConditionalPortal: FC<CConditionalPortalProps> = ({ children, portal }) => {
return typeof window !== 'undefined' && portal ? (
createPortal(children, document.body)
export const CConditionalPortal: FC<CConditionalPortalProps> = ({
children,
container,
portal,
}) => {
const [_container, setContainer] = useState<ReturnType<typeof getContainer>>(null)

useEffect(() => {
portal && setContainer(getContainer(container) || document.body)
}, [container, portal])

return typeof window !== 'undefined' && portal && _container ? (
createPortal(children, _container)
) : (
<>{children}</>
)
}

CConditionalPortal.propTypes = {
children: PropTypes.node,
portal: PropTypes.bool.isRequired,
container: PropTypes.any, // HTMLElement
portal: PropTypes.bool,
}

CConditionalPortal.displayName = 'CConditionalPortal'
8 changes: 8 additions & 0 deletions packages/coreui-react/src/components/dropdown/CDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ export interface CDropdownProps extends HTMLAttributes<HTMLDivElement | HTMLLIEl
* Component used for the root node. Either a string to use a HTML element or a component.
*/
component?: string | ElementType
/**
* 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`.
*
* @since v4.11.0
*/
container?: Element | (() => Element | null) | null
/**
* Sets a darker color scheme to match a dark navbar.
*/
Expand Down Expand Up @@ -147,6 +153,7 @@ export const CDropdown = forwardRef<HTMLDivElement | HTMLLIElement, CDropdownPro
alignment,
autoClose = true,
className,
container,
dark,
direction,
offset = [0, 2],
Expand Down Expand Up @@ -179,6 +186,7 @@ export const CDropdown = forwardRef<HTMLDivElement | HTMLLIElement, CDropdownPro

const contextValues = {
alignment,
container,
dark,
dropdownToggleRef,
dropdownMenuRef,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,13 @@ const alignmentClassNames = (alignment: Alignments) => {

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

const forkedRef = useForkedRef(ref, dropdownMenuRef)

return (
<CConditionalPortal portal={portal ?? false}>
<CConditionalPortal container={container} portal={portal ?? false}>
<Component
className={classNames(
'dropdown-menu',
Expand Down
87 changes: 47 additions & 40 deletions packages/coreui-react/src/components/popover/CPopover.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React, { forwardRef, HTMLAttributes, ReactNode, useRef, useEffect, useState } from 'react'
import { createPortal } from 'react-dom'
// import { createPortal } from 'react-dom'
import classNames from 'classnames'
import PropTypes from 'prop-types'
import { Transition } from 'react-transition-group'

import { CConditionalPortal } from '../conditional-portal'
import { useForkedRef, usePopper } from '../../hooks'
import { fallbackPlacementsPropType, triggerPropType } from '../../props'
import type { Placements, Triggers } from '../../types'
Expand All @@ -20,6 +21,12 @@ export interface CPopoverProps extends Omit<HTMLAttributes<HTMLDivElement>, 'tit
* A string of all className you want applied to the component.
*/
className?: string
/**
* 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`.
*
* @since v4.11.0
*/
container?: Element | (() => Element | null) | null
/**
* Content node for your component.
*/
Expand Down Expand Up @@ -74,6 +81,7 @@ export const CPopover = forwardRef<HTMLDivElement, CPopoverProps>(
children,
animation = true,
className,
container,
content,
delay = 0,
fallbackPlacements = ['top', 'right', 'bottom', 'left'],
Expand Down Expand Up @@ -160,45 +168,43 @@ export const CPopover = forwardRef<HTMLDivElement, CPopoverProps>(
onMouseLeave: () => toggleVisible(false),
}),
})}
{typeof window !== 'undefined' &&
createPortal(
<Transition
in={_visible}
mountOnEnter
nodeRef={popoverRef}
onEnter={onShow}
onExit={onHide}
timeout={{
enter: 0,
exit: popoverRef.current
? getTransitionDurationFromElement(popoverRef.current) + 50
: 200,
}}
unmountOnExit
>
{(state) => (
<div
className={classNames(
'popover',
'bs-popover-auto',
{
fade: animation,
show: state === 'entered',
},
className,
)}
ref={forkedRef}
role="tooltip"
{...rest}
>
<div className="popover-arrow"></div>
<div className="popover-header">{title}</div>
<div className="popover-body">{content}</div>
</div>
)}
</Transition>,
document.body,
)}
<CConditionalPortal container={container} portal={true}>
<Transition
in={_visible}
mountOnEnter
nodeRef={popoverRef}
onEnter={onShow}
onExit={onHide}
timeout={{
enter: 0,
exit: popoverRef.current
? getTransitionDurationFromElement(popoverRef.current) + 50
: 200,
}}
unmountOnExit
>
{(state) => (
<div
className={classNames(
'popover',
'bs-popover-auto',
{
fade: animation,
show: state === 'entered',
},
className,
)}
ref={forkedRef}
role="tooltip"
{...rest}
>
<div className="popover-arrow"></div>
<div className="popover-header">{title}</div>
<div className="popover-body">{content}</div>
</div>
)}
</Transition>
</CConditionalPortal>
</>
)
},
Expand All @@ -208,6 +214,7 @@ CPopover.propTypes = {
animation: PropTypes.bool,
children: PropTypes.node,
className: PropTypes.string,
container: PropTypes.any,
content: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
delay: PropTypes.oneOfType([
PropTypes.number,
Expand Down
93 changes: 46 additions & 47 deletions packages/coreui-react/src/components/tooltip/CTooltip.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,9 @@
import React, {
forwardRef,
HTMLAttributes,
ReactNode,
useRef,
useEffect,
useState,
} from 'react'
import { createPortal } from 'react-dom'
import React, { forwardRef, HTMLAttributes, ReactNode, useRef, useEffect, useState } from 'react'
import classNames from 'classnames'
import PropTypes from 'prop-types'
import { Transition } from 'react-transition-group'

import { CConditionalPortal } from '../conditional-portal'
import { useForkedRef, usePopper } from '../../hooks'
import { fallbackPlacementsPropType, triggerPropType } from '../../props'
import type { Placements, Triggers } from '../../types'
Expand All @@ -27,6 +20,12 @@ export interface CTooltipProps extends Omit<HTMLAttributes<HTMLDivElement>, 'con
* A string of all className you want applied to the component.
*/
className?: string
/**
* 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`.
*
* @since v4.11.0
*/
container?: Element | (() => Element | null) | null
/**
* Content node for your component.
*/
Expand Down Expand Up @@ -77,6 +76,7 @@ export const CTooltip = forwardRef<HTMLDivElement, CTooltipProps>(
children,
animation = true,
className,
container,
content,
delay = 0,
fallbackPlacements = ['top', 'right', 'bottom', 'left'],
Expand Down Expand Up @@ -162,44 +162,42 @@ export const CTooltip = forwardRef<HTMLDivElement, CTooltipProps>(
onMouseLeave: () => toggleVisible(false),
}),
})}
{typeof window !== 'undefined' &&
createPortal(
<Transition
in={_visible}
mountOnEnter
nodeRef={tooltipRef}
onEnter={onShow}
onExit={onHide}
timeout={{
enter: 0,
exit: tooltipRef.current
? getTransitionDurationFromElement(tooltipRef.current) + 50
: 200,
}}
unmountOnExit
>
{(state) => (
<div
className={classNames(
'tooltip',
'bs-tooltip-auto',
{
fade: animation,
show: state === 'entered',
},
className,
)}
ref={forkedRef}
role="tooltip"
{...rest}
>
<div className="tooltip-arrow"></div>
<div className="tooltip-inner">{content}</div>
</div>
)}
</Transition>,
document.body,
)}
<CConditionalPortal container={container} portal={true}>
<Transition
in={_visible}
mountOnEnter
nodeRef={tooltipRef}
onEnter={onShow}
onExit={onHide}
timeout={{
enter: 0,
exit: tooltipRef.current
? getTransitionDurationFromElement(tooltipRef.current) + 50
: 200,
}}
unmountOnExit
>
{(state) => (
<div
className={classNames(
'tooltip',
'bs-tooltip-auto',
{
fade: animation,
show: state === 'entered',
},
className,
)}
ref={forkedRef}
role="tooltip"
{...rest}
>
<div className="tooltip-arrow"></div>
<div className="tooltip-inner">{content}</div>
</div>
)}
</Transition>
</CConditionalPortal>
</>
)
},
Expand All @@ -208,6 +206,7 @@ export const CTooltip = forwardRef<HTMLDivElement, CTooltipProps>(
CTooltip.propTypes = {
animation: PropTypes.bool,
children: PropTypes.node,
container: PropTypes.any,
content: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
delay: PropTypes.oneOfType([
PropTypes.number,
Expand Down
3 changes: 2 additions & 1 deletion packages/docs/content/api/CConditionalPortal.api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ import CConditionalPortal from '@coreui/react/src/components/conditional-portal/

| Property | Description | Type | Default |
| --- | --- | --- | --- |
| **portal** | Render some children into a different part of the DOM | `boolean` | - |
| **container** **_v4.11.0+_** | An HTML element or function that returns a single element, with `document.body` as the default. | `Element` \| `(() => Element)` | - |
| **portal** | Render some children into a different part of the DOM | `any` | - |
1 change: 1 addition & 0 deletions packages/docs/content/api/CDropdown.api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import CDropdown from '@coreui/react/src/components/dropdown/CDropdown'
| **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 |
| **className** | A string of all className you want applied to the base component. | `string` | - |
| **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 |
| **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)` | - |
| **dark** | Sets a darker color scheme to match a dark navbar. | `boolean` | - |
| **direction** | Sets a specified direction and location of the dropdown menu. | `'center'` \| `'dropup'` \| `'dropup-center'` \| `'dropend'` \| `'dropstart'` | - |
| **offset** | Offset of the dropdown menu relative to its target. | `[number, number]` | [0, 2] |
Expand Down
1 change: 1 addition & 0 deletions packages/docs/content/api/CModal.api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import CModal from '@coreui/react/src/components/modal/CModal'
| **alignment** | Align the modal in the center or top of the screen. | `'top'` \| `'center'` | - |
| **backdrop** | Apply a backdrop on body while modal is open. | `boolean` \| `'static'` | true |
| **className** | A string of all className you want applied to the base component. | `string` | - |
| **focus** **_v4.10.0+_** | Puts the focus on the modal when shown. | `boolean` | true |
| **fullscreen** | Set modal to covers the entire user viewport. | `boolean` \| `'sm'` \| `'md'` \| `'lg'` \| `'xl'` \| `'xxl'` | - |
| **keyboard** | Closes the modal when escape key is pressed. | `boolean` | true |
| **onClose** | Callback fired when the component requests to be closed. | `() => void` | - |
Expand Down
1 change: 1 addition & 0 deletions packages/docs/content/api/CPopover.api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import CPopover from '@coreui/react/src/components/popover/CPopover'
| --- | --- | --- | --- |
| **animation** **_4.9.0+_** | Apply a CSS fade transition to the popover. | `boolean` | true |
| **className** | A string of all className you want applied to the component. | `string` | - |
| **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)` | - |
| **content** | Content node for your component. | `ReactNode` | - |
| **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 |
| **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'] |
Expand Down
1 change: 1 addition & 0 deletions packages/docs/content/api/CTooltip.api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import CTooltip from '@coreui/react/src/components/tooltip/CTooltip'
| --- | --- | --- | --- |
| **animation** **_4.9.0+_** | Apply a CSS fade transition to the tooltip. | `boolean` | true |
| **className** | A string of all className you want applied to the component. | `string` | - |
| **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)` | - |
| **content** | Content node for your component. | `ReactNode` | - |
| **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 |
| **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'] |
Expand Down

0 comments on commit dd3106a

Please sign in to comment.