Shameem

June 8, 2023

A custom popup component with react.js, tailwindcss and react transition group

import { useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import { IconClose } from '@/components/icons';
import { Transition } from 'react-transition-group';

// Used to detect clicked inside popup body
function hasSomeParentTheClass(element, classname) {
  if (element && element.classList && element.classList.length && element.classList.value.split(' ').indexOf(classname) >= 0) return true;
  return element.parentNode && element.parentNode !== document && hasSomeParentTheClass(element.parentNode, classname);
}
// Popup tailwind classes, these classes will not change
const defaultClasses = 'pl-popup fixed top-0 left-0 right-0 bottom-0 z-[60] bg-black/30 backdrop-blur-sm transition-opacity duration-800 opacity-0 lg:pr-1';
// Popup tailwind classes for each transition state, these classes will change according to transition state
const transitionStyles = {
  entering: 'opacity-100',
  entered: 'opacity-100',
  exiting: 'opacity-0',
  exited: 'opacity-0'
};
// Popup body tailwind classes for each transition state, these classes will change according to transition state
const transformStyles = {
  entering: 'translate-y-0 md:scale-100',
  entered: 'translate-y-0 md:scale-100',
  exiting: 'translate-y-10 md:scale-90',
  exited: 'translate-y-10 md:scale-90'
};

/*
@params
* size - controls width of the popup body
* open - controls open state of popup
* title - popup title or popup header
* actions - popup action buttons on footer
* onClose - callback function on popup close 
* onOpen - callback function before popup open 
* children - popup body html 
* customScrolling - controls scroll behavior, if false popup header and footer will be static and popup body scoll
*/
export default function Popup({ size, open, title, actions, onClose, onOpen, children, customScrolling }) {
  const [show, setShow] = useState(false);
  const closeHandler = (e) => {
    setShow(false);
    onClose();
  };

  const handleBodyScroll = (status) => {
    // check body scrollbar is visible
    var hasVerticalScrollbar = document.body.scrollHeight > document.documentElement.clientHeight;
    // classes added to body when popup opened to prevent body scroll
    const bodyClasses = hasVerticalScrollbar ? ['overflow-hidden', 'lg:pr-1'] : ['overflow-hidden'];
    if (typeof status !== 'undefined') {
      document.querySelector('body').classList.add(...bodyClasses);
    } else {
    if (document.querySelectorAll('.pl-popup').length === 1) {
      document.querySelector('body').classList.remove(...bodyClasses);
    }
  }
};
// Watch open prop to open and close popup
useEffect(() => {
  setShow(open);
  if (open && onOpen) {
  onOpen();
 }
}, [open]);

// Close popup when click outside popup body 
const clickOutside = (event) => {
  if (!hasSomeParentTheClass(event.target, 'pl-popup-body')) {
  closeHandler();
 }
};

const popupWrap = useRef(null);
// createPortal will append popup to <body>
return createPortal(
  <Transition in={show} timeout={300} unmountOnExit nodeRef={popupWrap} onEnter={handleBodyScroll} onExited={handleBodyScroll}>
   {(state) => (
    <div ref={popupWrap} className={`${defaultClasses} ${transitionStyles[state]}`} onClick={clickOutside}>
      <div className={`${transformStyles[state]} ${size === 'sm' ? 'sm:max-w-[424px]' : 'sm:max-w-[424px]'} flex h-full w-full mx-auto md:py-16 pt-16 transition-transform duration-600`}>
        <div className="mt-auto w-full rounded border border-white/5 bg-secondary-bg pl-popup-body sm:m-auto" ref={popupBody}>
        {title && (
           <div className="flex items-center border-b border-white/5 p-4 leading-6">
             <h2 className="text-xl font-medium sm:text-2xl">{title}</h2>
             <button onClick={closeHandler} className="ml-auto text-2xl transition-colors duration-200 text-secondary-text hover:text-white">
                <IconClose />
             </button>
          </div>
         )}
     <div className={`${customScrolling ? `p-3 sm:pb-0` : `py-3 px-4 sm:pb-0 overflow-y-auto max-h-[calc(100vh-176px)] sm:max-h-[calc(100vh-286px)]`}`}>{children}</div>
    {actions && <div className="sm:p-4">{actions}</div>}
  </div>
</div>
</div>
)}
</Transition>,
document.body
);
}

Top comments
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments