ํฌ์ปค์ค ํธ๋ฉ (Focus Trap) ๐
TOC
๊ตฌ๊ธ๋ง ํด๋ณด๋ฉด ์๋์ ๊ฐ์ ์ค๋ช ์ ๋ณด์ฌ์ค๋ค.
๋ชจ๋ฌ ์ปดํฌ๋ํธ๊ฐ ์ด๋ฆฐ ์ํ์์ ํค๋ณด๋ ํฌ์ปค์ค๊ฐ ๋ชจ๋ฌ ์ธ๋ถ(๋ชจ๋ฌ ์ปดํฌ๋ํธ๊ฐ ์๋ ๋ชจ๋ฌ ๋ค์ ์์๋ค)๋ก ๋น ์ ธ๋๊ฐ์ง ๋ชปํ๋๋ก ๊ฐ๋๋ ๊ฒ์ ํฌ์ปค์ค ํธ๋ฉ์ด๋ผ๊ณ ๋ถ๋ฆ ๋๋ค.
๋ธ๋ผ์ฐ์ ์์ Tab ํค๋ฅผ ๊ณ์ ๋๋ฅด๋ฉด ๋ฐ๊นฅ์ชฝ ํ์ด์ง input ์์ญ ~ ( ๊ฐ๋ฐ์ ๋๊ตฌ๋ฅผ ์ฐ ์ํ์์ )๊ฐ๋ฐ์ ๋๊ตฌ ์์ญ๊น์ง Tab ํค์ ์ํฅ์ด ๊ฐ๊ฒ๋๋ค.
์ด๋ ์ฌ์ฉ์ ๊ฒฝํ์ ์์ข๊ฒ ํ ์ ์๋ค.
๊ทธ๋ฌ๋ฉด
1. ๋ชจ๋ฌ์ฐฝ์ด ๋์์ง๋ฉด ๋ชจ๋ฌ ๋ด์์๋ง Tab ์ ํจ (๋ง์ง๋ง keyin์์์ ๋ค๋ฌ์ผ๋ฉด ์ฒซ ์์๋ก ํญ&ํฌ์ปค์ค)
2. Tab + Shift๋ฅผ ๋๋ฅด๊ฒ ๋๋ฉด ์ด์ Tab์ ์์๋ก ์์ง์ด๊ธฐ (์ฒซ keyin์์์ ๋ค๋ฌ์ผ๋ฉด ๋ง์ง๋ง ์์๋ก ํญ&ํฌ์ปค์ค)
#๊ตฌํ ํด๋ณด์.
const FocusTrapModal = ({ isOpen, onClose }) => {
const focusTrapArea = useRef(null);
const focusPossibleEles = useRef([]);
const currFocusIdx = useRef(0);
const handleTab = () => {
const currHtml = focusPossibleEles.current[currFocusIdx.current + 1];
if (currHtml !== undefined) {
currHtml.focus();
currFocusIdx.current++;
return;
}
focusPossibleEles.current[0].focus();
currFocusIdx.current = 0;
};
const wrapHandleTab = (e) => {
if (!e.shiftKey && e.key === "Tab") {
e.preventDefault();
handleTab();
}
};
const handleShiftTab = () => {
const currenthtml = focusPossibleEles.current[currFocusIdx.current - 1];
if (currenthtml !== undefined) {
currenthtml.focus();
currFocusIdx.current--;
return;
}
focusPossibleEles.current.at(-1).focus();
currFocusIdx.current = focusPossibleEles.current.length - 1;
};
const wrapHandleShiftTab = (e) => {
if (e.shiftKey && e.key === "Tab") {
e.preventDefault();
handleShiftTab();
}
};
const preventKeyDown = (e) => {
// ํ๊ธ ์ ์ด
if (e.isComposing || e.key === "Backspace") {
return;
}
wrapHandleTab(e);
wrapHandleShiftTab(e);
};
const handleBeforeUnload = (event) => {
const message = "๋ณ๊ฒฝ์ฌํญ์ด ์ ์ฅ๋์ง ์์์ต๋๋ค. ์ ๋ง ๋ ๋์๊ฒ ์ต๋๊น?";
event.returnValue = message;
return message;
};
useEffect(
function functionAnjunghwan() {
if (isOpen) {
focusPossibleEles.current = Array.from(
focusTrapArea.current.children
).filter((val) => val.tabIndex >= 0 && val.disabled !== true);
focusPossibleEles.current[0].focus();
focusTrapArea.current.addEventListener("keydown", preventKeyDown);
window.addEventListener("beforeunload", handleBeforeUnload);
}
return () => {
if (focusTrapArea.current) {
focusTrapArea.current.removeEventListener("keydown", preventKeyDown);
}
window.removeEventListener("beforeunload", handleBeforeUnload);
};
},
[isOpen]
);
const handleOnClick = (e) => {
e.preventDefault();
currFocusIdx.current = e.target.tabIndex;
focusPossibleEles.current[e.target.tabIndex].focus();
};
return (
<div className="modal-overlay">
<div className="modal" ref={focusTrapArea}>
<button className="close-btn" onClick={onClose} tabIndex={-1}>
X
</button>
<h2>๋ชจ๋ฌ ์ฐฝ</h2>
<input
type="text"
placeholder="์ฌ๊ธฐ์ ์
๋ ฅ"
onClick={handleOnClick}
tabIndex={0}
/>
<textarea
placeholder="์ฌ๊ธฐ์ ํ
์คํธ ์
๋ ฅ"
onClick={handleOnClick}
tabIndex={1}
/>
<input
type="text"
placeholder="์ฌ๊ธฐ์ ์
๋ ฅ"
onClick={handleOnClick}
tabIndex={2}
/>
<input
type="text"
placeholder="์ฌ๊ธฐ์ ์
๋ ฅ"
onClick={handleOnClick}
tabIndex={3}
/>
<button onClick={onClose} tabIndex={-1}>
์ ์ฅ
</button>
<button onClick={onClose} tabIndex={-1}>
์ทจ์
</button>
<button disabled onClick={onClose} tabIndex={-1}>
์์์ ์ฅ
</button>
</div>
</div>
);
};
export default FocusTrapModal;
๊ทธ๋ฐ๋ฐ keyin ์์์ ์์ด๋ฅผ keyinํ๊ณ Tabํ๋ฉด ์๋๋๋ฐ
ํ๊ธ์ keyinํ๊ณ Tabํ๋ฉด ๋ง์ง๋ง ๊ธ์๊ฐ ๋ณต์ฌ๋์ด Tab์ดํ์ ๋ค์ keyin์์์ ๋ณต์ ๋๋ ์ํฉ์ด ์๊ฒผ๋ค.
์ด๋ฌํ ๋ฌธ์ ๊ฐ ๋ฐ์ํ์๋๋
if(e.isComposing){
return;
}
์ผ๋ก ๋ง์์ฃผ๋ฉด ๋๋ค.
๊ตฌ๊ธ๋ง ํด๋ณด๋ฉด
ํ๊ธ์ ์์ด์ ๋ฌ๋ฆฌ ์์,๋ชจ์์ผ๋ก ์ด๋ฃจ์ด์ ธ ์๊ธฐ ๋๋ฌธ์ด๋ผ๊ณ ํ๋ค.
์์,๋ชจ์์ ์
๋ ฅํ ๋ IME(Input Method Editor)๊ฐ ์
๋ ฅ์ค(๋ฌธ์ ์กฐํฉ ์ค)์์ ๋ํ๋ด๋ `isComposing`์ํ๊ฐ ๋๋ค๊ณ ํ๋ค.
