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
// Modal.tsx
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
// App.tsx
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
// ModalBtn.tsx
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 속성을 설정해줘야하는 번거로움이 있다.

댓글 공유

  • page 1 of 1

loco9939

author.bio


author.job