접근가능한레이어팝업 Feat. WAI-ARIA 콘텐츠연합플랫폼클라이언트개발부지성봉
Modal Window
Modal Window 사용자인터페이스디자인개념에서자식윈도에서부모윈도로돌아가기전에사용자의상호동작을요구하는창. 응용프로그램의메인창의작업흐름을방해한다.
Native HTML 의한계점 팝업이떴다라는정보를인지할수없다. 팝업이외의문서정보에접근이된다. 키보드 tab 키운용이팝업을벗어난다. 키보드트랩문제 (IE8 ~ 10)
Requirement 팝업이열렸을때팝업내용인식가능팝업아래의 Windows는비활성화 tab키운용이팝업내부에서만순환팝업이닫혔을때초점이원래있던곳으로반환
How To?
Step 1. 콘텐츠역할정의 role="dialog" 사용자가정보를입력하거나응답할것을요구하도록유도하기위해 어플리케이션의현재처리를중단시키도록설계된어플리케이션윈도우
<div class="popup-wrap"> <div class="popup-body" r o l e ="dialog" a r i a - l a b e l l e d b y=" p o p - t i t l e "> <strong i d="pop-title" class="modal-header"> 접근가능한레이어팝업 < /strong> <div class="modal-body"> <p> 접근가능한레이어팝업이란? < /p>...
Step 2. 팝업이열릴때내용인식 대화상자내부로초점이동 모든상황에서초점은대화상자안에있는요소로이동 첫번째초점을얻을수있는요소로이동하는것이기본 첫번째포커스가능한요소로초점을이동시키는것이 콘텐츠의시작부분을스크롤밖으로밀어낼경우 대화상자안에초점을받을수있는요소가없을경우 정적요소에 tabindex="-1" 을추가하여이요소로초점이동
<div class="popup-wrap"> <div class="popup-body" role="dialog" aria-labelledby="pop-title"> <a class="placeholder" tabindex="-1"></a> <strong id="pop-title" class="modal-header"> 접근가능한레이어팝업 < /strong> <div id="popup-contents" class="modal-body"> <p> 접근가능한레이어팝업이란? < /p>
f u n c t i o n openpopup () { v a r popupbody = document.queryselector(".popup-body"); d o c u m e n t.documentelement.classlist.add("on-popup"); p o p u p B o d y.queryselector(".placeholder").focus();
Step 3. 팝업아래 Windows 비활성화 Browse mode 진입차단 대화상자내에서 tab 이동순환
Browse Mode 진입차단 role="dialog" 가자동으로처리 단, NVDA 2017.4+, NVDA 2017.2 + FireFox, JAWS 18+ 등에서만지원 aria-modal="true" in ARIA 1.1 ios 10.x/10.2 에서는문제가있는것으로리포트됨 ( 대화상자제목과지시사항들이읽는순서에따라접근가능하지않게되는문제발생 )
Browse Mode 진입차단 차선책 : 대화상자외타콘텐츠에 aria-hidden="true" 설정 단, 마크업순서에따라적용이어려워지는상황이발생. 가급적 dialog 요소를 level 1 수준으로위치시키는것이정신건강에좋음
f u n c t i o n setsiblingshidden(currelem){ v a r ommits = ["script", "meta", "link", "style", "base"]; f o r(var i = -1, node; node = currelem.parentnode.children[++i];){ i f (node == currelem ommits.indexof(node.tagname.tolowercase()) > -1) c o n t i n u e; n o d e.setattribute("aria-hidden", "true"); n o d e.setattribute("data-outside-modal", "true"); f u n c t i o n openpopup(){ v a r popupbody = document.queryselector(".popup-body"); d o c u m e n t.documentelement.classlist.add("on-popup"); s e t S i b l i n g s H i d d e n (document.queryselector(".popup-wrap")); p o p u p B o d y.queryselector(".placeholder").focus();
f u n c t i o n unsetsiblingshidden(currelem){ f o r(var i = -1, n o d e, o u t s i d e s= document.queryselectorall("[data-outside-modal]"); n o d e = outsides[++i]; ) { n o d e.removeattribute("aria-hidden"); n o d e.removeattribute("data-outside-modal"); f u n c t i o n closepopup(event){ e v e n t = event window.event; d o c u m e n t.documentelement.classlist.remove("on-popup"); u n s e t S i b l i n g s H i d d e n ();
대화상자내에서 Tab 이동순환 Tab 키대화상자내다음 tabbable 요소로이동마지막 tabbable 요소에있는경우포커스를대화상자내첫번째 tabbable 요소로이동 SHFIT + Tab 키대화상자내이전 tabbable 요소로이동첫번째 tabbable 요소에있는경우포커스를대화상자내마지막 tabbable 요소로이동
(function(){ v a r focuslock = (function(){ v a r firstelem, lastelem; r e t u r n { s e t F i r s t B t n : function(el){ f i r s t E l e m = el;, s e t L a s t B t n : function(el){ l a s t E l e m = el;, f o c u s l o c k K e y D o w n : function(event){ e v e n t = event window.event; v a r keycode = event.which event.keycode; i f (event.shi Key && keycode === 9 && event.target === firstelem){ e v e n t.preventdefault? event.preventdefault() : event.returnvalue = false; l a s t E l e m.focus(); else if(!event.shi Key && keycode === 9 && event.target === lastelem){ e v e n t.preventdefault? event.preventdefault() : event.returnvalue = false; f i r s t E l e m.focus(); ; ()); w i n d o w.focuslock = window.focuslock focuslock; ());
f u n c t i o n openpopup(){ v a r popupbody = document.queryselector(".popup-body"); d o c u m e n t.documentelement.classlist.add("on-popup"); f o c u s l o c k.setfirstbtn(btnclosepopup); f o c u s l o c k.setlastbtn(btnclosepopup); p o p u p B o d y.addeventlistener("keydown", focuslock.focuslockkeydown); s e t S i b l i n g s H i d d e n (document.queryselector(".popup-wrap")); p o p u p B o d y.queryselector(".placeholder").focus();
f u n c t i o n closepopup(event){ e v e n t = event window.event; v a r popupbody = document.queryselector(".popup-body"); d o c u m e n t.documentelement.classlist.remove("on-popup"); p o p u p B o d y.removeeventlistener("keydown", focuslock.focuslockkeydown); u n s e t S i b l i n g s H i d d e n ();
Step 4. 키보드트랩방지 팝업이열릴때초점이얻어진요소를기억해두었다가팝업이닫힐때해당요소에다시초점이동
(function () { v a r focusedelem = null; v a r btnopenpopup = document.getelementbyid("open-popup"); v a r btnclosepopup = document.getelementbyid("close-popup");... f u n c t i o n openpopup(){ v a r popupbody = document.queryselector(".popup-body"); d o c u m e n t.documentelement.classlist.add("on-popup"); f o c u s e d E l e m = this; f o c u s l o c k.setfirstbtn(btnclosepopup); f o c u s l o c k.setlastbtn(btnclosepopup); p o p u p B o d y.addeventlistener("keydown", focuslock.focuslockkeydown);... f u n c t i o n closepopup(event){ e v e n t = event window.event; v a r popupbody = document.queryselector(".popup-body");... f o c u s e d E l e m.focus(); ());
Step 5. ESC 키를눌렀을때팝업닫기
f u n c t i o n openpopup(){... d o c u m e n t.addeventlistener("keydown", closepopup); f u n c t i o n closepopup(event){ e v e n t = event window.event; i f (event.type === 'keydown' && event.keycode!== 27){ r e t u r n;... d o c u m e n t.removeeventlistener("keydown", closepopup);
Used ARIA Role, Property Roles/Property dialog aria-label aria-labelledby aria-describe aria-describedby Description 사용자가정보를입력하거나응답할것을요구하도록유도하기위해어플리케이션의현재처리를중단시키도록설계된어플리케이션윈도우해당객체의 label( 이름 ) 을설정해당객체에대한설명추가
Keyboard Interaction Key Tab Shift + Tab Esc Behavior 대화상자내다음 tabbable 요소로초점이동마지막 tabbable 요소에있는경우포커스를대화상자내첫번째 tabbable 요소로이동대화상자내이전 tabbable 요소로초점이동첫번째 tabbable 요소에있는경우포커스를대화상자내마지막 tabbable 요소로이동대화상자닫기
Reference SSB BART group- ARIA Dialog Role https://labs.ssbbartgroup.com/index.php/aria_dialog_role Dialog (Modal) Design Patterns - W3C https://www.w3.org/tr/wai-aria-practices-1.1/#dialog_modal
감사합니다 NIAW AOA Github https://github.com/niawa/aoa publisher@publisher.name