[JavaScript]テキストエリア間を矢印キーで移動したい

# やりたいこと
- テキストエリア間のフォーカスを`KeyboardEvent`イベントで発火させて移動させたい
- フォーカスをテキストエリアの最初とか途中とか任意の場所に移動させたい
  - テキスト最後尾で↓キーを押したら次のテキストの先頭に移動させる 
  - テキストの先頭で↑キーを押したら前のテキストの最後尾に移動させる

# 結論
- `keydown`イベントで現在のテキストカーソルの位置を判定する
- `keyup`イベントでフォーカスの移動、フォーカスの位置の指定を行う

<p class="codepen" data-height="575" data-theme-id="light" data-default-tab="js,result" data-user="cornercrap" data-slug-hash="pogaeXX" style="height: 575px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="moving_focus">
  <span>See the Pen <a href="https://codepen.io/cornercrap/pen/pogaeXX">
  moving_focus</a> by hodaka (<a href="https://codepen.io/cornercrap">@cornercrap</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

# 動機
フォームなどのフォーカスの移動には`tab`キーを使うことが一般的かと思いますが、矢印キーで操作できた方が操作性も上がるかなと思い検証しました。しかし、思ったより難航したのでここに書き留めておきます。

# 主な登場人物

##### [keydown/keyupイベント](https://developer.mozilla.org/ja/docs/Web/API/Element/keydown_event)
それぞれキーを押したとき/離したときに発生するイベント

##### [KeyboardEvent.code](https://developer.mozilla.org/ja/docs/Web/API/Element/keydown_event)
押された/離したキーの種類

##### [focus()](https://developer.mozilla.org/ja/docs/Web/API/HTMLOrForeignElement/focus)
その要素がフォーカスされる

##### [setSelectionRange(selectionStart, selectionEnd)](https://developer.mozilla.org/ja/docs/Web/API/HTMLInputElement/setSelectionRange)
任意のテキストの開始と終了のインデックスを指定することでテキストを選択状態にする
→引数に同じインデックスを指定すればテキストカーソルの移動と同じ意味になる

今回操作するHTMLはこんな感じです。

```HTML:HTML
<div>
  <textarea id="textarea">テキストテキスト</textarea>
</div>
<div>
  <textarea id="textarea2">テキスト2テキスト2</textarea>
</div>
```

# 失敗例1

まず手始めに`keyup`だけでできるだろうと思い、組んでみました。

```javascript:失敗例1
const textArea = document.getElementById("textarea");
const textArea2 = document.getElementById("textarea2");

textArea.addEventListener("keyup", event => { // use keyup event
  if(event.code == "ArrowDown" && textArea.selectionStart == textArea.value.length){
      textArea2.focus();
  }
});
textArea2.addEventListener("keyup", event => {
  if(event.code == "ArrowUp" && textArea2.selectionStart == 0){
      textArea.focus();
  }
});
```
カーソルがテキストの途中にあってもフォーカスが移動してしまいます。
↓キーを押すとテキストカーソルは、`複数行ある場合は次の行へ移動する、最終行だった場合、最後尾に移動`します。
最後尾に移動してしまうと後半の条件式が満たされてしまうのでフォーカスが移動してしまいます。

# 失敗例2
ならばと`keydown`で試してみます。テキストカーソルの位置が想定どおりに行かないので`setSelectionRange()`で明示的に指定しています。

```javascript:失敗例2
const textArea = document.getElementById("textarea");
const textArea2 = document.getElementById("textarea2");

textArea.addEventListener("keydown", event => { // use keydown event
  if(event.code == "ArrowDown" && textArea.selectionStart == textArea.value.length){
      textArea2.focus();
      textArea2.setSelectionRange(0, 0);
  }
});
textArea2.addEventListener("keydown", event => {
  if(event.code == "ArrowUp" && textArea2.selectionStart == 0){
      textArea.focus();
      textArea.setSelectionRange(textArea.value.length, textArea.value.length);
  }
});
```
`setSelectionRange()`を使ってもカーソルの位置が指定できなくなってしまいました。

# 成功例?
`keydown`の中では`setSelectionRange()`がうまく機能していませんでした。
しかし、調べていたところ、[setTimeout()を使って回避する方法](https://stackoverflow.com/questions/11723420/chrome-setselectionrange-not-work-in-oninput-handler)が見つかりました。こちらを参考にしてみます。

```javascript:成功例?
const textArea = document.getElementById("textarea");
const textArea2 = document.getElementById("textarea2");

textArea.addEventListener("keydown", (event) => {
  if (
    event.code == "ArrowDown" &&
    textArea.selectionStart == textArea.value.length
  ) {
    window.setTimeout(() => { // use setTimeout()
      textArea2.focus();
      textArea2.setSelectionRange(0, 0);
    });
  }
});
textArea2.addEventListener("keydown", (event) => {
  if (event.code == "ArrowUp" && textArea2.selectionStart == 0) {
    window.setTimeout(() => {
      textArea.focus();
      textArea.setSelectionRange(textArea.value.length, textArea.value.length);
    });
  }
});
```
不思議なことに想定どおりに動いてくれています。とりあえず実装したい場合はこれでいいと思いますが、この解法に関しては最善策でないと述べられていますし、私自身動く理由がよくわからないので、もう少し模索してみます。

# 成功例（結論）
`keyup`ではカーソルの位置が正しく判定されず、`keydown`ではフォーカス後のカーソルがうまく指定できませんでした。
なので両方のイベントを使ってそれぞれの欠点を補います。

```javascript:成功例（結論）
let movingFlg = false;

textArea.addEventListener("keydown", (event) => {
  if (
    event.code == "ArrowDown" &&
    textArea.selectionStart == textArea.value.length
  ) {
    movingFlg = true;
  }
});
textArea.addEventListener("keyup", (event) => {
  if (event.code == "ArrowDown" && movingFlg) {
    movingFlg = false;
    textArea2.focus();
    textArea2.setSelectionRange(0, 0);
  }
});

textArea2.addEventListener("keydown", (event) => {
  if (event.code == "ArrowUp" && textArea2.selectionStart == 0) {
    movingFlg = true;
  }
});
textArea2.addEventListener("keyup", (event) => {
  if (event.code == "ArrowUp" && movingFlg) {
    movingFlg = false;
    textArea.focus();
    textArea.setSelectionRange(textArea.value.length, textArea.value.length);
  }
});
```

`keydown`では単に移動するべきかどうかを判定して、フォーカスやカーソル位置指定は`keyup`で行います。フラグも出現してコード量も多くなりましたが腑に落ちるレベルまでは到達できました。

# 終わりに

改善案・ご指摘・捕捉など歓迎です。優しく教えてください。