キーイベントを利用して、@tabIndex を渡り歩く。
WAI-ARIAのドラッグ&ドロップ、考えるほどに、悩みが増える。
@tabIndex="-1" にした要素を、どうやってキーボードだけでフォーカスさせるのか?
このWAI-ARIAのドラッグ&ドロップは、@tabIndex="0"にしたほうが遥かに簡単なのでは?
@tabindexがついた要素を集め、
1以上のものはソートし、0と-1は、出現順に並べリストに追加する。
offsetFocusで指定された要素をリストの中から調べだし、そのオフセットの要素を返す
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML+ARIA 1.0//EN" "http://www.w3.org/WAI/ARIA/schemata/html4-aria-1.dtd"> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <meta http-equiv="Content-Script-Type" content="text/javascript"> <meta http-equiv="Content-Style-Type" content="text/css"> <body> <div> 4<a href="#">aaaa</a><br> 5.<input type="text"><br> 6.<select name="a"> <optiont>a <optiont>b </select><br> 7.<textarea></textarea><br> <h1 tabindex="-1">8abc</h1> <h2 tabindex="0">9.def</h2> <h3 tabindex="1">1.ABC</h3> <h4 tabindex="2">2.ABC</h4> <h5 tabindex="3">3.ABC</h5> </div> <script type="text/javascript"> (function () { var doc = this.document; var tabList = []; var TabIndex = function () {;} // 要素の位置を返す var indexOf = function (e) { for (var i = 0, m = tabList.length; i < m; i += 1) if (e === tabList[i]) return i; return -1; }; // オフセット要素を返す var offsetFocus = function (e, offset) { var n0 = indexOf (e); var n1 = n0 + offset; return (n0 < 0 || n1 < 0 || tabList.length <= n1) ? null: tabList[n1]; }; // @tabindexの要素をリロードする var reset = function (init) { var es; var buf = []; tabList = []; if (init) Array.slice (doc.querySelectorAll ('a, input, select, textarea')). forEach (function (e) { e.hasAttribute ('tabindex') || (e.tabIndex = 0); }); Array.slice (doc.querySelectorAll ('* [tabindex]')). forEach (function (e) { var n = e.tabIndex; (n < 1) ? tabList.push (e) : buf.push ({ no: n, element: e }); }); tabList = buf.sort (function (a, b) { return ((a.no > b.no) - (a.no < b.no)); }). reduce (function (a, b) { a.push (b.element); return a; }, []). concat (tabList); }; // tabIndexListの指定された領域の要素を帰す var getRange = function (start, end) { var s = indexOf (start); var e = indexOf (end); return (s < 0 || e < 0) ? []: (s < e) ? tabList.slice (s, e): tabList.slice (e, s); }; //___________ TabIndex.getRange = getRange; TabIndex.reset = reset; TabIndex.offsetFocus = offsetFocus; this.TabIndex = TabIndex; })(); TabIndex.reset (true); var onKeydown = function (evt) { var e = evt.target; var n; switch (evt.keyCode) { case 9 : if (n = TabIndex.offsetFocus (e, evt.shiftKey ? -1: 1)) { n.focus (); evt.preventDefault (); } } return false; }; document.addEventListener ('keydown', onKeydown, true); </script>
「WAI-ARIA キーボードによるドラッグ&ドロップ」タブキーの移動を組み込む
だが、aria-controls は、だめなまま。
ドラッグ対象となった要素または、その祖先をたどり、aria-controls で示された要素を見つけ出し、その要素のaria-drageffect を適当に変え・・・この適当に変えるの基準はどこからにしよう?
キーボードで a と b と c をドラッグしたとして、c の位置で [ctrl]+[m] を押すとドロップ要素を選択できるようにするよりも、tabキーを延々と押しながらドロップするまで移動するのがよいのやら・・・
なぜか、重要な「見ため」は、後回し。
http://standards.mitsue.co.jp/archives/2009/07/
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML+ARIA 1.0//EN" "http://www.w3.org/WAI/ARIA/schemata/html4-aria-1.dtd"> <html lang="ja"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <meta http-equiv="Content-Script-Type" content="text/javascript"> <meta http-equiv="Content-Style-Type" content="text/css"> <title>WAI ARIA のドラッグ&ドロップの実装</title> <style type="text/css"> * [aria-grabbed="false"] { /*ドラッグ対象可能*/ border: 3px #fdd solid !important; } * [aria-grabbed="false"]:focus { /*ドラッグ対象にフォーカスが当たったら*/ border: 3px #f00 outset !important; } * [aria-grabbed="true"] { /*ドラッグ対象に選ばれている状態*/ border: 3px #f0f dotted !important; } * [aria-grabbed="true"]:focus { /*ドラッグ対象に選ばれている状態で尚且つフォーカスがあたっている*/ border: 3px #f0f inset !important; } * [aria-dropeffect] { min-height: 2em; border : 3px #eef solid; } * [aria-dropeffect]:focus { /*ドロップにフォーカスが当たっている*/ border: 3px #00f inset !important; cursor: move; } li { margin:2px; } em { color:green; } </style> <head> <body> <h1>WAI ARIA のキーボードによるドラッグ&ドロップの実装</h1> <h2>動作サンプルの説明</h2> <dl> <dt>ドラッグするには、</dt> <dd>[Tab]:項目移動 / [Spe] :選択or解除 / [CTRL]+[Spc]: 複数選択 / [SHIFT]+[Spc]:範囲選択 <dt>ドロップするには、</dt> <dd>[CTRL]+([M] || [Enter]) :決定 / [Esc]: 選択範囲解除 </dl> <hr> <h3>東北6県</h3> <ol aria-dropeffect="none" aria-controls="hoge"> <li aria-grabbed="false">青森県(<em tabindex="1">八戸市</em>)</li> <li aria-grabbed="false">岩手県(<em tabindex="2">洋野町</em>)</li> <li aria-grabbed="false">秋田県</li> <li aria-grabbed="false">宮城県</li> <li aria-grabbed="false">山形県</li> <li aria-grabbed="false">福島県</li> </ol> <h3>雪が多い県</h3> <ol aria-dropeffect="none" id="hoge"></ol> <textarea name="test" cols="66" rows="6"></textarea> <script type="application/javascript;version=1.8"> (function () { var doc =document; /*### tabIndex ###*/ var tabList = []; // 要素の位置を返す var indexOf = function (e) { for (var i = 0, m = tabList.length; i < m; i += 1) if (e === tabList[i]) return i; return -1; }; // オフセット要素を返す var offsetFocus = function (e, offset) { var n0 = indexOf (e); var n1 = n0 + offset; return (n0 < 0 || n1 < 0 || tabList.length <= n1) ? null: tabList[n1]; }; // @tabindexの要素をリロードする var reLoad = function () { var es; var buf = []; tabList = []; Array.slice (doc.querySelectorAll ('* [tabindex]')). forEach (function (e) { var n = e.tabIndex; (n < 1) ? tabList.push (e) : buf.push ({ no: n, element: e }); }); tabList = buf.sort (function (a, b) { return ((a.no > b.no) - (a.no < b.no)); }). reduce (function (a, b) { a.push (b.element); return a; }, []). concat (tabList); }; // tabIndexListの指定された領域の要素を帰す var getRange = function (start, end) { var s = indexOf (start); var e = indexOf (end); return (s < 0 || e < 0) ? []: (s < e) ? tabList.slice (s, e + 1): tabList.slice (e, s + 1); }; //___________ var TabIndex = { getRange : getRange, reLoad : reLoad, offsetFocus : offsetFocus }; //___________ var getParentGrabbed = function (n) { // 親要素のドラッグ対象要素を返す return n ? n === doc ? null: n.getAttribute ('aria-grabbed') ? n: arguments.callee (n.parentNode): null; }; var getParentAriaControls = function (n) { return n ? n === doc ? null: n.getAttribute ('aria-controls') ? n: arguments.callee (n.parentNode): null; }; var drag = { add : function (target, add, range) { var last = doc.querySelector ('* [aria-grabbed][aria-posinset="last"]') || doc.querySelector ('* [aria-grabbed="false"]'); if (range) { this.clear (); TabIndex.getRange (target, last). forEach (function (e) { e.setAttribute ('aria-grabbed', 'true'); }); return; } else if (add); else if (last !== target) this.clear (); target.setAttribute ('aria-grabbed', String ('true' !== target.getAttribute ('aria-grabbed'))); if (last) last.removeAttribute ('aria-posinset'); target.setAttribute ('aria-posinset', 'last'); }, clear : function () { Array.forEach (doc.querySelectorAll ('* [aria-grabbed="true"]'), function (e) { e.setAttribute ('aria-grabbed', 'false'); }); }, drop : function (target) { var tName = target.nodeName; var selected = doc.querySelectorAll ('* [aria-grabbed="true"]'); if (! selected.length) return; Array.forEach (selected, function (e) { var effect = e.getAttribute ('aria-controls'); var parent = e.parentNode; switch (effect) { case 'move' : default : if (tName === parent.nodeName) { target.appendChild (e.cloneNode (true)); e.parentNode.removeChild (e); } else if ('value' in target) { target.value += e.textContent; } break; case 'copy' : if (tName === parent.nodeName) { target.appendChild (e.cloneNode (true)); } else if ('value' in target) { target.value += e.textContent; } break; } }); // ドロップ対象の @aria-dropeffect を none に戻す target.setAttribute ('aria-dropeffect', 'none'); // ドラッグ対象の @aria-grabbed を false に戻す Array.forEach (doc.querySelectorAll ('* [aria-grabbed="true"]'), function (e) { e.setAttribute ('aria-grabbed', 'false'); }); } }; //_____________ var onKeydown = function (evt) { var e = evt.target; var g = getParentGrabbed (e); // ドラッグ対象の要素 var f = e.hasAttribute ('aria-dropeffect'); // ドロップ対象の effect switch (evt.keyCode) { case 9 : if (n = TabIndex.offsetFocus (e, evt.shiftKey ? -1: 1)) { n.focus (); evt.preventDefault (); } break; case 77 : case 13 : if (f) if (evt.ctrlKey) { drag.drop (e); // [CTRL] + [ M || Enter ] ドラッグ確定 TabIndex.reLoad (); } break; case 27 : if (g || f) drag.clear (); // [ESC] 選択解除 break; case 32 : if (g) { drag.add (g, evt.ctrlKey, evt.shiftKey); // drag } break; } }; //_____________ Array.slice (doc.querySelectorAll ('a[href], input, select, textarea')). forEach (function (e) { e.hasAttribute ('tabindex') || (e.tabIndex = 0); }); Array.slice (doc.querySelectorAll ('input, select, textarea')). forEach (function (e) { e.setAttribute ('aria-dropeffect', 'none'); }); Array.slice (doc.querySelectorAll ('* [draggable="true"]')). forEach (function (e) { e.hasAttribute ('aria-grabbed') || e.setAttribute ('aria-grabbed', 'false'); }); Array.slice (doc.querySelectorAll ('* [aria-dropeffect], * [aria-grabbed="false"]')). forEach (function (e) { e.hasAttribute ('tabindex') || (e.tabIndex = -1); }) TabIndex.reLoad (); document.addEventListener ('keydown', onKeydown, false); })(); </script>