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"> </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:


댓글 남기기