Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 54 additions & 1 deletion src/components/common/modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,28 @@ import { twMerge } from "tailwind-merge";

import CloseIcon from "@/assets/icon/common/close.svg?react";

const FOCUSABLE_SELECTORS = [
"button:not([disabled])",
"input:not([disabled])",
"select:not([disabled])",
"textarea:not([disabled])",
"a[href]",
'[tabindex]:not([tabindex="-1"])',
].join(", ");
Comment thread
Seojegyeong marked this conversation as resolved.

function getFocusable(container: HTMLElement): HTMLElement[] {
return Array.from(
container.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTORS),
).filter(
(el) =>
el.tabIndex >= 0 &&
el.getClientRects().length > 0 &&
getComputedStyle(el).visibility !== "hidden" &&
!el.closest('[aria-hidden="true"]') &&
!el.closest("[inert]"),
);
}

export interface IModalProps {
isOpen: boolean;
onClose: () => void;
Expand Down Expand Up @@ -53,7 +75,12 @@ function Modal({
useLayoutEffect(() => {
if (!isOpen) return;
previousActiveElement.current = document.activeElement as HTMLElement;
const id = requestAnimationFrame(() => modalRef.current?.focus());
const id = requestAnimationFrame(() => {
const first = modalRef.current
? getFocusable(modalRef.current)[0]
: undefined;
(first ?? modalRef.current)?.focus();
});
return () => cancelAnimationFrame(id);
}, [isOpen]);

Expand All @@ -62,6 +89,31 @@ function Modal({
previousActiveElement.current?.focus();
}, []);

const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.key !== "Tab") return;
const focusable = modalRef.current ? getFocusable(modalRef.current) : [];
if (focusable.length === 0) {
e.preventDefault();
return;
}
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey) {
if (document.activeElement === first) {
e.preventDefault();
last.focus();
}
} else {
if (document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
},
[],
);

Comment thread
Seojegyeong marked this conversation as resolved.
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === "Escape" && isOpen) {
Expand Down Expand Up @@ -149,6 +201,7 @@ function Modal({
}}
transition={{ duration: openMs, ease: easeOut }}
onClick={handleContentClick}
onKeyDown={handleKeyDown}
tabIndex={-1}
>
{title ? (
Expand Down
Loading