import * as React from 'react' import { createPortal } from 'react-dom' export type PortalProps = { /** Styles to apply to the portal root element. */ style?: React.CSSProperties /** Where the portal is rendered into. */ renderTo?: HTMLElement | string /** The HTML element to create. Defaults to `div`. */ renderTag?: string /** Callback when Portal is updated. */ onUpdate?: () => void /** Returns the rendered node similar to refs. */ getPortalNode?: (HTMLElement) => void } /** Accepts any additional HTML attributes. */ class Portal extends React.Component { static defaultProps = { renderTag: 'div', renderTo: null, } _portalNode: HTMLElement = null _needsUpdate = false _updated = false componentDidMount() { const secondChild = this._getSecondChild() if (secondChild) { this._setupPortal() this._updatePortal() } } componentDidUpdate() { const secondChild = this._getSecondChild() if (secondChild && !this._portalNode) { this._setupPortal() } else if (!secondChild && this._portalNode) { this._destroyPortal() } if (secondChild) { this._updatePortal() } if (this._needsUpdate) { if (this._updated) { if (this.props.onUpdate) { this.props.onUpdate() } this._updated = false this._needsUpdate = false } else { this.forceUpdate() this._updated = true } } } componentWillUnmount() { this._destroyPortal() } _getSecondChild() { return React.Children.toArray(this.props.children)[1] } _getRenderToNode() { const { renderTo } = this.props if (typeof renderTo === 'string') { return document.querySelector(renderTo) } else { return renderTo || document.body } } _setupPortal() { if (typeof document !== 'undefined') { this._portalNode = document.createElement(this.props.renderTag) this._getRenderToNode().appendChild(this._portalNode) if (this.props.getPortalNode) { this.props.getPortalNode(this._portalNode) } } this.forceUpdate() this._needsUpdate = true } _updatePortal() { const { renderTo, renderTag, onUpdate, getPortalNode, style, children, ...restProps } = this.props Object.keys(restProps).forEach((key) => { this._portalNode.setAttribute(key, restProps[key]) }) if (style) { Object.keys(style).forEach((key) => { this._portalNode.style[key] = style[key] }) } } _renderPortal(component) { return createPortal(component, this._portalNode) } _destroyPortal() { if (this._portalNode) { this._portalNode.parentNode.removeChild(this._portalNode) if (this.props.getPortalNode) { this.props.getPortalNode(null) } } this._portalNode = null } render() { const [firstChild, secondChild] = React.Children.toArray( this.props.children ) return this._portalNode ? ( <> {firstChild} {this._renderPortal(secondChild)} ) : ( firstChild ) } } export { Portal }