Skip to content

Instantly share code, notes, and snippets.

@rphlmr
Last active September 26, 2022 15:21
Show Gist options
  • Save rphlmr/694750bae7491258d591c244f3095245 to your computer and use it in GitHub Desktop.
Save rphlmr/694750bae7491258d591c244f3095245 to your computer and use it in GitHub Desktop.
Remix Route Modal with Tailwind and HeadlessUI
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>
);
}
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>;
}
export function scrollToTop(element?: HTMLElement | null) {
element?.scroll({
top: 0,
left: 0,
behavior: "smooth",
});
}
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>
);
}
import { twMerge } from "tailwind-merge";
export function tw(...args: Parameters<typeof twMerge>) {
return twMerge(...args);
}
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