import { createPortal } from 'react-dom'; import { memo, useLayoutEffect, useRef, forwardRef, useEffect } from 'react'; import { clsm } from '../../utils'; const DEFAULT_PORTAL_OPTIONS = { isAnimated: false, baseContainerClasses: [] }; /** * Creates and/or returns the container to attach the portal to. * * When prevSiblingEl is provided, the portal is rendered immediately * after it in the DOM tree. * - note that prevSiblingEl takes precedence over parentEl */ const initContainer = ({ containerClassname = '', containerId, parentEl = document.body, prevSiblingEl }) => { let container = document.getElementById(containerId); const containerParentEl = container?.parentElement; if (containerParentEl && containerParentEl !== parentEl) { container.remove(); container = null; } if (!container) { container = document.createElement('div'); container.setAttribute('id', containerId); if (containerClassname) container.setAttribute('class', containerClassname); if (prevSiblingEl) prevSiblingEl.after(container); else parentEl.appendChild(container); } else if (containerClassname) { // If the container already exists, update the classnames as they can be passed as props container.setAttribute('class', containerClassname); } return container; }; const withPortal = ( WrappedComponent, containerId, { isAnimated = DEFAULT_PORTAL_OPTIONS.isAnimated, baseContainerClasses = DEFAULT_PORTAL_OPTIONS.baseContainerClasses } = DEFAULT_PORTAL_OPTIONS ) => memo( // eslint-disable-next-line react/prop-types forwardRef( ( { containerClasses, isOpen, parentEl, prevSiblingEl, position, ...props }, ref ) => { const id = useRef(`${containerId}-container`); const container = isOpen ? initContainer({ containerClassname: clsm(baseContainerClasses, containerClasses), containerId: id.current, parentEl, prevSiblingEl }) : null; useLayoutEffect(() => { if (position && container) { for (const attr in position) { const val = position[attr]; if (val) container.style[attr] = `${val}px`; } } }, [container, parentEl, position, prevSiblingEl]); useEffect(() => { if (!isAnimated) { return () => container?.remove(); } }, [container]); return ( container && createPortal( , container ) ); } ) ); export default withPortal;