본문으로 바로가기

jQuery skip navigation 플러그인

category Code Lab 2016. 1. 14. 17:00

웹 접근성 강화를 위한 Skip Navigation Plugin Pattern

웹 사이트 제작시 많이 사용되는 스킵 네비게이션에 대해 알아봅니다.



접근성 강화를 위한 스킵 네비게이션을 플러그인화 시켜보고 뒤로가기(shift+tab)을 눌렀을 때에도 올바른 접근을 하도록 구현해 봅니다.



Navi 마크업

일반적인 마크업 구성은 아래와 같습니다.

사용자 콘텐츠에 적합하게 구성하시되 네비 항목의 맵핑만 주의하시면 됩니다.

html
<body>

<div id="skip-menu">
    <a href="#art-5">art-5</a>
    <a href="#art-6">art-6</a>
    <a href="#art-7">art-7</a>
    <a href="#art-8">art-8</a>
    <a href="#art-9">art-9</a>
    <a href="#art-10">art-10</a>
</div>

<div class="container">

    <h1>jQuery.fn.skipNav() 플러그인</h1>

    <article id="art-5">
        <h2><a href="">한글 로렘입숨. 가슴을, 소리.</a></h2>
        <p>한글 로렘입숨. 찬란한, 청춘의, 설레는. 햇볕처럼 설레게 빛나는 설레는 피어나는 해도, 숨소리가, 꽃보다 땀 젊음의.</p>
        <p>꽃처럼, 찬란한. 어디에나 않고 사명으로 실수조차도? 젊음의 청춘아! 피어나는 쉬지 청춘의 소리 고동소리. 어디에나 가슴을, 씩씩하게 소리 보기만 쉬지. 가슴을 땀 청춘아! 설레게 소리 심장, 장엄한. 꽃보다 숨소리가, 하루를 거추장스런 엮어가는 씩씩하게 심장 만들 세상을 하고 찬란한 피어나는 운동화도 청춘아! 사명으로 내일의 흐르는 티셔츠 설레는 너야말로, 해도 젊음의 설레게 눈부신 꽃처럼 소리 않고 청춘의 아름답다 햇볕처럼. 않고 아름답다 그대 해도 눈부신 꽃보다 가슴을 아름다운 어디에나, 운동화도. 쉬지 찬란한 청춘 설레게 땀, 그대? 땀 하루를, 운동화도 실수조차도 씩씩하게, </p>
    </article>

    <article id="art-6">
        <h2><a href="">그대 해도 쿵쿵대는 거추장스런?</a></h2>
        <p> 한글 로렘입숨. 쉬지 장엄한 하루를 설레는 흐르는 꽃처럼 젊음의 아름답다 청춘, 사명으로 실수조차도 그대 땀 쿵쿵대는 소리 청춘의. 다 들리는 심장 설레게 땀, 눈부신, 해도. 하루를 심장 꽃처럼 소리 운동화도 청춘 다 들리는, 내일의 햇볕처럼 보기만 너 실수조차도 젊음의 거추장스런 설레게 해도 만들 숨 죽여도 쉬지 눈부신 꽃보다. 실수조차도 세상을 가슴을, 찬란한 청춘의 씩씩하게 만들 숨소리가 심장 아름답다 설레게 빛나는 운동화도 눈부신 숨 죽여도, 티셔츠 땀 </p>
        <p>고동소리 청춘의 티셔츠, 씩씩하게 어디에나 아름답다 숨소리가, 청춘아! 피어나는 청춘 그대, 너 사명으로 보기만 하고 내일의 </p>
    </article>

    <article id="art-7">
        <h2><a href="">거추장스런 아름다운 어디에나 흐르는.</a></h2>
        <p>흐르는 세상을 소리, 설레게 피어나는. 땀, 설레는 꽃보다 숨 죽여도 세상을 청춘아! 피어나는 고동소리 햇볕처럼 다 들리는 눈부신 꽃처럼 너야말로 찬란한 사명으로 거추장스런 심장 쿵쿵대는 젊음의, 쉬지 소리 청춘의. 찬란한 씩씩하게, 장엄한 어디에나 너야말로 너 흐르는 청춘! 눈부신 젊음의 가슴을 쉬지 씩씩하게, 소리 다 들리는 숨 죽여도 엮어가는 하고 아름답다 사명으로 피어나는 빛나는 해도 쿵쿵대는 하루를 청춘 아름다운 고동소리 세상을 햇볕처럼 설레는 땀 청춘아! 내일의 그대 만들. 눈부신 땀 만들 내일의 가슴을 다 들리는 고동소리, 청춘의 사명으로 해도 세상을</p>
    </article>

    <article id="art-8">
        <h2><a href="">젊음의 청춘아! 거추장스런 숨소리가.</a></h2>
        <p> 한글 로렘입숨. 아름답다 않고 청춘아! 실수조차도 만들 않고 고동소리 장엄한, 찬란한 거추장 씩하게 그대 청춘의 땀 흐르는 사명으로 너야말로 가슴을 청춘 장엄한 실수조차도, 어디에나 찬란한 숨 죽여도! 심장 꽃보다 피어나는 내일의, 운동화도 설레게 사명으로 청춘아!. 찬란한 쿵쿵대는 하루를 땀 장엄한, 보기만 햇볕처럼 피어나는 청춘 실수조차도? 만들 장엄한 소리 아름답다 티셔츠 청춘, 너야말로 설레는 가슴을 엮어가는 하고 쉬지 피어나는 청춘의 하루를 햇볕처럼 않고 운동화도 땀, 찬란한 쿵쿵대는 청춘아!, 어디에나 꽃처럼 꽃보다 너. 세상을 티셔츠 꽃처럼 거추장스런 눈부신 </p>
    </article>
    <article id="art-9">
        <h2><a href="">설레는 찬란한 숨소리가 눈부신.</a></h2>
        <p> 한글 로렘입숨. 씩씩하게 내일의 숨 죽여도, 엄한 하고 해도 찬란한.대는 다 들리는 가슴을 내일의 티셔츠 청춘아! 숨 죽여도 만들 꽃보다? 젊음의 운동화</p>
        <p>꽃처럼 눈부신 그대, 장엄한 흐르는 쉬지</p>
    </article>

    <article id="art-10">
        <h2><a href="">내일의 설레게 가슴을 티셔츠.</a></h2>
        <p>너 보기만 장엄한 다 들리는, 씩씩하게 청춘 숨 죽여도 엮어가는 꽃보다 심장 거추장스런, 땀 어디에나 설레게 해도, 사명으로 티셔츠 찬란한 흐르는 쉬지 쿵쿵대는 내일의 세상을? 보기만, 흐르는 청춘아! 만들 설레게 청춘 빛나는 내일의 않고, 운동화도 청춘의 피어나는 쉬지 아름다운 꽃보다 장엄한 쿵쿵대는 심장 하고 해도 소리 거추장스런 숨소리가 숨 죽여도 엮어가는 설레는 젊음의 다 들리는 고동소리 아름답다. 그대 쉬지 씩씩하게 꽃처럼, 엮어가는 설레는 꽃보다 </p>
    </article>

