업데이트:

블로그에 사용할 레이어팝업을 만들면서, 웹 접근성이 충분히 보장되도록 작업을 했고, 코드는 다음과 같다.

See the Pen YzGpLNZ by sel (@selucky) on CodePen.

HTML

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 레이어 열기 버튼 -->
<button type="button" class="open-lp" aria-controls="lp1">open</button>

<!-- 레이어 팝업 -->
<div class="layer-pop" id="lp1" role="dialog" aria-modal="true" aria-labelledby="lp-title">
    <div class="layer-pop__inner">
        <button type="button" aria-label="닫기" class="layer-pop__close">X</button>

        <h2 id="lp-title">레이어 타이틀</h2>
        <a href="#">내용</a>
        <span tabindex="0">내용</span>
    </div>
</div>

role이나 aria- 로 시작하는 속성들에 대해선 WAI-ARIA를 찾아보길 권하며, 마크업에 대한 설명은 딱히 적지 않을 것임. 이 글을 보고 있는 이들 대부분은 많이 알고 있을 것이라 생각하므로(나만 이렇게 생각하나)..

Javascript

Vanilla JS가 아닌 jQuery로 작성하였다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// jQuery 환경에서만 작동할 수 있게 처리
if (typeof jQuery === "undefined") throw new Error("Modal requires jQuery.");

// 레이어 팝업 열기 버튼 클릭 시 팝업 보이기
$(".open-lp").on("click", function() {
    var op = $(this);
    var lp = $("#" + $(this).attr("aria-controls"));
    var lpObj = lp.children(".layer-pop__inner");
    var lpObjClose = lp.find(".layer-pop__close");
    var lpObjTabbable = lpObj.find("button, input:not([type='hidden']), select, iframe, textarea, [href], [tabindex]:not([tabindex='-1'])");
    var lpObjTabbableFirst = lpObjTabbable && lpObjTabbable.first();
    var lpObjTabbableLast = lpObjTabbable && lpObjTabbable.last();
    var lpOuterObjHidden = $(".skip-links, .masthead, .initial-content, .search-content, .page__footer"); // 레이어 바깥 영역의 요소
    var all = $(".masthead, .page__footer").add(lp);
    var tabDisable;
    var nowScrollPos = $(window).scrollTop();
    
    $("body").css("top", - nowScrollPos).addClass("scroll-off").on("scroll touchmove mousewheel", function(event){
        event.preventDefault(); // iOS 레이어 열린 상태에서 body 스크롤되는 문제 방지
    });

    function lpClose() { // 레이어 닫기 함수
        $("body").removeClass("scroll-off").css("top", "").off("scroll touchmove mousewheel");
        $(window).scrollTop(nowScrollPos); // 레이어 닫은 후 화면 최상단으로 이동 방지
        if (tabDisable === true) lpObj.attr("tabindex", "-1");
        all.removeClass("on");
        lpOuterObjHidden.removeAttr("aria-hidden");
        op.focus(); // 레이어 닫은 후 원래 있던 곳으로 초점 이동
        $(document).off("keydown.lp_keydown");
    }

    $(this).blur();
    all.addClass("on");        
    lpOuterObjHidden.attr("aria-hidden", "true"); // 레이어 바깥 영역을 스크린리더가 읽지 않게
    lpObjTabbable.length ? lpObjTabbableFirst.focus().on("keydown", function(event) { 
        // 레이어 열리자마자 초점 받을 수 있는 첫번째 요소로 초점 이동
        if (event.shiftKey && (event.keyCode || event.which) === 9) {
            // Shift + Tab키 : 초점 받을 수 있는 첫번째 요소에서 마지막 요소로 초점 이동
            event.preventDefault();
            lpObjTabbableLast.focus();
        }
    }) : lpObj.attr("tabindex", "0").focus().on("keydown", function(event){
        tabDisable = true;
        if ((event.keyCode || event.which) === 9) event.preventDefault();
        // Tab키 / Shift + Tab키 : 초점 받을 수 있는 요소가 없을 경우 레이어 밖으로 초점 이동 안되게
    });

    lpObjTabbableLast.on("keydown", function(event) {
        if (!event.shiftKey && (event.keyCode || event.which) === 9) {
            // Tab키 : 초점 받을 수 있는 마지막 요소에서 첫번째 요소으로 초점 이동
            event.preventDefault();
            lpObjTabbableFirst.focus();
        }
    });
  
    lpObjClose.on("click", lpClose); // 닫기 버튼 클릭 시 레이어 닫기

    lp.on("click", function(event){
        if (event.target === event.currentTarget) {
            // 반투명 배경 클릭 시 레이어 닫기
            lpClose();
        }
    });
    
    $(document).on("keydown.lp_keydown", function(event) {
        // Esc키 : 레이어 닫기
        var keyType = event.keyCode || event.which;
      
        if (keyType === 27 && lp.hasClass("on")) {
          lpClose();
        }
    });
});

주요 기능

  • 레이어가 열리면 레이어 바깥 영역(최상단 header, 메인콘텐츠, 최하단 footer 등)을 스크린리더가 읽지 않게 처리
  • 레이어가 열리면 초점을 받을 수 있는 요소 중 첫번째 요소로 초점 이동
    • 요소가 없을 경우 타겟 레이어(레이어의 container)로 초점 이동 및 레이어 밖으로 초점 이동이 가지 않게 처리.
  • 초점 받을 수 있는 첫번째 요소에서 Shift + Tab키 누르면 마지막 요소로 초점 이동
  • 초점 받을 수 있는 마지막 요소에서 Tab키 누르면 첫번째 요소로 초점 이동. 즉 초점 이동이 레이어 내부에서만 돌게 처리.
  • 검은 반투명 배경 클릭 시 레이어 닫기
  • Esc키 누르면 레이어 닫기

레이어가 열리면 초점을 받을 수 있는 첫번째 요소가 초점을 받는 게 개인적으로 맞다고 본다.
타겟 레이어가 먼저 초점을 받아야 한다고 한때 생각했었는데, 타겟 레이어에 초점이 잡혀서 레이어에 진입했다는 걸 시각적으로 알려줄 수 있다는 것이다. 이렇게 구현했었으나, 초점을 받을 수 있는 첫번째 요소에 초점 이동이 되게 하여 사용자로 하여금 더 수월하게 레이어 내부 콘텐츠를 탐색할 수 있게끔 하는 게 맞다고 생각을 바꿔먹고 다시 작업하였음.

200624, 기존 테마 삭제하고 새 테마로 교체해서 레이어팝업도 다시 추가 예정(언제쯤..?) 210213, 지금 보니 레이어팝업은 필요가 없다.. -_-

Hero image from danigeza