createPortal 사용하여 접근성 높인 모달 구현하기
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| import { useEffect } from "react"; import { createPortal } from "react-dom";
function Modal({ onClick }: { onClick: () => void }) { const handleModalClose = (e: KeyboardEvent) => { if (e.key !== "Escape") return; onClick(); }; useEffect(() => { document.body.style.cssText = `position: fixed; top: -${window.scrollY}px`; return () => { const scrollY = document.body.style.top; document.body.style.cssText = `position: ""; top: "";`; window.scrollTo(0, parseInt(scrollY || "0") * -1); }; }, []);
useEffect(() => { document.addEventListener("keyup", handleModalClose); return () => document.removeEventListener("keyup", handleModalClose); }); return createPortal( <div> <article role="dialog" aria-modal="true" className="Dialog"> <header className="Dialog__header"> <h2>React 포털로 연 다이얼로그(with 모달)</h2> </header> <div className="Dialog__body"> <p>여기가 React 앱 밖의 세상인가요?!</p> </div> <footer className="Dialog__footer"> <button type="button" className="closeButton" aria-label="모달 다이얼로그 닫기" title="모달 다이얼로그 닫기" onClick={onClick} > <svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd" > <path d="M12 11.293l10.293-10.293.707.707-10.293 10.293 10.293 10.293-.707.707-10.293-10.293-10.293 10.293-.707-.707 10.293-10.293-10.293-10.293.707-.707 10.293 10.293z" /> </svg> </button> </footer> </article> <div className="Dialog__dim" onClick={onClick}></div> </div>, document.getElementById("modal") as HTMLElement ); }
export default Modal;
|
- createPortal로 root 태그가 아닌 새로운 id가 modal인 태그에 modal을 생성하였다.
- Esc 키를 눌러 모달을 끌 수 있게 하였다.
- 모달이 열려있을 때, 모달 뒤의 배경이 스크롤되지 않도록 하였다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import { useState } from "react"; import "./App.css"; import Modal from "./Modal"; import ModalBtn from "./ModalBtn";
function App() { const [modalOpen, setModalOpen] = useState(false);
const handleModalOpen = () => setModalOpen(true); const handleModalClose = () => setModalOpen(false); return ( <div className="App"> <ModalBtn onClick={handleModalOpen} modalOpen={modalOpen} /> {modalOpen && <Modal onClick={handleModalClose} />} </div> ); }
export default App;
|
- ModalBtn을 클릭하여 모달을 열 수 있게한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| function ModalBtn({ onClick, modalOpen, }: { onClick: () => void; modalOpen: boolean; }) { return ( <div className="box"> <button type="button" className="openDialogButton" aria-haspopup="dialog" aria-label="모달 다이얼로그 열기" title="모달 다이얼로그 열기" onClick={onClick} tabIndex={modalOpen ? -1 : 0} > <svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd" > <path d="M14 4h-13v18h20v-11h1v12h-22v-20h14v1zm10 5h-1v-6.293l-11.646 11.647-.708-.708 11.647-11.646h-6.293v-1h8v8z" /> </svg> </button> </div> ); }
export default ModalBtn;
|
- ModalBtn에 모달 상태를 전달하여 모달이 열려있다면 모달 배경에 포커스가 가지 않도록 하였다.
배경 요소에 모든 focus 요소에 tabIndex 속성을 설정해줘야하는 번거로움이 있다.