Help us understand the problem. What is going on with this article?

カーソルキーでプルダウンを選択する

始めに

よくセレクトボックスで操作感をよくするためにキーボードでも選択ができるようにしているサイトがありますが、具体的にどうやっているかをまとめました。

実装方法

端的にいえば、focus処理をJS側で泥臭く設定しています。もうちょっとなんとかならないのかなと思いましたが、これがスタンダードなやり方な気がしました(他にやり方があれば教えて欲しいです)。
具体的にはtabindex="0"を設定してfocusできる要素にして、後はキーイベントを拾ってそれに応じて前や後の要素をfocusさせます。
実装イメージは以下の通りです。

実装イメージ
<template lang="pug">
.select-list
  template(v-for="option in $props.options")
    //- tabindexをセットして、keydownイベントでキー操作をハンドリングする
    .select-list__item(
      :key="option.id"
      ref="elItems"
      tabindex="0"
      @click="onSelect(option.id)"
      @keydown="onItemKeyDown"
    )
      | {{ option.text }}
</template>

<script>
/** キーボードの入力コード */
const KEY_CODES = {
  ENTER: 13,
  UP: 38,
  DOWN: 40,
};

Vue.component('SelectBox', {
  data() {
    return {
      focusIndex: 0,
    };
  },
  methods: {
    focusItem(index) {
      this.$data.focusIndex = _.clamp(index, 0, this.$refs.elItems.length - 1);
      const elItem = this.$refs.elItems[this.$data.focusIndex];
      elItem.focus();
    },
    onItemKeyDown(event) {
      event.preventDefault();
      event.stopPropagation();

      // Enterキー: クリックイベントを発火させて、クリックと同じ挙動にする
      if (event.keyCode === KEY_CODES.ENTER) {
        const elItem = this.$refs.elItems[this.$data.focusIndex];
        elItem.click();
      }

      // ↑キー: 一つ上の要素をフォーカスさせる
      if (event.keyCode === KEY_CODES.UP) {
        this.focusItem(this.$data.focusIndex - 1);
        return;
      }

      // ↓キー: 一つ下の要素をフォーカスさせる
      if (event.keyCode === KEY_CODES.DOWN) {
        this.focusItem(this.$data.focusIndex + 1);
        return;
      }
    },
  },
});
</script>

実際に実装したサンプルコードは以下になります。

See the Pen カーソルキーでプルダウンを選択する by wintyo (@wintyo) on CodePen.

検索+バーチャルスクロールの場合

検索ボックスがあってそこから↓キーで項目にfocusする方法も同じようにJSでハンドリングすればいいですが、項目数が多くてバーチャルスクロールで実装した場合はかなりの鬼門でした。。。
ここではvue-virtual-scrollerのRecycleScrollerを使った際の実装方法について書いていきます。

focus対象の要素はquerySelectorで探す

何番目とかで見つけられないので、クラスを付与してそのクラスを探すようにしました。ここで注意なのは再利用しているということで使われていない要素に昔の情報が残っていることがあるので、キチンとactiveなものにだけクラスをつけるようにします。

実装イメージ
<template lang="pug">
RecycleScroller(
  ref="scroller"
  :items="_filteredOptions"
  :itemSize="40"
  keyField="id"
  v-slot="{ item, active }"
)
  .select-item(
    tabindex="0"
    :class="{ [`js-active-item-${item.id}`]: active }"
    @click="onSelect(item.id)"
    @keydown="onItemKeyDown"
  )
    | {{ item.text }}
</template>

<script>
Vue.component('SelectBox', {
  methods: {
    focusItem(index) {
      if (!this.$refs.scroller) {
        console.warn('scroller not found');
        return;
      }
      this.$data.focusIndex = _.clamp(index, 0, this._filteredOptions.length - 1);
      const option = this._filteredOptions[this.$data.focusIndex];
      // RecycleScrollerにあるエレメントから対象のDOMを探す
      const elItem = this.$refs.scroller.$el.querySelector(`.js-active-item-${option.id}`);
      if (!elItem) {
        console.warn('focus item lost!');
        return;
      }
      elItem.focus();
    },
  },
});
</script>

検索ボックスから項目に移動する際は上にスクロールしてからにする

バーチャルスクロールで一番上の要素が無い場合があるのでスクロールしてからフォーカスします。
scrollToItemはドキュメントには書かれてなかったのですが、Issueには書かれてました(結構危ないかも。。。)
https://github.com/Akryum/vue-virtual-scroller/issues/180

スクロールしてからフォーカス
if (event.keyCode === KEY_CODES.DOWN) {
  // 上にスクロールしてからフォーカスする
  this.$refs.scroller.scrollToItem(0);
  window.setTimeout(() => {
    this.focusItem(0);
  }, 10);
}

要素がblurされるたびにfocusし直す

使いまわしていくので、中身が更新されてfocusが外れてしまう場合があります。これをどうしようか色々検討したのですが、最終的にはblurされたらfocusし直すやり方が一番focusをキープできました。ただしこれでも上手くfocusできない時がありました。。。そもそもスクロールしているとfocus対象の要素が存在しない時もありますし、これが限界のような気がしました。

検討メモ

却下された他の方法についても記載しておきます。

RecycleScrollerのupdateイベントをみて更新する
updateイベントは更新後だと思っていましたが、どうやら違うような気がしていて、updateイベント後にフォーカスし直しても外れてしまうことが多々ありました。setTimeoutを挟んで一定時間置いてからfocusするという方法もなくは無いですが、それだと不安定なので却下となりました。

サンプルコード

以上の実装をしたサンプルは以下の通りです。

See the Pen カーソルキーでプルダウンする(バーチャルスクロール版) by wintyo (@wintyo) on CodePen.

終わりに

以上がカーソルキーでプルダウンを操作する方法でした。バーチャルスクロールについてはキー操作させるのはかなり難しく、中途半端な感じになってしまいました。。。vue-selectというライブラリはhover時もfocusする挙動になっていて、これにしていれば途中でfocusが外れてもそこまで気にならないのかなとか思ってきました。
ググり力が足りないだけかもしれませんが、意外とこういった記事が見当たらなかったので誰かのお役に立てれば幸いです。

wintyo
team-lab
最新のテクノロジーを活用したシステムやデジタルコンテンツの開発を行うウルトラテクノロジスト集団
https://www.team-lab.com
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした