Skip to content

Commit 1b626c5

Browse files
committed
Export portal component
1 parent 8876163 commit 1b626c5

File tree

7 files changed

+146
-10
lines changed

7 files changed

+146
-10
lines changed

packages/react-components/source/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import Loading from './react/library/loading';
2626
import Logo from './react/library/logo';
2727
import Modal from './react/library/modal';
2828
import Popover from './react/library/popover';
29+
import Portal from './react/library/portal';
2930
import Stepper from './react/library/stepper';
3031
import RadioButton from './react/library/radiobutton';
3132
import Select from './react/library/select';
@@ -67,6 +68,7 @@ export {
6768
Modal,
6869
Overlay,
6970
Popover,
71+
Portal,
7072
Stepper,
7173
RadioButton,
7274
Select,

packages/react-components/source/react/library/copy/Copy.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,10 @@ const Copy = ({
5656
const child = React.Children.only(children);
5757
// An explicitly set `value` prop on the child node supercedes child text
5858
// value = child.props?.children ? child.props.children : value;
59-
if (typeof child.props?.children === 'string')
59+
if (child.props && typeof child.props.children === 'string')
6060
copyValue = child.props.children;
6161

62-
if (child.props?.value) copyValue = child.props.value;
62+
if (child.props && child.props.value) copyValue = child.props.value;
6363
} catch (e) {
6464
// If `children` is not a single React element, a string node is a valid value
6565
if (typeof React.Children.toArray(children)[0] === 'string')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
## Overview
2+
3+
Portals provide a quick and easy way to render elements at any given point in the DOM hierarchy. This can be useful when positioning tooltips, modals, nav menus, or other elements that need to be positioned higher in the DOM but controlled or triggered from a deeply nested component.
4+
5+
<div id="rc-portal-ex-overview"><div>
6+
### Portal
7+
8+
```jsx
9+
const { useState } = require('react');
10+
import Portal from '../portal';
11+
import Button from '../button';
12+
import Content from '../content';
13+
import Heading from '../heading';
14+
import Link from '../link';
15+
import Text from '../text';
16+
17+
const [portalLocation, setPortalLocation] = useState();
18+
const [portalActive, setPortalActive] = useState(false);
19+
const renderIn = target => {
20+
setPortalLocation(target);
21+
setPortalActive(!!target);
22+
};
23+
24+
const location = {
25+
'ex-sibling': 'the sibling div',
26+
'ex-overview': 'the Overview section',
27+
'ex-parent': 'the parent',
28+
};
29+
<>
30+
<div id="rc-portal-ex-parent" style={{ color: 'blue' }}>
31+
<div
32+
id="rc-portal-ex-sibling"
33+
style={{
34+
color: 'MintCream',
35+
padding: '8px',
36+
backgroundColor: 'LightSlateGrey',
37+
marginBottom: '8px',
38+
}}
39+
>
40+
Sibling Element
41+
</div>
42+
<div style={{ color: 'red' }}>
43+
<Portal target={portalLocation} active={portalActive}>
44+
<h3>
45+
{portalLocation
46+
? `I'm rendering in ${location[portalLocation]}!`
47+
: `I'm not rendering in a portal `}
48+
</h3>
49+
</Portal>
50+
</div>
51+
</div>
52+
<Button onClick={() => renderIn('ex-sibling')}>Render in sibling</Button>
53+
<Button onClick={() => renderIn('ex-overview')}>Render in Overview</Button>
54+
<Button onClick={() => renderIn('ex-parent')}>Render in parent</Button>
55+
<Button onClick={() => renderIn()}>Deactivate portal</Button>
56+
</>;
57+
```
58+
59+
## Variations
60+
61+
By default, if the target id was not found within the DOM, a div will be created and appended to the root node of the application. The target id, style, and className are then applied to the newly created div. If the target div already exists, the portal's children are appended to it.
62+
63+
```jsx
64+
const { useState } = require('react');
65+
import Button from '../button';
66+
import Portal from '../portal';
67+
68+
const [showMenu, setShowMenu] = useState(false);
69+
const [showMore, setShowMore] = useState(false);
70+
71+
const menuStyle = {
72+
backgroundColor: 'lightSlateGrey',
73+
borderRadius: '4px',
74+
color: 'mintCream',
75+
height: 'fit-content',
76+
width: '90%',
77+
position: 'absolute',
78+
top: '25px',
79+
left: '5%',
80+
zIndex: '100',
81+
textAlign: 'center',
82+
};
83+
<>
84+
<Button onClick={() => setShowMenu(!showMenu)}>
85+
{showMenu ? 'Close' : 'Render in'} menu
86+
</Button>
87+
<Portal active={showMenu} style={menuStyle} className="test">
88+
<h3>I'm some menu content</h3>
89+
</Portal>
90+
91+
<Button onClick={() => setShowMore(!showMore)}>
92+
{showMenu && showMore ? 'Hide' : 'Show'} more content
93+
</Button>
94+
<Portal>{showMenu && showMore && <h3>I'm more content</h3>}</Portal>
95+
</>;
96+
```
97+
98+
## Related
99+
100+
- [TooltipHoverArea](#/React%20Components/TooltipHoverArea)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import Portal from './Portal';
2+
3+
export default Portal;
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,57 @@
1+
import React, { useEffect } from 'react';
12
import { createPortal } from 'react-dom';
23
import PropTypes from 'prop-types';
34

45
const propTypes = {
5-
target: PropTypes.oneOf(['tooltip']),
6+
/** Target id of div where portal will append content. Creates the div at application root if id can't be found in the DOM. All target div Ids must have the prefix: `rc-portal-`*/
7+
target: PropTypes.string,
8+
/** Boolean value used to conditionally render content in target div. If false, it will render the content at its current location in the dom*/
9+
active: PropTypes.bool,
10+
/** Optional additional className to apply to portal div*/
11+
className: PropTypes.string,
12+
/** Optional inline styles to apply to portal div*/
13+
style: PropTypes.shape({}),
14+
};
15+
const defaultProps = {
16+
target: 'default',
17+
active: true,
618
};
719

8-
const Portal = ({ children, target = 'tooltip' }) => {
20+
const Portal = ({ children, target, active, style, className, method }) => {
921
// portal target with fallbacks
1022
const root =
1123
document.getElementsByClassName('app')[0] ||
1224
document.getElementById('root') ||
1325
document.body;
1426

15-
const portalId = `portal-${target}`;
27+
const portalId = `rc-portal-${target}`;
1628
let portal = document.getElementById(portalId);
1729

18-
if (!portal && root) {
30+
if (!portal && root && target) {
1931
portal = document.createElement('div');
2032
portal.id = portalId;
33+
34+
// TODO: add option to prepend
2135
root.appendChild(portal);
2236
}
23-
return createPortal(children, portal);
37+
38+
//Apply classes and styles to portal div
39+
if (className) portal.className = className;
40+
if (style) Object.assign(portal.style, style);
41+
42+
// Remove portal on unmount
43+
useEffect(() => {
44+
return () => {
45+
const p = document.getElementById(portalId);
46+
if (p) p.remove();
47+
};
48+
}, []);
49+
50+
// Remove portal if not active
51+
if (!active && portal) portal.remove();
52+
53+
return <>{active && target ? createPortal(children, portal) : children}</>;
2454
};
2555
Portal.propTypes = propTypes;
56+
Portal.defaultProps = defaultProps;
2657
export default Portal;

packages/react-components/source/react/library/tooltips/TooltipHoverArea.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef } from 'react';
22
import PropTypes from 'prop-types';
33
import classNames from 'classnames';
44
import { usePopper } from 'react-popper';
5-
import Portal from '../portal/portal';
5+
import Portal from '../portal';
66

77
export const propTypes = {
88
/** Position of tooltip relative to the activating element */
@@ -128,7 +128,7 @@ const TooltipHoverArea = ({
128128
return (
129129
<>
130130
{!!children && !!tooltip && (
131-
<Portal>
131+
<Portal target="tooltip">
132132
{/* eslint-disable-next-line */}
133133
<div
134134
id={tooltipId}

packages/react-components/source/react/library/tooltips/TooltipHoverArea.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ const [modalIsDisabled, setDisabled] = useState(false);
5959
anchor="right"
6060
>
6161
<Button onClick={() => setDisabled(!modalIsDisabled)}>
62-
{`Click me to ${modalIsDisabled ? 'disable' : 'enable'} tooltip`}
62+
{`Click me to ${!modalIsDisabled ? 'disable' : 'enable'} tooltip`}
6363
</Button>
6464
</TooltipHoverArea>
6565
</div>;

0 commit comments

Comments
 (0)