Created
December 6, 2021 08:59
-
-
Save audunolsen/d4aadcfc4e1a1b7d7cd6ff7522b86d71 to your computer and use it in GitHub Desktop.
usePortalV2.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Benefits over v1: | |
1. No intermidiate portal div. The portal is the immidiate descendant of the parent/mounting div | |
2. Portal element can be rendered as any valid React.ElementType | |
3. Portal can be given ref | |
4. Portal can be given a className and any other valid html attribute | |
5. Portal supports fully reactive classes for both body and parent divs. (Useful for locking scroll etc…) | |
6. More flexible mounting options. can e.g. list many fallbacks. Mount will update on subsequent portal openings if dom changes | |
*/ | |
import React, { | |
useEffect, | |
useCallback, | |
useMemo, | |
createElement, | |
forwardRef, | |
} from "react"; | |
import { createPortal } from "react-dom"; | |
import useBool from "./useBool"; | |
import usePrevious from "./usePrevious"; | |
interface Options { | |
parent?: HTMLElement | string | string[]; | |
open?: boolean; | |
} | |
interface Return { | |
isOpen: boolean; | |
openPortal: () => void; | |
closePortal: () => void; | |
Portal: React.ElementType; | |
} | |
interface PortalProps extends React.HTMLAttributes<HTMLElement> { | |
children: React.ReactNode; | |
tag?: React.ElementType; | |
parentClassName?: string; | |
bodyClassName?: string; | |
// --- ignore, used internally --- | |
mount: Element; | |
isOpen: boolean; | |
} | |
const Portal = forwardRef<HTMLElement, PortalProps>(function Portal( | |
{ tag, children, parentClassName, bodyClassName, mount, isOpen, ...props }, | |
ref, | |
) { | |
const prevBodyClass = usePrevious(parentClassName); | |
const prevParentClass = usePrevious(bodyClassName); | |
const classMap = { | |
...(bodyClassName && { [bodyClassName]: document.body }), | |
...(parentClassName && { [parentClassName]: mount }), | |
}; | |
const toggleClasses = () => { | |
for (const [classes, el] of Object.entries(classMap)) | |
for (const cls of classes.split(" ")) el.classList.toggle(cls, isOpen); | |
}; | |
const removeClasses = () => { | |
for (const [classes, el] of Object.entries(classMap)) | |
el.classList.remove(...classes.split(" ")); | |
}; | |
useEffect(() => { | |
const prevClassMap = { | |
...(prevBodyClass && { [prevBodyClass]: document.body }), | |
...(prevParentClass && { [prevParentClass]: mount }), | |
}; | |
for (const [cls, el] of Object.entries(prevClassMap)) | |
el.classList.remove(...cls.split(" ")); | |
toggleClasses(); | |
}, [prevBodyClass, prevParentClass]); | |
useEffect(() => { | |
return removeClasses; | |
}, []); | |
toggleClasses(); | |
if (!isOpen) return null; | |
return createPortal( | |
createElement(tag ?? "div", { ...props, ref }, children), | |
mount, | |
); | |
}); | |
export default function usePortal({ | |
parent, | |
open: defaultOpen, | |
}: Options = {}): Return { | |
const [isOpen, openPortal, closePortal] = useBool(!!defaultOpen); | |
const mount = useMemo(() => { | |
if (Array.isArray(parent) || typeof parent === "string") { | |
const selectors = [parent].flat(); | |
for (const s of selectors) { | |
const el = document.querySelector(s); | |
if (el) return el; | |
} | |
} | |
return parent instanceof Element ? parent : document.body; | |
}, [parent, isOpen]); | |
const PortalWithStateProps = forwardRef<HTMLElement, PortalProps>( | |
function PortalWithState(props, ref) { | |
return createElement(Portal, { ...props, isOpen, mount, ref }); | |
}, | |
); | |
return { | |
isOpen, | |
openPortal, | |
closePortal, | |
Portal: useCallback(PortalWithStateProps, [isOpen]), | |
}; | |
} | |
// Usage ––––––––––––––––––––––––– | |
const { Portal, openPortal, closePortal, isOpen } = usePortal(); | |
// … | |
<Portal ref={portalRef} as="dialog" bodyClassName="lock-body-scroll" className="modal"> | |
<h1>Hello World</h1> | |
</portal> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment