Last active
          September 26, 2022 15:21 
        
      - 
      
- 
        Save rphlmr/694750bae7491258d591c244f3095245 to your computer and use it in GitHub Desktop. 
    Remix Route Modal with Tailwind and HeadlessUI
  
        
  
    
      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
    
  
  
    
  | import { HashtagIcon, XMarkIcon } from "@heroicons/react/24/outline"; | |
| import type { LoaderArgs, MetaFunction } from "@remix-run/node"; | |
| import { metaTitle } from "@helpers"; | |
| import { useGetRootPath } from "@hooks"; | |
| import { | |
| IconLink, | |
| Modal, | |
| ModalContent, | |
| ModalHeader, | |
| Title, | |
| } from "@components"; | |
| export const meta: MetaFunction = () => ({ | |
| title: "title", | |
| }); | |
| export function loader({ request }: LoaderArgs) { | |
| return null; // do what you want, it's a classic Route! | |
| } | |
| export default function PublicationWizardScreen() { | |
| const rootPath = useGetRootPath(); | |
| return ( | |
| <Modal> | |
| <ModalHeader | |
| title={ | |
| <Title variant="h2" leftIcon={<HashtagIcon />}> | |
| My title | |
| </Title> | |
| } | |
| closeButton={ | |
| <IconLink | |
| icon={<XMarkIcon />} | |
| to={rootPath} | |
| state={{ scroll: false }} | |
| /> | |
| } | |
| /> | |
| <ModalContent> | |
| // content here | |
| // First child receive a ref to link to. This ref is for scroll to top on Modal title click | |
| // If you put a component here, remember to forwardRef ;) | |
| // If it's a regular div, ref is auto linked | |
| </ModalContent> | |
| </Modal> | |
| ); | |
| } | 
  
    
      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
    
  
  
    
  | import React, { | |
| cloneElement, | |
| isValidElement, | |
| createContext, | |
| useContext, | |
| useMemo, | |
| useRef, | |
| } from "react"; | |
| import { Dialog } from "@headlessui/react"; | |
| import { scrollToTop, tw } from "@utils"; | |
| import { Title } from "@components"; | |
| type ModalContextValue = { contentRef: React.RefObject<HTMLDivElement> }; | |
| const ModalContext = createContext<ModalContextValue | undefined>(undefined); | |
| function ModalProvider({ children }: { children: React.ReactNode }) { | |
| const contentRef = useRef<HTMLDivElement>(null); | |
| const value = useMemo(() => ({ contentRef }), []); | |
| return ( | |
| <ModalContext.Provider value={value}>{children}</ModalContext.Provider> | |
| ); | |
| } | |
| function useModalContext() { | |
| const context = useContext(ModalContext); | |
| if (!context) { | |
| throw new Error("useModalContext must be used within a ModalProvider"); | |
| } | |
| return context; | |
| } | |
| export function Modal({ children }: { children: React.ReactNode }) { | |
| return ( | |
| <Dialog onClose={() => null} open> | |
| <div className="fixed inset-0 bg-black/25" /> | |
| <div className="fixed inset-0 flex justify-center lg:items-center lg:rounded-3xl lg:py-4"> | |
| <Dialog.Panel className="h-full w-full space-y-3 bg-neutral-50 p-3 lg:max-h-full lg:max-w-3xl lg:rounded-2xl"> | |
| <div className={tw("flex h-full flex-col space-y-3 overflow-hidden")}> | |
| <ModalProvider>{children}</ModalProvider> | |
| </div> | |
| </Dialog.Panel> | |
| </div> | |
| </Dialog> | |
| ); | |
| } | |
| export function ModalHeader({ | |
| closeButton, | |
| title, | |
| subTitle, | |
| }: { | |
| closeButton?: React.ReactNode; | |
| title: React.ReactNode; | |
| subTitle?: React.ReactNode; | |
| }) { | |
| const { contentRef } = useModalContext(); | |
| return ( | |
| <div | |
| className={tw("flex cursor-pointer flex-col")} | |
| onClick={() => scrollToTop(contentRef.current)} | |
| > | |
| <div className={tw("flex items-center justify-between")}> | |
| {typeof title === "string" ? ( | |
| <Title variant="h2">{title}</Title> | |
| ) : ( | |
| title | |
| )} | |
| {closeButton} | |
| </div> | |
| {subTitle} | |
| </div> | |
| ); | |
| } | |
| export function ModalContent({ children }: { children: React.ReactNode }) { | |
| const { contentRef } = useModalContext(); | |
| return ( | |
| <> | |
| {isValidElement(children) | |
| ? cloneElement(children, { | |
| ref: contentRef, | |
| ...children.props, | |
| }) | |
| : children} | |
| </> | |
| ); | |
| } | |
| export function ModalFooter({ children }: { children: React.ReactNode }) { | |
| return <div>{children}</div>; | |
| } | 
  
    
      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
    
  
  
    
  | export function scrollToTop(element?: HTMLElement | null) { | |
| element?.scroll({ | |
| top: 0, | |
| left: 0, | |
| behavior: "smooth", | |
| }); | |
| } | 
  
    
      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
    
  
  
    
  | import { cloneElement } from "react"; | |
| import { tw } from "@utils"; | |
| export function Title({ | |
| children, | |
| leftIcon, | |
| variant, | |
| noTruncate = false, | |
| textCenter: center = false, | |
| }: { | |
| children: string; | |
| leftIcon?: React.ReactElement<{ className?: string }>; | |
| variant: "h1" | "h2"; | |
| noTruncate?: boolean; | |
| textCenter?: boolean; | |
| }) { | |
| return ( | |
| <div className="inline-flex items-center space-x-1 overflow-hidden"> | |
| {leftIcon | |
| ? cloneElement(leftIcon, { | |
| className: tw("h-5 w-5 shrink-0 stroke-2 lg:h-6 lg:w-6"), | |
| }) | |
| : null} | |
| <span | |
| className={tw( | |
| "font-bold", | |
| noTruncate ? "" : "truncate", | |
| center && "w-full text-center", | |
| variant === "h1" && "text-2xl lg:text-3xl", | |
| variant === "h2" && "text-lg lg:text-xl" | |
| )} | |
| > | |
| {children} | |
| </span> | |
| </div> | |
| ); | |
| } | 
  
    
      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
    
  
  
    
  | import { twMerge } from "tailwind-merge"; | |
| export function tw(...args: Parameters<typeof twMerge>) { | |
| return twMerge(...args); | |
| } | 
  
    
      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
    
  
  
    
  | import { useLocation } from "@remix-run/react"; | |
| import { isEmpty } from "@utils"; | |
| export function useGetRootPath() { | |
| const location = useLocation(); | |
| const rootPath = | |
| location.pathname.split("/").filter((path) => !isEmpty(path))[0] || ""; | |
| return `/${rootPath}`; | |
| } | 
  
    Sign up for free
    to join this conversation on GitHub.
    Already have an account?
    Sign in to comment