React app 개발시 모달 component의 접근성을 높이는 방법을 알아보자
개발자도구에서 접근성 확인하는 방법
Elements Tab -> 우측Accessibility Tab
을 활용
모달 컴포넌트
모달 컴포넌트는 UI 디자인 개념에서 모달 다이얼로그(Dialog, 대화상자)로 불리며 사용자와 인터렉션한다.
모달 컴포넌트가 활성화되면 애플리케이션 위에 오버레이(overlay)되며, 다른 부분의 영역과 인터렉션 할 수 없도록 비활성화 처리된다.
role
: 위젯, 구조 및 동작에 대한 의미 정보를 올바르게 전달하기 위해 사용aria-labelledby
: ARIA 속성을 사용해 레이블 역할의 요소와 연결하는 것aria-haspopup
: 스크린 리더 사용자에게 해당 요소가 하위메뉴를 포함하고 있다는(팝업될 수 있다는) 정보를 제공합니다.aria-pressed
: 스크린리더는 사용자에게 해당 요소를 버튼이 아닌 토글버튼이라고 읽어주며, 선택/미선택에 대한 상태 정보도 제공합니다.return {
<OpenButton
aria-haspopup="true"
aria-pressed={this.state.isPressed}
/>
<Modal
role="dialog" // 모달의 역할을 명시
/* 임의의 고유 ID 값을 부여해 제목역할을 수행하는 요소와 연결 */
aria-labelledby="modal-jtw"
>
/* 제목역할을 수행하는 컴포넌트에 동일한 ID값을 설정 */
<Header id="modal-jtw" ... />
</Modal>
}
ARIA 상태 업데이트
ARIA 상태 속성은 컴포넌트가 업데이트 될 때 현재 상태를 변경해야 한다.
aria-pressed
상태 속성 값이 눌러졌을때true
, 눌러지지 않았을 경우false
로 상황에 따라 업데이트 되어야 한다.<a role="button" href="#toggleDialogButton" aria-haspopup="true" aria-pressed={this.state.isPressed} 다이얼로그 보기 </a>
기본으로 모달 컴포넌트가 보이는 상황에서 tab은 모달 바깥으로 활성화된다.
tabIndex
: 요소의 tab
순서를 지정하는 속성, tabIndex = 0
지정시 focus
을 받을 수 없는 h1, div 등과 같은 요소들도 tab
으로 focus
을 받을 수 있도록 처리됨tabIndex = -1
지정시 기본적으로 focus
을 받는 태그가 tab
을 못받도록 처리return (
<Modal
role="dialog" // 모달의 역할을 명시
/* 임의의 고유 ID 값을 부여해 제목역할을 수행하는 요소와 연결 */
aria-labelledby="modal-jtw"
tabIndex="0" // 기본적으로 초점을 받을 수 없는 요소에 focus을 받을 수 있게 처리
>
/* 제목역할을 수행하는 컴포넌트에 동일한 ID값을 설정 */
<Header id="modal-jtw" ... />
</Modal>
)
focus 이동이란?
- 키보드 접근성이란 스크린 리더 사용자가 키보드를 통해 웹 페이지의 정보에 접근하는 것을 의미
- 특히,
focus
이동이란[tab]
키를 누르면 좌측상단부터focus
받는 요소로 부터 이동되고,[shift]
+[tab]
키를 누르면 반대로 올라가는 것을 의미
기본적으로
focus
을 받는 태그
HTML 태그 중 기본적으로focus
을 받는 것들이 있다.
a
,button
,input
,select
,textarea
element
모달을 열었을 때 모달 컴포넌트로 focus
설정하면 모달 내부로 자연스럽게 탐색 가능
ref
를 모달과 연결하여 focus
설정const modalRef = useRef(null);
// 모달 열기 핸들러
const handleOpen = useCallback(() => {
setIsOpen(true);
// 시간차를 두고 포커스 설정
window.setTimeout(() => modalRef.current.ref.current.Focus());
}, []);
return (
<Modal
ref={modalRef} // modalRef 연결
role="dialog" // 모달의 역할을 명시
/* 임의의 고유 ID 값을 부여해 제목역할을 수행하는 요소와 연결 */
aria-labelledby="modal-jtw"
tabIndex="0" // 기본적으로 초점을 받을 수 없는 요소에 focus을 받을 수 있게 처리
>
/* 제목역할을 수행하는 컴포넌트에 동일한 ID값을 설정 */
<Header id="modal-jtw" ... />
</Modal>
)
setTimeout을 사용한 이유는 클래스 컴포넌트의 this.setState와 달리, useState 훅은 콜백 함수 처리 기능이 없기 때문입니다.
모달을 닫고나서 focus
를 모달을 여는 트리거를 갖는 버튼 으로 지정
const openButtonRef = useRef(null);
const handleClose = useCallback(() => {
setIsOpen(false);
// 모달을 닫았을 때 시간차를 두고 openButtonRef의 포커스 이동 설정
window.setTimeout(() => openButtonRef.current.ref.current.focus());
}, []);
return (
<OpenButton
ref={openButtonRef} //openButtonRef 설정
onClick={handleOpen}
>
Open
</OpenButton>
)
모달 내부에 focus
이동 요소들을 순환처리(첫번째 focus
요소와 마지막 focus
요소에 이벤트 연결
키보드 트랩이란?
Modal 컴포넌트가 활성화 된 상태일 동이나, 키보드 포커스가 컴포넌트 외부로 빠져나가지 못하도록 가두는 것을 말합니다. 키보드 사용자가 Tab 키를 눌러 탐색 시, 의도치 않게 Modal 밖으로 벗어나지 못하게 함으로서 접근성을 높이는 것입니다.
const modalRef = useRef(null);
const closeButtonRef = useRef(null);
// 모달 포커스 이동 핸들러
const handleFocusModal = useCallback((e) => {
/* [shift]키를 누르지 않고 [tab}키만 눌럿을 때 */
if(!e.shiftKey && e.keyCode === 9) {
e.preventDefault(); // 기본 이벤트 동작 막기
modalRef.current.ref.current.focus(); // modalRef로 포커스 이동
}
},[]);
const handleFocusCloseButton = useCallback((e) => {
/* [shift]키와 [tab}키를 동시에 눌럿을 때 */
if(e.shiftKey && e.keyCode === 9) {
e.preventDefault(); // 기본 이벤트 동작 막기
closeButtonRef.current.ref.current.focus(); // closeButtonRef로 포커스 이동
}
},[]);
return (
<ModalContent>
<FirstButton
ref={modalRef}
// 첫번째 focuse 이동 요소에 keydown 이벤트(handleFocusCloseButton) 연결
onKeyDown={handleFocusCloseButton}
/>
<FinalButton
ref={closeButtonRef}
// 마지막 focus 이동 요소에 keydown 이벤트(handleFocusModal) 연결
onKeyDown={handleFocusModal}
/>
</ModalContent>
)
모달이 열린 상태일 때 esc를 누르면 모달이 닫혀야함
const handleClose = useCallback(() => {
setIsOpen(false);
// 모달을 닫았을 때 시간차를 두고 openButtonRef의 포커스 이동 설정
window.setTimeout(() => openButtonRef.current.ref.current.focus());
}, []);
useEffect(
() => {
// Esc 키 메뉴 닫기 핸들러(포함)
const handleEscCloseModal = (e) => {
// esc를 누르면(true) handleClose 메소드 실행
e.keyCode === 27 && handleClose();
}
const methodName = isOpen ? 'addEventListener' : 'removeElementListener';
window[methodName]('keydown', handleEscCloseModal);
},
[ isOpen, handleClose ]
);