Skip to content

Commit

Permalink
stub: modal-dialog improvements
Browse files Browse the repository at this point in the history
- feat: modal transitions on open/close
  • Loading branch information
pongstr committed Dec 25, 2024
1 parent e5ab3b5 commit 3005927
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 0 deletions.
126 changes: 126 additions & 0 deletions src/modal/Modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { X } from "lucide-react";
import {
FC,
PropsWithChildren,
useContext,
useEffect,
useRef,
forwardRef,
useId,
} from "react";
import { ModalContext, ModalContextType } from "./ModalContext";

export const Modal: FC<PropsWithChildren<Omit<ModalContextType, "id">>> = ({
children,
onEscapeClose = true,
onBackdropClose = true,
open,
onOpenChange,
}) => {
const dialogId = useId();
const dialogRef = useRef<HTMLDialogElement>(null);

useEffect(() => {
const dialog = dialogRef.current!;

if (open) {
dialog.showModal();
requestAnimationFrame(() => {
dialog.dataset.open = "";
delete dialog.dataset.closing;
});

const handleEscape = (e: KeyboardEvent) => {
if (!onEscapeClose) return;
if (e.key === "Escape") {
e.preventDefault();
onOpenChange(false);
}
};

window.addEventListener("keydown", handleEscape);
return () => void window.removeEventListener("keydown", handleEscape);
} else {
dialog.dataset.closing = "";
delete dialog.dataset.open;

const backdrop = dialog.firstElementChild as HTMLElement;
const content = backdrop?.firstElementChild as HTMLElement;

let transitionsComplete = 0;

const handleTransitionEnd = () => {
// Increment counter to track when both backdrop and content transitions are complete
transitionsComplete++;

// If both transitions are completee the dialog
if (transitionsComplete >= 2) {
dialog.close();
backdrop?.removeEventListener("transitionend", handleTransitionEnd);
content?.removeEventListener("transitionend", handleTransitionEnd);
}
};

backdrop?.addEventListener("transitionend", handleTransitionEnd);
content?.addEventListener("transitionend", handleTransitionEnd);

return () => {
backdrop.removeEventListener("transitionend", handleTransitionEnd);
content?.removeEventListener("transitionend", handleTransitionEnd);
};
}
}, [onEscapeClose, onOpenChange, open]);

const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>) => {
if (!onBackdropClose) return;
if (e.target === e.currentTarget) {
onOpenChange(false);
}
};

return (
<ModalContext.Provider value={{ id: dialogId, open, onOpenChange }}>
<dialog id={dialogId} ref={dialogRef} className="group">
<div className="fixed w-full h-full overflow-y inset-0 grid place-content-center bg-black/30 backdrop-blur-sm opacity-0 transition-all duration-300 ease-in-out group-data-[open]:opacity-100 group-data-[closing]:opacity-0">
<div
className="overflow-y-auto w-screen h-screen place-content-center scale-75 py-10 opacity-0 shadow-lg transition-all duration-300 ease-out group-data-[open]:scale-100 group-data-[open]:opacity-100 group-data-[closing]:scale-75 group-data-[closing]:opacity-0"
onClick={handleBackdropClick}
>
{children}
</div>
</div>
</dialog>
</ModalContext.Provider>
);
};

type ModalContentProps = PropsWithChildren<{
className?: string;
showCloseButton?: boolean;
}>;

export const ModalContent = forwardRef<HTMLDivElement, ModalContentProps>(
function ModalContent({ children, className, showCloseButton = true }, ref) {
const { onOpenChange } = useContext(ModalContext);
return (
<div
ref={ref}
className={[
"relative m-auto rounded-lg bg-base-100 text-base-content min-w-96 p-4",
className,
].join(" ")}
>
{children}
{showCloseButton && (
<button
className="absolute top-3.5 right-3.5 text-base-content opacity-50 hover:opacity-100 transition-opacity duration-300"
onClick={() => onOpenChange(false)}
>
<span className="sr-only">Close</span>
<X className="size-4" />
</button>
)}
</div>
);
},
);
14 changes: 14 additions & 0 deletions src/modal/ModalContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from "react";
import { Dispatch, PropsWithChildren, SetStateAction } from "react";

export type ModalContextType = PropsWithChildren<{
id: string;
open: boolean;
onEscapeClose?: boolean;
onBackdropClose?: boolean;
onOpenChange: (open: boolean) => void | Dispatch<SetStateAction<boolean>>;
}>;

export const ModalContext = React.createContext<ModalContextType>(
{} as ModalContextType,
);
6 changes: 6 additions & 0 deletions src/modal/useModalRef.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { ModalContext, ModalContextType } from "./ModalContext";
import { useContext } from "react";

export function useModalRef(): ModalContextType {
return useContext(ModalContext);
}

0 comments on commit 3005927

Please sign in to comment.