</div>

이미 아시겠지만 navi 는 문서 내의 <body> 시작태그 바로 밑에 작성합니다.



CSS 구성

여기서는 이미 플러그인을 구현해 놓은 상태로 CSS의 클래스명은 JS를 통해 추가할 것을 가정하여 먼저 작성해 놓은 것입니다.

css
/**
 * Define skip-navigation
 * --------------------------------
 */
.skipNav-container a {
	/*display: inline-block;*/
	text-decoration: none;
	padding: 0.1875rem 0.375rem;
	background: #34C0FF;
	color: #fff;
	text-align: center;
}

.skipNav-container a {
	display: block;
	padding: 0.4rem 0.375rem;
	color: #fff;
	background: #34c0ff;
	font-family: Verdana;
	font-variant: small-caps;
	text-decoration: none;
	text-shadow: 0 0 10px #fff;
}

.skipNav-container a:focus {
	outline: none;
	box-shadow: 0px 2px 3px hsla(0, 0%, 0%, .25);
        background: #e30816;
        background-image: linear-gradient(#e30816, #f65167);
}
.a11y-hidden {
	overflow: hidden;
	clip: rect(0 0 0 0);
	clip: rect(0, 0, 0, 0);
	position: absolute;
	width: 1px;
	height: 1px;
	margin: -1px;
	border: 0;
	padding: 0;
}

.a11y-hidden.focusable:focus,
.a11y-hidden.focusable:active {
	overflow: visible;
	clip: auto;
	position: static;
	width: auto;
	height: auto;
	margin: 0;
}



디자인(설계) 패턴에 따른 플러그인을 단계별로 구성하여 진행합니다.


플러그인 기본 패턴 #1

javascript
/**
 * $().skipNav()의 역할 정의(요구 사항 정리)
 * 내부의 링크 아이템은 3~4개 정도 선으로 봅니다.
 * URL 뒤에 붙는 hash 를 붙이지 않아야 합니다.
 * 즉, 웹 브라우저의 기본 동작을 차단해야 함
 * 뒤로가기 버튼을 적용했을 때, 메모리
 */

// 플러그인 네이밍 설정
var plugin = 'skipNav';

// 플러그인 존재 유무 확인하기
$.fn[plugin] = $.fn[plugin] || function (options, callback) {

    // 플러그인 기본 옵션과 사용자 정의 옵션 병합 설정
    var settings = $.extend({}, $.fn[plugin].defaults, options);

    // 플러그인이 적용된 $() 인스턴스를 참조
    var $this = this;

    // 식별자 class, data- 접두사 속성을 추가
    $this
        // 식별자인 class 를 외부에서 컨트롤할 수 있도록 옵션값으로 제어
        .addClass(settings.container)
        .on('click', 'a', function (e) {
            // 브라우저 기본동작을 차단
            e.preventDefault();

            // 스킵 네비게이션 링크 아이템의 href 속성을 참조
            var path = e.target.getAttribute('href');

            // Destination(이동할 목적지) ID === path
            var $target = $(path);

            // 목적지인 ID 요소에 접근성을 부여하고자 tabindex = 0 속성 및 포커스를 정의
            $target
                    .attr('tabindex', 0)
                    .focus()
                    // 포커스를 잃었을 때 즉, 해당 요소를 떠났을 때 tabindex 재정의
                    .on('blur', function() {
                        $target.attr('tabindex', -1);
                    });

            // 뒤로가기 버튼을 적용했을 때, 메모리(URL 뒤에 붙는 hash를 참조) 설정
            // setting.setHash 값이 참일 때만 아래 코드를 수행하라.
            /*if (settings.setHash) {
                window.location.hash = path;
             }*/

            // 리팩토링
            settings.setHash && (window.location.hash = path);

        });

    /**
     * 플러그인이 적용된 내부의 a 요소는 화면에 감춰진 상태에서 class 속성이 부여되도록 설정하고,
     * 감춰진 a 요소에 포커스를 활성화 시키면 사용자에게 화면에 보여지도록 한다.
     * class 는 옵션으로 설정하여 외부 컨트롤이 가능하도록 한다.(먼저, 플러그인 기본 옵션을 설정)
     */
    $this.find('a')
            .addClass(settings.linkClasses.hidden + ' ' + settings.linkClasses.focusable);

    // callback 함수를 전달하는 경우, 플러그인 실행 후 callback 함수 실행하도록 설정
    if(callback && $.isFunction(callback)) {
        // callback 함수 내부의 this 가 $target 을 참조하도록 설정하고,
        // callbakc 함수 내부에 전달되는 첫번째 인자값을 settings 로 설정한다.
        callback.call($this, settings);
    }

    // 제이쿼리 체이닝을 위해 jQuery 인스턴스 반환 설정
    return $this;

};

// 플러그인에 사용할 기본 옵션 설정
$.fn[plugin].defaults = {
    'container' : 'skipNav-container',
    'linkClasses' : {
        'hidden' : 'a11y-hidden',
        'focusable' : 'focusable'
    },
    'setHash' : true

};

// Usage $.skipNav()
// skipNav()를 적용할 컨테이너 요소 선택자 전달
$('#skip-menu').skipNav({
    'setHash' : !false
});


Skip Navigation Plugin DEMO #1




플러그인 생성자 함수 패턴 #2

javascript
// 플러그인 이름 정의
var skipNavPlugin = 'skipNav';

var _skipNav = {
    // 초기화 수행
    'init' : function ($el, options, callback) {
        this.$el = $el.eq(0);
        this.$links = this.$el.find('a');
        this.settings = $.extend({}, $.fn[skipNavPlugin].defaults, options);
        this.controls();
        this.events();

        return this; // _skipNav 객체를 반환
    }
    , 'controls' : function () {
        this.$el.addClass(this.settings.containerClass);
        this.$links.addClass(this.settings.linkClasses.hidden + ' ' + this.settings.linkClasses.focusable)
    }
    , 'events' : function () {
        this.$el.on('click', 'a', $.proxy(this.linkAction, this))
    }
    , 'linkAction' : function (e) {
        e.preventDefault();
        var path = e.target.getAttribute('href');
        var $target = $(path);
        $target
                .attr('tabindex', 0)
                .focus()
                .on('blur', $.proxy(this.resetTabindex, $target));

        this.settings.setHash && (window.location.hash = path);
    }
    , 'resetTabindex' : function () {
        this.attr('tabindex', -1)
    }


};

$.fn[skipNavPlugin] =  $.fn[skipNavPlugin] || function (options, callback) {
    // SkipNav 객체 초기화 수행
    var __skipNav = _skipNav.init(this, options, callback);
    this.data('_skipNav', __skipNav);

    // 제이쿼리 체이닝 설정
    return this;
};

$.fn[skipNavPlugin].defaults = {
    'containerClass' : 'skipNav-container',
    'linkClasses' : {
        'hidden' : 'a11y-hidden',
        'focusable' : 'focusable'
    },
    'setHash' : !true
};


$('#skip-menu').skipNav({
    'setHash' : !false
});

// console.log($('#skip-menu').data());


Skip Navigation Plugin DEMO #2




플러그인 프로토타입 패턴 #3

javascript
var pluginName = 'skipNav';

/**
 * 플러그인 객체 생성자(plugin constructor)
 * --------------------------------------- */
function SkipNav($el, options) {
    // 플러그인 적용 대상 객체 참조
    this.$el = $el;
    this.$links = this.$el.find('a');

    // 플러그인 옵션 병합(오버라이딩)
    this.config = $.extend({}, $.fn[pluginName].defaults, options);

    this.init();
}

SkipNav.prototype = {

    'init' : function () {
        this.controls();
        this.events();
    }
    ,'controls' : function () {
        this.$el.addClass(this.config.container);
        this.$links.addClass(this.config.linkClasses.hidden + ' ' + this.config.linkClasses.focusable)
    }
    ,'events' : function () {
        // this 가 skipNav 를 계속 참조하도록 $.proxy 를 사용
        this.$el.on('click','a', $.proxy(this.linkAction, this))
    }
    ,'linkAction' : function (e) {
        e.preventDefault();
        var path = e.target.getAttribute('href');
        var $target = $(path);
        $target
                .attr('tabindex', 0)
                .focus()
                .on('blur', $.proxy(this.resetTabindex, $target));

        this.config.setHash && (window.location.href = path);
    }
    ,'resetTabindex' : function () {
        this.attr('tabindex', -1);
    }
};

$.fn[pluginName] = $.fn[pluginName] || function (options) {
    // $this 에 jQuery() 인스턴스 객체(집합) 참조
    var $this = this;

    // $this 인스턴스 객체(집합)에 개별적으로 플러그인 적용
    return $.each($this, function (index, el) {
        // 개별 인스턴스 객체 참조
        var $el = $this.eq(index);

        // data() 메소드를 이용
        // data 에 참조되어 있다면 생성자 호출(new)을 한번만 실행
        if (!$.data(this, 'plugin_' + pluginName)) {
            $.data(this, 'plugin_' + pluginName, new SkipNav($el, options));
        }

        // jQuery 체이닝 처리를 위한 return
        return $el;
    })

};

/**
 * 플러그인 기본 옵션 설정
 * -------------------------------- */
$.fn[pluginName].defaults = {
    'container': 'skipNav-container',
    'linkClasses' : {
        'hidden' : 'a11y-hidden',
        'focusable' : 'focusable'
    },
    'setHash' : false
};

$('#skip-menu').skipNav({
    'setHash' : !false
});


Skip Navigation Plugin DEMO #3


See the Pen skip-navigation 플러그인 #3 (프로토타입 패턴) by jaeheekim (@jaehee) on CodePen.




플러그인 Widget(위젯팩토리) 패턴 #4

Widget Factory를 사용하기 위해서는 basic 제이쿼리와 jQuery-ui 가 필요합니다.

HTML
<head>
    <title>jQuery 플러그인 제작</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/jquery.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js"></script>
</head>


javascript
/**
 * $.widget("ui.mywidget", {options: {key:value}, _method: function() {}, ...})
 * -----------------------------------------------------------------------------
 */

$.widget('ui.skipNav',{
    // Default options
    // options : 위젯에서 사용할 기본 옵션을 정의
    options: {
        'container': 'skipNav-container',
        'linkClasses' : {
            'hidden' : 'a11y-hidden',
            'focusable' : 'focusable'
        },
        'setHash' : !true
    },
    _create: function() {
        this.$el = this.element;
        this.$links = this.$el.find('a');

        this._control();
        this._events();
    },
    _control : function () {
        this.$el.addClass(this.options.container);
        this.$links.addClass(this.options.linkClasses.hidden + ' ' + this.options.linkClasses.focusable);
    },
    _events : function () {
        this.$el.on('click', 'a', $.proxy(this._linkAction, this))
    },
    _linkAction : function (e) {
        e.preventDefault();
        var path = e.target.getAttribute('href'),
            $target = $(path);

        $target
                .attr('tabindex', 0)
                .focus()
                .on('blur', $.proxy(this._resetTabindex, $target));

        this.options.setHash && (window.location.href = path)
    },
    _resetTabindex : function () {
        this.attr('tabindex', -1);
    }
});

$('#skip-menu').skipNav({
    setHash : !true
});


Skip Navigation Plugin DEMO #4