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