dialog창이 떠 있을때, dialog창 뒤에 있는 컴포넌트들의 키입력이 먹혀서 의도치않은 UX가 되어버리는 경우가 많이 발생했다.
이를 막기 위해 아래처럼 훅을 만들었다.
이왕 키입력을 막는 김에, 탭키로 탭인덱스를 이동하지 못하도록 수정하였다.
11/22 추가수정
import { MutableRefObject, useEffect, useRef } from 'react';
import Const from 'constant/Const';
/**
* @사용법 const elRef = useKeyboardTrap(); 선언 후, wrapper 엘리먼트에 ref={elRef} 추가
* @returns {MutableRefObject<T | null>} elRef
*/
function useKeyboardTrap<T extends HTMLElement = HTMLElement>() {
// 키보드 트랩, 포커스 트랩
const elRef = useRef<T | null>(null);
useEffect(() => {
const handleFocus = (evt: KeyboardEvent) => _handleFocus(evt, elRef);
elRef.current?.addEventListener(
Const.EVENT_TYPE.KEYDOWN,
handleFocus as EventListener
);
document.addEventListener(Const.EVENT_TYPE.KEYUP, preventEvt);
document.addEventListener(Const.EVENT_TYPE.KEYDOWN, preventEvt);
return () => {
elRef.current?.removeEventListener(
Const.EVENT_TYPE.KEYDOWN,
handleFocus as EventListener
);
document.removeEventListener(Const.EVENT_TYPE.KEYUP, preventEvt);
document.removeEventListener(Const.EVENT_TYPE.KEYDOWN, preventEvt);
};
}, []);
return elRef;
}
export default useKeyboardTrap;
// funcs
/**
* Handles focus for keyboard navigation within focusable elements.
* @param {KeyboardEvent} evt - The keyboard event object.
* @param {MutableRefObject<HTMLElement | null>} elRef - Reference to the HTMLElement.
*/
function _handleFocus(
evt: KeyboardEvent,
elRef: MutableRefObject<HTMLElement | null>
) {
const TAB_KEY = 'Tab';
const focusableElementSelector = elRef.current?.querySelectorAll<HTMLElement>(
'a[href]:not([disabled]), area[href]:not([disabled]), button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), iframe, summary, details, video[controls], audio[controls], [contenteditable=""], [contenteditable="true"], [tabindex]:not([tabindex^="-"])'
);
if (!focusableElementSelector || !elRef.current) return;
const firstFocusableEl = focusableElementSelector[0];
const lastFocusableEl =
focusableElementSelector[focusableElementSelector.length - 1];
const isTabPressed = evt.key === TAB_KEY;
// Tab key
if (isTabPressed) {
if (evt.shiftKey) {
// Shift + Tab
if (document.activeElement !== firstFocusableEl) return;
lastFocusableEl?.focus();
evt.preventDefault();
} else {
// Tab
if (document.activeElement !== lastFocusableEl) return;
firstFocusableEl.focus();
evt.preventDefault();
}
}
}
function preventEvt(evt: Event) {
// 다른 컴포넌트에서 입력된 이벤트를 막기 위해 사용
evt.stopPropagation();
evt.stopImmediatePropagation();
}
이렇게 포커스 트랩과, 키보드 트랩을 동시에할 수 있는 훅을 사용하면된다.
사용하고 싶은 곳에서 const elRef = useKeyboardTrap()
을 선언한 후, Wrapper 컴포넌트에 ref={elRef} 를 주면 된다.
여기서 중요한 점은,
elRef에 이벤트를 달아줄 때, keyup을 하면 절대 안된다는 것이다.
down했을 때 이벤트를 읽고, 현재 포커스되어 있는 상태에서 다음 위치를 정하는 로직이기 때문이다.