Skip to content

Export portal component #561

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/react-components/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/react-components/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@puppet/react-components",
"version": "5.33.4",
"version": "5.33.5",
"author": "Puppet, Inc.",
"license": "Apache-2.0",
"main": "build/library.js",
Expand Down
2 changes: 2 additions & 0 deletions packages/react-components/source/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import Loading from './react/library/loading';
import Logo from './react/library/logo';
import Modal from './react/library/modal';
import Popover from './react/library/popover';
import Portal from './react/library/portal';
import Stepper from './react/library/stepper';
import RadioButton from './react/library/radiobutton';
import Select from './react/library/select';
Expand Down Expand Up @@ -67,6 +68,7 @@ export {
Modal,
Overlay,
Popover,
Portal,
Stepper,
RadioButton,
Select,
Expand Down
4 changes: 2 additions & 2 deletions packages/react-components/source/react/library/copy/Copy.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,10 @@ const Copy = ({
const child = React.Children.only(children);
// An explicitly set `value` prop on the child node supercedes child text
// value = child.props?.children ? child.props.children : value;
if (typeof child.props?.children === 'string')
if (child.props && typeof child.props.children === 'string')
copyValue = child.props.children;

if (child.props?.value) copyValue = child.props.value;
if (child.props && child.props.value) copyValue = child.props.value;
} catch (e) {
// If `children` is not a single React element, a string node is a valid value
if (typeof React.Children.toArray(children)[0] === 'string')
Expand Down
100 changes: 100 additions & 0 deletions packages/react-components/source/react/library/portal/Portal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
## Overview

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.

<div id="rc-portal-ex-overview"><div>
### Portal

```jsx
const { useState } = require('react');
import Portal from '../portal';
import Button from '../button';
import Content from '../content';
import Heading from '../heading';
import Link from '../link';
import Text from '../text';

const [portalLocation, setPortalLocation] = useState();
const [portalActive, setPortalActive] = useState(false);
const renderIn = target => {
setPortalLocation(target);
setPortalActive(!!target);
};

const location = {
'ex-sibling': 'the sibling div',
'ex-overview': 'the Overview section',
'ex-parent': 'the parent',
};
<>
<div id="rc-portal-ex-parent" style={{ color: 'blue' }}>
<div
id="rc-portal-ex-sibling"
style={{
color: 'MintCream',
padding: '8px',
backgroundColor: 'LightSlateGrey',
marginBottom: '8px',
}}
>
Sibling Element
</div>
<div style={{ color: 'red' }}>
<Portal target={portalLocation} active={portalActive}>
<h3>
{portalLocation
? `I'm rendering in ${location[portalLocation]}!`
: `I'm not rendering in a portal `}
</h3>
</Portal>
</div>
</div>
<Button onClick={() => renderIn('ex-sibling')}>Render in sibling</Button>
<Button onClick={() => renderIn('ex-overview')}>Render in Overview</Button>
<Button onClick={() => renderIn('ex-parent')}>Render in parent</Button>
<Button onClick={() => renderIn()}>Deactivate portal</Button>
</>;
```

## Variations

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.

```jsx
const { useState } = require('react');
import Button from '../button';
import Portal from '../portal';

const [showMenu, setShowMenu] = useState(false);
const [showMore, setShowMore] = useState(false);

const menuStyle = {
backgroundColor: 'lightSlateGrey',
borderRadius: '4px',
color: 'mintCream',
height: 'fit-content',
width: '90%',
position: 'absolute',
top: '25px',
left: '5%',
zIndex: '100',
textAlign: 'center',
};
<>
<Button onClick={() => setShowMenu(!showMenu)}>
{showMenu ? 'Close' : 'Render in'} menu
</Button>
<Portal active={showMenu} style={menuStyle} className="test">
<h3>I'm some menu content</h3>
</Portal>

<Button onClick={() => setShowMore(!showMore)}>
{showMenu && showMore ? 'Hide' : 'Show'} more content
</Button>
<Portal>{showMenu && showMore && <h3>I'm more content</h3>}</Portal>
</>;
```

## Related

- [TooltipHoverArea](#/React%20Components/TooltipHoverArea)
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Portal from './portal';

export default Portal;
46 changes: 41 additions & 5 deletions packages/react-components/source/react/library/portal/portal.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,62 @@
import React, { useEffect } from 'react';
import { createPortal } from 'react-dom';
import PropTypes from 'prop-types';

const propTypes = {
target: PropTypes.oneOf(['tooltip']),
/** 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-` */
target: PropTypes.string,
/** Boolean value used to conditionally render content in target div. If false, it will render the content at its current location in the dom */
active: PropTypes.bool,
/** Optional additional className to apply to portal div */
className: PropTypes.string,
/** Optional inline styles to apply to portal div */
style: PropTypes.shape({}),
/** Content to render in portal */
children: PropTypes.node,
};
const defaultProps = {
target: 'default',
active: true,
className: '',
style: {},
children: null,
};

const Portal = ({ children, target = 'tooltip' }) => {
const Portal = ({ children, target, active, style, className }) => {
// portal target with fallbacks
const root =
document.getElementsByClassName('app')[0] ||
document.getElementById('root') ||
document.body;

const portalId = `portal-${target}`;
const portalId = `rc-portal-${target}`;
let portal = document.getElementById(portalId);

if (!portal && root) {
if (!portal && root && target) {
portal = document.createElement('div');
portal.id = portalId;

// TODO: add option to prepend
root.appendChild(portal);
}
return createPortal(children, portal);

// Apply classes and styles to portal div
if (className) portal.className = className;
if (style) Object.assign(portal.style, style);

// Remove portal on unmount
useEffect(() => {
return () => {
const p = document.getElementById(portalId);
if (p) p.remove();
};
}, []);

// Remove portal if not active
if (!active && portal) portal.remove();

return <>{active && target ? createPortal(children, portal) : children}</>;
};
Portal.propTypes = propTypes;
Portal.defaultProps = defaultProps;
export default Portal;
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { usePopper } from 'react-popper';
import Portal from '../portal/portal';
import Portal from '../portal';

export const propTypes = {
/** Position of tooltip relative to the activating element */
Expand Down Expand Up @@ -128,7 +128,7 @@ const TooltipHoverArea = ({
return (
<>
{!!children && !!tooltip && (
<Portal>
<Portal target="tooltip">
{/* eslint-disable-next-line */}
<div
id={tooltipId}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ const [modalIsDisabled, setDisabled] = useState(false);
anchor="right"
>
<Button onClick={() => setDisabled(!modalIsDisabled)}>
{`Click me to ${modalIsDisabled ? 'disable' : 'enable'} tooltip`}
{`Click me to ${!modalIsDisabled ? 'disable' : 'enable'} tooltip`}
</Button>
</TooltipHoverArea>
</div>;
Expand Down