Skip to content

Instantly share code, notes, and snippets.

@crisu83
Last active December 10, 2020 11:54
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save crisu83/b534109128dc1065a7cd3730c47e30d4 to your computer and use it in GitHub Desktop.
Save crisu83/b534109128dc1065a7cd3730c47e30d4 to your computer and use it in GitHub Desktop.
React Portal implementation
import React from 'react';
import {Button} from '@/design-system';
import {useModal} from './modal';
export default function ModalExample() {
const {Modal, closePortal, openPortal} = useModal();
return (
<>
<Button onClick={openPortal}>Open modal</Button>
<Modal>
Hello from the modal!
<Button onClick={closePortal}>Close</Button>
</Modal>
</>
);
}
import {Box} from '@/design-system';
import {isServer} from '@/utils';
import useWindowSize from '@/utils/use-window-size';
import React, {useEffect} from 'react';
import {animated, useSpring} from 'react-spring';
import {PortalConfig, PortalProps, usePortal} from './portal';
export function useModal(config?: PortalConfig) {
const calculateCoords = () =>
!isServer
? {
left: window.innerWidth / 2,
top: window.innerHeight / 2,
}
: undefined;
const {Portal, isOpen, setCoords, ...rest} = usePortal({
closeOnOverlayClick: false,
initialCoords: calculateCoords(),
overlayOpacity: 0.3,
...config,
});
const windowSize = useWindowSize();
useEffect(() => {
setCoords(calculateCoords());
}, [windowSize, setCoords]);
const spring = useSpring({
opacity: isOpen ? 1 : 0,
transform: `translateY(${isOpen ? '0' : '30px'})`,
});
return {
Modal({children, sx, ...props}: PortalProps) {
return (
<Portal sx={{transform: 'translate(-50%, -50%)', ...sx}} {...props}>
<animated.div style={spring}>
<Box
bg="noContrast"
borderRadius="medium"
boxShadow="large"
minWidth="600px"
p={4}
>
{children}
</Box>
</animated.div>
</Portal>
);
},
isOpen,
...rest,
};
}
import React from 'react';
import {Button} from '@/design-system';
import {usePopover} from './popover';
export default function PopoverExample() {
const calculateCoords = (rect: DOMRect) => ({
// Implementation detail
});
const {Popover, openPortal, setCoords} = usePopover();
const handleOpen = (event: any) => {
openPortal();
const rect = (event.target as HTMLElement).getBoundingClientRect();
const coords = calculateCoords(rect);
setCoords(coords);
};
return (
<>
<Button onClick={handleOpen}>Open popover</Button>
<Popover>
Hello from a popover!
</Popover>
</>
);
}
import {Box, BoxPropsWithoutRef} from '@/design-system';
import React, {PropsWithChildren} from 'react';
import {animated, useSpring} from 'react-spring';
import {PortalConfig, usePortal} from './portal';
type PopoverProps = PropsWithChildren<BoxPropsWithoutRef<'div'>>;
export function usePopover(config?: PortalConfig) {
const {Portal, isOpen, ...rest} = usePortal(config);
const spring = useSpring({
opacity: isOpen ? 1 : 0,
transform: `translateY(${isOpen ? '0' : '10px'})`,
});
return {
Popover({children, sx, ...props}: PopoverProps) {
return (
<Portal sx={{position: 'absolute', ...sx}} {...props}>
<animated.div style={spring}>
<Box
bg="noContrast"
borderRadius="small"
boxShadow="small"
minWidth="160px"
overflow="hidden"
>
{children}
</Box>
</animated.div>
</Portal>
);
},
isOpen,
...rest,
};
}
// See: https://blog.logrocket.com/learn-react-portals-by-example/
import {Box, BoxPropsWithoutRef} from '@/design-system';
import {noop, safeDocument} from '@/utils';
import {PropsWithChildren, useLayoutEffect, useState} from 'react';
import React from 'react';
import {createPortal} from 'react-dom';
import {animated, useSpring} from 'react-spring';
const PORTAL_ZINDEX = 1010;
function PortalContainer({
children,
...props
}: PropsWithChildren<BoxPropsWithoutRef<'div'>>) {
return (
<Box fontFamily="body" lineHeight="body" {...props}>
{children}
</Box>
);
}
type PortalOverlayProps = BoxPropsWithoutRef<'div'> & {opacity: number};
function PortalOverlay({onClick, opacity}: PortalOverlayProps) {
const spring = useSpring({
from: {opacity: opacity},
to: {opacity},
});
return (
<animated.div style={spring}>
<Box
onClick={onClick}
sx={{
bg: 'maxContrast',
left: 0,
height: '100%',
position: 'fixed',
top: 0,
width: '100%',
zIndex: PORTAL_ZINDEX - 10,
}}
/>
</animated.div>
);
}
const portalRoot = safeDocument.getElementById('portal-root');
type PortalRootProps = PropsWithChildren<
BoxPropsWithoutRef<'div'> & {
onClose: () => void;
overlayOpacity: number;
enableOverlay: boolean;
closeOnOverlayClick: boolean;
isOpen: boolean;
}
>;
export function PortalRoot({
children,
closeOnOverlayClick,
enableOverlay,
isOpen,
onClose,
overlayOpacity,
sx,
...props
}: PortalRootProps) {
// TODO: Figure out if there is a way to remove this extra `<div>` element
const el = safeDocument.createElement('div');
useLayoutEffect(() => {
portalRoot.appendChild(el);
return () => portalRoot.removeChild(el);
}, [el]);
return isOpen && el
? createPortal(
<>
<PortalContainer
onClick={event => {
event.stopPropagation();
}}
sx={{...sx, zIndex: PORTAL_ZINDEX}}
{...props}
>
{children}
</PortalContainer>
{enableOverlay && (
<PortalOverlay
onClick={closeOnOverlayClick ? onClose : noop}
opacity={overlayOpacity}
/>
)}
</>,
el
)
: null;
}
type Position = number | string;
type Coords = {
bottom?: Position;
left?: Position;
right?: Position;
top?: Position;
};
export type PortalConfig = {
closeOnOverlayClick?: boolean;
enableOverlay?: boolean;
initialCoords?: Coords;
overlayOpacity?: number;
};
export type PortalProps = PropsWithChildren<BoxPropsWithoutRef<'div'>>;
export function usePortal({
closeOnOverlayClick = true,
enableOverlay = true,
initialCoords = {},
overlayOpacity = 0,
}: PortalConfig = {}) {
const [isOpen, setIsOpen] = useState(false);
const [coords, setCoords] = useState<Coords>(initialCoords);
const openPortal = () => setIsOpen(true);
const closePortal = () => setIsOpen(false);
return {
Portal({sx, ...props}: PortalProps) {
return (
<PortalRoot
closeOnOverlayClick={closeOnOverlayClick}
enableOverlay={enableOverlay}
isOpen={isOpen}
onClose={closePortal}
overlayOpacity={overlayOpacity}
sx={{position: 'fixed', ...coords, ...sx}}
{...props}
/>
);
},
closePortal,
isOpen,
openPortal,
setCoords,
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment