ํฌ์ปค์ค ํธ๋ฉ (Focus Trap) ๐
๊ตฌ๊ธ๋ง ํด๋ณด๋ฉด ์๋์ ๊ฐ์ ์ค๋ช ์ ๋ณด์ฌ์ค๋ค.
๋ชจ๋ฌ ์ปดํฌ๋ํธ๊ฐ ์ด๋ฆฐ ์ํ์์ ํค๋ณด๋ ํฌ์ปค์ค๊ฐ ๋ชจ๋ฌ ์ธ๋ถ(๋ชจ๋ฌ ์ปดํฌ๋ํธ๊ฐ ์๋ ๋ชจ๋ฌ ๋ค์ ์์๋ค)๋ก ๋น ์ ธ๋๊ฐ์ง ๋ชปํ๋๋ก ๊ฐ๋๋ ๊ฒ์ ํฌ์ปค์ค ํธ๋ฉ์ด๋ผ๊ณ ๋ถ๋ฆ ๋๋ค.
๋ธ๋ผ์ฐ์ ์์ 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`
์ํ๊ฐ ๋๋ค๊ณ ํ๋ค.