Skip to content

Instantly share code, notes, and snippets.

@muhsalaa
Last active September 5, 2024 03:35
Show Gist options
  • Save muhsalaa/38bdc9feccb57bde87f4802e79f4a26f to your computer and use it in GitHub Desktop.
Save muhsalaa/38bdc9feccb57bde87f4802e79f4a26f to your computer and use it in GitHub Desktop.
Creating an accessible modal with RadixUI like API
"use client";
import { X } from "lucide-react";
import React, { useEffect } from "react";
import { createPortal } from "react-dom";
import ReactFocusLock from "react-focus-lock";
export default function AccessibleDialogOrModal() {
return (
<>
<div className="w-full min-h-screen flex flex-col gap-4 items-center justify-center">
<h1 className="text-xl font-bold max-w-sm text-center">
An effort for optimized dialog for accessibility and behavior
</h1>
<Modal>
<ModalTrigger asChild aria-label="Show Modal Parent">
<button
aria-label="Show Modal"
className="px-4 py-2 bg-neutral-900 hover:bg-neutral-800 text-white rounded-md focus:outline outline-2 outline-offset-2 outline-neutral-900"
>
Show Modal
</button>
</ModalTrigger>
<ModalContent title="Modal Title">
<ModalHeader></ModalHeader>
<ModalBody>
<p>Modal Content</p>
</ModalBody>
<ModalFooter>
<ModalTrigger asChild>
<button className="px-4 py-2 bg-neutral-900 hover:bg-neutral-800 text-white rounded-md focus:outline outline-2 outline-offset-2 outline-neutral-900">
Close
</button>
</ModalTrigger>
<button className="px-4 py-2 bg-emerald-900 hover:bg-emerald-800 text-white rounded-md focus:outline outline-2 outline-offset-2 outline-emerald-900">
Confirm
</button>
</ModalFooter>
</ModalContent>
</Modal>
</div>
<div className="h-screen bg-pink-400"> scroller </div>
<div className="h-screen bg-red-400"> scroller </div>
<div className="h-screen bg-cyan-400"> scroller </div>
</>
);
}
const modalContext = React.createContext({
toggleModal: () => {},
isOpen: false,
});
const Modal = ({ children }: { children: React.ReactNode }) => {
const [isOpen, setIsOpen] = React.useState(false);
function toggleModal() {
setIsOpen((c) => !c);
}
return (
<modalContext.Provider value={{ toggleModal, isOpen }}>
{children}
</modalContext.Provider>
);
};
function Slot({ children, ...props }: { children?: React.ReactNode }) {
if (React.Children.count(children) > 1) {
throw new Error("Only one child allowed");
}
if (React.isValidElement(children)) {
return React.cloneElement(children, props);
}
return null;
}
const ModalTrigger = ({
asChild,
...props
}: {
children: React.ReactNode;
asChild?: boolean;
[key: string]: any;
}) => {
const { toggleModal } = React.useContext(modalContext);
const Comp = asChild ? Slot : "button";
return <Comp onClick={toggleModal} data-testid="modal-trigger" {...props} />;
};
const ModalHeader = () => {
const { toggleModal } = React.useContext(modalContext);
return (
<div data-testid="modal-header" className="flex justify-end border-b p-4">
<button
onClick={toggleModal}
className="p-2 hover:bg-neutral-100 border border-neutral-900 rounded-md focus:outline outline-2 outline-offset-2 outline-neutral-900"
>
<span className="sr-only">Dismiss Modal</span>
<X className="size-6" />
</button>
</div>
);
};
const ModalFooter = ({ children }: { children: React.ReactNode }) => {
return (
<div className="flex justify-end items-center gap-4 p-4 border-t">
{children}
</div>
);
};
const ModalBody = ({ children }: { children: React.ReactNode }) => {
return <div className="p-4 h-full">{children}</div>;
};
const ModalContent = ({
children,
title,
}: {
children: React.ReactNode;
title?: string;
}) => {
const { isOpen, toggleModal } = React.useContext(modalContext);
useEffect(() => {
// prevent scrolling and width glitch
const oldWidth = document.body.clientWidth;
if (isOpen) {
document.body.style.overflow = "hidden";
document.body.style.width = oldWidth + "px";
} else {
document.body.style.overflow = "unset";
document.body.style.width = "auto";
}
}, [isOpen]);
// escape close modal
React.useEffect(() => {
function handleKeyDown(event: KeyboardEvent) {
if (event.code === "Escape" && isOpen) {
toggleModal();
}
}
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [toggleModal, isOpen]);
return (
<>
{isOpen &&
createPortal(
<ReactFocusLock returnFocus>
<div
data-testid="modal-container"
className="flex fixed inset-0 bg-neutral-700/70 justify-center sm:items-center items-end"
onClick={toggleModal}
aria-label={title || "A Modal"}
aria-modal
role="dialog"
>
<div
data-testid="modal-dialog"
className="bg-white rounded-md h-2/3 w-full sm:max-w-xl flex flex-col fade-in-up"
onClick={(e) => e.stopPropagation()}
>
{children}
</div>
</div>
</ReactFocusLock>,
document.body
)}
</>
);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment