FORM 내 일부 항목의 tab index 순서를 재정의 하기

HTML 문서 안에서 <A> ,  <INPUT> , <SELECT> , <TEXTAREA>  등의 태그는 키보드나 마우스 입력을 통해 focus를 가지며, 키보드의 tab 키를 눌러 다음 항목으로, shift + tab 을 눌러 이전 항목으로 focus를 이동할 수 있다.

항목(태그)에 tabindex 속성이 주어지지 않았을 경우, 일반적으로, HTML에 코딩된 순서대로 위에서 아래로, 왼쪽에서 오른쪽으로 focus가 이동을 한다.

정상적인 탭 순서

위와 같은 화면에서는 왼쪽에서 오른쪽으로 tab 이 이동하는데, 위에서 아래로 먼저 이동하도록 설정하고 싶을 수 있을 것이다. 그럴 때는 tabindex=”1″ 과 같이 각 태그에 부여하면 이 값의 순서에 따라 focus 순서가 재배치 된다.

변경하고자 하는 탭 순서

하지만, 문서 전체에 아주 많은 항목들이 있고, 그 중 일부의 항목에 대해서만 tab index 를 변경하려면, 그 문서 내에 모든 항목에 전부 tabindex 속성을 지정해야한다. 이는 너무 비효율적이다.

변경하지 않을 항목에 tabindex 속성을 생략하면, 생략된 항목들은 tabindex 속성이 있는 항목들보다 나중 순서에 focus를 갖게 된다. 즉,  tabindex 가 부여된 항목을 모두 지난 후,  tabindex 가 없는 항목을 다시 지나가게 된다. 이것은 의도하지 않은 결과이다.

현재로서는 Javascript의 도움을 받지 않고 HTML 내에서 일부의 항목만 변경된 index를 갖는 방법은 없는 듯 하다.

How to create tabindex groups?

이 문서의 솔루션을 참고하여 새로 코드를 작성하였다. 일단 jquery 와 underscore 라이브러리가 필요하다.

<input type="text" data-tabgroup="section1" data-tabgroupindex="2" />

data-tabgroup 속성으로 변경된 tab 순서를 가질 항목들을 제한하고, data-tabgroupindex 속성으로 순서를 재정의한다. 이 값을 가지는 항목(태그)들은 위 아래 tabindex 속성을 가지지 않은 다른 태그들과 잘 어울려 의도한 대로 tab 순서를 갖게 된다.

윈 소스와 다른 점은,

  • tab index가 같은 tabgroup 항목들 안에서만 이동하지 않고, 위 아래의 (tab index 가 지정되지 않은) 다른 항목들로 자연스럽게 이동.
  • tabgroup 에 처음 진입할 때, 해당 항목의 index 가 첫번째가 아니어도 지정된 index 로 이동.
    • 마우스로 focus 를 얻을 때는 동작하지 않도록 (키보드로만 동작하도록) 개선 필요.
  • 문서 내에  <form> 이 여러 개 존재하더라도 관계없이 동작.

Source Code:

<!doctype html>
<html lang="en">
 <head>

    <script type="text/javascript" src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
    <script type="text/javascript" src="http://underscorejs.org/underscore-min.js"></script>
    <style>
label {
  width: 80px;
  display: inline-block;
}
div.label {
  width: 80px;
  float: left;
}
input {
  width: 80px;
}
textarea {
  width: 80px;
}

div.row {
  margin: 5px;
  clear: both;
}
</style>
 </head>

 <body>

<form>
  <h3>Section 1</h3>
  <div>
    <div class="row">
      <label for="field11">Field 11</label>
      <input type="text" id="field11" name="field11" />
    </div>
    <div class="row">
      <label for="field12">Field 12</label>
      <input type="text" id="field12" name="field12" data-tabgroup="section1" data-tabgroupindex="3" placeholder="tabindex: 3" />
    </div>
    <div class="row">
      <label for="field13">Field 13</label>
      <a href="#" id="field13" data-tabgroup="section1" data-tabgroupindex="5" >Link13 (tabindex: 5)</a>
    </div>
    <div class="row">
      <label for="field14">Field 14</label>
      <input type="text" id="field14" name="field14" data-tabgroup="section1" data-tabgroupindex="2" placeholder="tabindex: 2" />
    </div>
    <div class="row">
      <label for="field15">Field 15</label>
      <input type="text" id="field15" name="field15" data-tabgroup="section1" data-tabgroupindex="4" placeholder="tabindex: 4" />
    </div>
  </div>
</form>
<form>
  <h3>Section 2</h3>
  <div>
    <div class="row">
      <label for="field1">Field 21</label>
      <input type="text" id="field21" name="field21" />
    </div>
    <div class="row">
      <label for="field2">Field 22</label>
      <input type="text" id="field22" name="field22" />
    </div>
    <div class="row">
      <div class="label">&nbsp;</div>
      <div class="label">Field 23</div>
      <div class="label">Field 24</div>
      <div class="label">Field 25</div>
    </div>
    <div class="row">
      <label>Min</label>
      <input type="text" size="5" data-tabgroup="section2" data-tabgroupindex="1" placeholder="tabindex: 1" />
      <input type="text" size="5" data-tabgroup="section2" data-tabgroupindex="3" placeholder="tabindex: 3" />
      <input type="text" size="5" data-tabgroup="section2" data-tabgroupindex="5" placeholder="tabindex: 5" />
    </div>
    <div class="row">
      <label>Max</label>
      <input type="text" size="5" data-tabgroup="section2" data-tabgroupindex="2" placeholder="tabindex: 2" />
      <input type="text" size="5" data-tabgroup="section2" data-tabgroupindex="4" placeholder="tabindex: 4" />
      <input type="text" size="5" data-tabgroup="section2" data-tabgroupindex="6" placeholder="tabindex: 6" />
    </div>
  </div>
</form>
<form>
  <h3 class="header">Section 3</h3>
  <div>
    <div class="row">
      <label for="field31">Field31</label>
      <input type="text" id="field31" name="field31" data-tabgroup="section3" data-tabgroupindex="1" placeholder="tabindex: 1" />
    </div>
    <div class="row">
      <label for="field32">Field32</label>
      <textarea rows="3" id="field32" name="field32" data-tabgroup="section3" data-tabgroupindex="3" placeholder="tabindex: 3"></textarea>
    </div>
    <div class="row">
      <label for="field33">Field32</label>
      <select  id="field33" name="field33" data-tabgroup="section3" data-tabgroupindex="2">
        <option>option 1</option>
        <option>option 2</option>
        <option>option 3</option>
      </select>
    </div>
    <div class="row">
      <label for="field34">Field34</label>
      <textarea rows="3" id="field34" name="field34" data-tabgroup="section3" data-tabgroupindex="4" placeholder="tabindex: 4"></textarea>
    </div>
  </div>
</form>

<script>

$(document).on('focus', '[data-tabgroup]', function(e) {
    // TODO : 키보드로 들어올 때만 체크 필요
    var node = $(e.target);
    var nodeIndex = node.data("tabgroupindex");
    var tabgroup = node.data("tabgroup");
    var tabgroupNodes = $("[data-tabgroup='" + tabgroup + "']");
    var tabgroupIndexes = [];
    _.each(tabgroupNodes, function(item) {
      tabgroupIndexes.push(+$(item).data("tabgroupindex"));
    });
    tabgroupIndexes = _(tabgroupIndexes).compact();
    var orderedTabgroupIndexes = _.sortBy(tabgroupIndexes, function(num) { return num; });
    var lastNodeIndex = tabgroupIndexes.length - 1;

    //console.log('nodeIndex: ' + nodeIndex);
    //console.log('tabgroupIndexes[0]: ' + tabgroupIndexes[0]);
    //console.log('orderedTabgroupIndexes[0]: ' + orderedTabgroupIndexes[0]);
    //console.log('$(e.relatedTarget).data(tabgroup): ' + $(e.relatedTarget).data('tabgroup'));
    // tabgroup 의 첫번째 객체이긴 하지만, 가장 빠른 tabgroupindex 가 아니고, 외부에서 진입했을 때
    if (
        (nodeIndex == tabgroupIndexes[0])
        && (nodeIndex != orderedTabgroupIndexes[0])
        && ($(e.relatedTarget).data('tabgroup') != tabgroup)
    ) {
        $("[data-tabgroup='" + tabgroup + "'][data-tabgroupindex='" + orderedTabgroupIndexes[0] + "']").focus();
    }
    // tabgroup 의 마지막 객체이긴 하지만, 가장 나중 tabgroupindex 가 아니고, 외부에서 진입했을 때
    else if (
        (nodeIndex == tabgroupIndexes[lastNodeIndex])
        && (nodeIndex != orderedTabgroupIndexes[orderedTabgroupIndexes.length - 1])
         && ($(e.relatedTarget).data('tabgroup') != tabgroup)
    ) {
        $("[data-tabgroup='" + tabgroup + "'][data-tabgroupindex='" + orderedTabgroupIndexes[lastNodeIndex] + "']").focus();
    }
    e.preventDefault();
});

$(document).on('keydown', '[data-tabgroup]', function(e) {
  if (e.which === 9) {
    var node = $(e.target);
    var nodeIndex = node.data("tabgroupindex");
    var tabgroup = node.data("tabgroup");
    var allNodes = $(document).find("input,a,textarea,select");
    var tabgroupNodes = $("[data-tabgroup='" + tabgroup + "']");
    var tabgroupIndexes = [];
    _.each(tabgroupNodes, function(item) {
      tabgroupIndexes.push(+$(item).data("tabgroupindex"));
    });
    tabgroupIndexes = _(tabgroupIndexes).compact();
    var orderedTabgroupIndexes = _.sortBy(tabgroupIndexes, function(num) {
      return num;
    });
    var lastNodeIndex = tabgroupIndexes.length - 1;

    // 숫자 validation
    if (isNaN(parseFloat(nodeIndex)) || !isFinite(nodeIndex)) {  return; }

    if (e.which === 9)
        if (e.shiftKey) {
            // Shift + Tab 키가 입력되었을 경우 (역방향)
            var prevElement = orderedTabgroupIndexes[orderedTabgroupIndexes.indexOf(nodeIndex) - 1];
            if (typeof(prevElement) === "undefined") {                           // prevElement is not exist
                var prevIndex = parseInt(
                    $.map(
                        allNodes,
                        function(obj, index) {
                            if ($(obj).data('tabgroup') == tabgroup && $(obj).data('tabgroupindex') == nodeIndex) {
                                return index;
                            }
                        }
                    )
                ) - tabgroupIndexes.indexOf(nodeIndex) - 1;
                allNodes[prevIndex].focus();
            } else {
                $("[data-tabgroup='" + tabgroup + "'][data-tabgroupindex='" + prevElement + "']").focus();
            }
        } else {
            // Tab 키가 눌렸을 경우 (정방향)
            var nextElement = orderedTabgroupIndexes[orderedTabgroupIndexes.indexOf(nodeIndex) + 1];
            if (typeof(nextElement) === "undefined") {
                var nextIndex = parseInt(
                    $.map(
                        allNodes,
                        function(obj, index) {
                            if ($(obj).data('tabgroup') == tabgroup && $(obj).data('tabgroupindex') == nodeIndex) {
                                return index;
                            }
                        }
                    )
                ) + (lastNodeIndex - tabgroupIndexes.indexOf(nodeIndex)) + 1;
                // 뒤에 더 이상 객체가 없으면 원래 동작을 수행 (주소창으로 이동)
                if (typeof(allNodes[nextIndex]) === "undefined") { return; }
                allNodes[nextIndex].focus();
            } else {
                $("[data-tabgroup='" + tabgroup + "'][data-tabgroupindex='" + nextElement + "']").focus();
            }
        }
        e.preventDefault();
    }
});

</script>
 </body>
</html>

JSFiddle:

도큐멘트 에 올린 글 태그됨: , , ,

댓글 남기기

이 사이트는 Akismet을 사용하여 스팸을 줄입니다. 댓글 데이터가 어떻게 처리되는지 알아보세요.