キーイベントを利用して、@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>