Yappli Tech Blog

株式会社ヤプリの開発メンバーによるブログです。最新の技術情報からチーム・働き方に関するテーマまで、日々の熱い想いを持って発信していきます。

VueでWAI-ARIA Rolesに準じたComboboxを作ろう

フロントエンドエンジニアのこん(@k0n_karin)です!
ヤプリでは毎四半期の末頃になると、各自が期初に立てた目標を達成するべく大量の記事が投稿されます!私ももれなくそのうちの一人です!

WAI-ARIA、Roleとは

十分調べて記事執筆しておりますが、もし間違いなどあればぜひ指摘いただけると助かります。

今回はアクセシビリティのお話です。冒頭からもしかしたら馴染みのない言葉が登場したかもしれません。
早速WAI-ARIAについて調べましょう。W3CにWAI-ARIAの概要があります。

WAI-ARIA, the Accessible Rich Internet Applications Suite, defines a way to make Web content and Web applications more accessible to people with disabilities. It especially helps with dynamic content and advanced user interface controls developed with HTML, JavaScript, and related technologies.

WAI-ARIA(アクセシブル・リッチ・インターネット・アプリケーション・スイート)は、ウェブ・コンテンツとウェブ・アプリケーションを障害者にとってよりアクセシブルなものにする方法を定義しています。特に、HTML、JavaScript、および関連技術で開発されたダイナミック・コンテンツや高度なユーザー・インターフェース・コントロールに役立ちます。

www.w3.org

WAI=Web Accessibility Initiativeで、ARIA=Accessible Rich Internet Applicationsなことがわかりました。WAI-ARIAとはW3Cで定義されるWebアクセシビリティの仕様です。どんな人でもWebコンテンツを利用しやすくするための仕様が定義されています。

続いてRoleです。WAI-ARIAにはRoles, States, Propertiesの3つの概念が存在します。
ここで説明するよりも100倍わかりやすい記事があるのでおすすめです。

zenn.dev

なんでアクセシビリティを考えるの?

はじめはメンターの方にアクセシビリティについてあれこれ説明いただき、なんとなくやらなきゃだな〜と思いながら、一方で正直に言えば、実装するの大変だな〜と思っていました。

このもやもやを払拭するとてもいい言葉がありましたので紹介します。

「アクセシビリティが必要な人とそうでない人がいる、のではない。アクセシビリティがすでに十分届いている人と、まだ届いていない人がいるということなのだ」

kanazawalab.txt-nifty.com

これを読んでから、アクセシビリティを前向きに捉える事ができるようになりました。

comboboxとは?

W3Cの解説があったので拝借しました。

A combobox is an input widget with an associated popup that enables users to select a value for the combobox from a collection of possible values.

コンボボックスは、関連するポップアップメニューを持つ入力ウィジェットであり、ユーザーがコンボボックスの値を選択するための可能な値のコレクションから選択することができる機能を提供します。

www.w3.org

コンボボックス、と一口に言ってもいくつかのパターンがあるようです。

今回はその中でも、オートコンプリートできるコンボボックスを実装してみます。

www.w3.org

考えること

言わずもがな今回使うロールはcomboboxです。ですが、オートコンプリートを実現するにあたって関連するロールが2つあります。

  1. listbox
  2. option

1はオートコンプリートを表示するポップアップメニューの入力ウィジェットのためのロールです。

2はlistboxロールに関連するロールで、ポップアップメニュー内の選択肢(オートコンプリートの候補)として利用します。

developer.mozilla.org

comboboxロールとlistbox/optionロールの関連付け

スクリーンリーダーなどでcomboboxを正しく認識するために、ここまでに登場した3つのロールのそれぞれを関連付ける必要があります。

comboboxで使うステートは以下の6つです。

  • aria-expanded
  • aria-haspopup
  • aria-controls
  • aria-activedescendant
  • aria-autocomplete
  • aria-selected

aria-expanded

comboboxのポップアップが開かれているかの状態を表します。通常、初期状態はfalseで、listboxロールが利用可能な状態の時trueにします。

aria-haspopup

comboboxがポップアップを持つかの状態を表します。comboboxロールは暗黙のaria-haspopupを持つため、ポップアップにlistboxロールを使う際は明示的に指定する必要はありません。

aria-controls

comboboxがどのlistboxと紐づいているかを識別します。具体的にはlistboxロールのidを指定します。

aria-activedescendant

comboboxのポップアップ内で現在選択されているアクティブな選択肢を識別します。具体的にはアクティブなoptionロールのidを指定します。

DOMフォーカスがcomboboxに残る場合に利用し、ダイアログのようにフォーカスが移動する場合は不要になります。

aria-autocomplete

オートコンプリート動作をする場合に、予測候補がどのように表示されるかを指定します。今回はlistを使います。

aria-selected

comboboxのポップアップ内の選択肢(optionロール)が選択されているかどうかを表します。選択されているときはtrue、そうでないときはfalseにします。

ロールで定義されたキーボード操作

各ロールには定義されたキーボード操作があります。ロールで定義されるキーボード操作はHTMLのタグと異なり、基本的にJavaScriptで実装することになります。ちょっとハードルが高いですね。

comboboxで必須となるキーボード操作は以下の3つです。

  • Down Arrow
  • Up Arrow
  • Enter

https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/combobox_role#keyboard_interactions

いざ実装

早速、完成したものがこちらになります。

github.com

ベースとなるコンポーネントを作る

以下のコミットでベースを作りました。

https://github.com/konkarin/vue-combobox/commit/8fa9756c4535b720300c780db835a01ef22e21b2

要素は大きく3つです。

  • テキスト入力となる<input>
  • ポップアップとなる<div>
  • ポップアップ内の各選択肢となる<button>

まず<input>にフォーカスが当たったら選択肢を持ったポップアップを表示します。focusとblurイベントでポップアップの表示非表示のフラグを切り替えます。

<input
  id="combobox"
  type="text"
  v-model="inputValue"
  class="combobox-input"
  @focus="toggleListbox(true)"
  @blur="toggleListbox(false)"
/>
<!-- popup -->
<div v-if="expandedListbox" class="combobox-listbox">
  <button v-for="option in filteredOptions" :key="option" class="combobox-option">
    {{ option }}
  </button>
</div>

そして、入力値によって選択肢を絞り込むcomputedを実装します。入力値が空のときは、デフォルトの配列を返すようにしましょう。

import { computed, ref } from 'vue'

const inputValue = ref('')

const options = ref(['hoge', 'fuga', 'piyo'])
const filteredOptions = computed(() => {
  if (!inputValue.value) return options.value

  return options.value.filter((option) => {
    return option.includes(inputValue.value)
  })
})

キーボード操作を実装する

https://github.com/konkarin/vue-combobox/commit/6fb3fa9959ddee1df3a1be9961909f2f8aa3cbfa

ロールを付与する前に、キーボード操作に必要なコードを実装しましょう。inputタグにフォーカスが当たった状態でキーボード操作をさせたいので、inputタグでキーごとのkeydownイベントをハンドリングします。

comboboxロールはEnter, Up Arrow, Down Arrowだけで十分ですが、ポップアップを閉じるためにEscも追加しましょう。

 <input
   id="combobox"
   type="text"
   v-model="inputValue"
   class="combobox-input"
   @focus="toggleListbox(true)"
   @blur="toggleListbox(false)"
+  @input="resetCurrentIndex()"
+  @keydown.prevent.up="onKeydownUp()"
+  @keydown.prevent.down="onKeydownDown()"
+  @keydown.prevent.enter="onKeydownEnter()"
+  @keydown.prevent.esc="onKeydownEsc()"
 />

各イベントに対応するコードを書きましょう。

まずUp/Down Arrowですが、現在選択している要素のインデックス用の値を用意します。

Up/Downを押すごとにcurrentSelectedIndexを増減させます。また、ポップアップ内の選択肢の最上部でUpした場合、最下部の選択肢に移動させます。Downでは逆のことをします。

const currentSelectedIndex = ref(-1)
const onKeydownUp = () => {
  if (currentSelectedIndex.value === -1) {
    toggleListbox(true)
  }

  if (currentSelectedIndex.value <= 0) {
    currentSelectedIndex.value = filteredOptions.value.length - 1
  } else {
     currentSelectedIndex.value += -1 
  }
}
const onKeydownDown = () => {
  if (currentSelectedIndex.value === -1) {
    toggleListbox(true)
  }

  if (currentSelectedIndex.value >= filteredOptions.value.length - 1) {
    currentSelectedIndex.value = 0
  } else {
    currentSelectedIndex.value += 1
  }
}

続いてEnterを押したときに選択した候補を受け入れる処理です。inputValueに現在の選択肢の値を代入するだけでOKですね。

また、Escを押したときはポップアップを閉じ、currentSelectedIndexをリセットします。

const onKeydownEnter = () => {
  if (filteredOptions.value[currentSelectedIndex.value] === undefined) {
    return
  }

  inputValue.value = filteredOptions.value[currentSelectedIndex.value]
  toggleListbox(false)
  resetCurrentIndex()
}

const onKeydownEsc = () => {
  toggleListbox(false)
  resetCurrentIndex()
}

const resetCurrentIndex = () => {
  currentSelectedIndex.value = -1
}

ARIAロールとステートを付与する

ARIAステートを付与する際は、文字列を渡しましょう。Vueでは特に、booleanをそのままbindすると正しくステートが付与されないため気をつけましょう。

https://github.com/konkarin/vue-combobox/commit/053661af2808856b32d3356e49a78dab510b0f35

   <input
      id="combobox"
+     role="combobox"
      type="text"
      v-model="inputValue"
      class="combobox-input"
+     :aria-activedescendant="activeOptionId"
+     aria-autocomplete="list"
+     aria-controls="listbox"
+     :aria-expanded="`${expandedListbox}`"
    >
+   <div v-if="expandedListbox" role="listbox" id="listbox" class="combobox-listbox">
      <button
        v-for="(option, index) in filteredOptions"
        :key="option"
+       role="option"
+       :id="`option-${index}`"
        class="combobox-option"
        :class="{ 'combobox-option-active': isActive(index) }"
+       :aria-selected="`${isActive(index)}`"
        @mouseover="currentSelectedIndex = index"
        @mouseleave="resetCurrentIndex()"
      >
        {{ option }}
      </button>
    </div>

Chrome開発者ツールのElementタブからAccessibilityパネルを見てみましょう。そうすると、アクセシビリティツリーや設定したARIA Attributesなどが要素ごとに細かく見れます。まずはここで正しくロールやステートが設定されているかを確認しましょう。

Chrome開発者ツールのアクセシビリティツリー

今回はフォーカス時に表示されるポップアップもあるため、そういうときは開発者ツールでEscを押しConsoleドロワーを開きます。︙のMore ToolsからRenderingタブを選択し、Emulate a focused pageをチェックすると、開発者ツールなどを触ってフォーカスが外れてもポップアップが表示され続けます。

フォーカスが外れてもフォーカスを矯正する設定

aria-activedescendantなどは動的に変わる値のため、正しく指定されるか確認してみましょう。

キーボード操作時のARIAステートの変化

スクリーンリーダーでcomboboxを使ってみよう

MacのVoiceOverなら⌘+F5、WindowsのナレーターはWindows+Ctrl+Eで使えます。細かい操作方法については今回省略します。

スクリーンリーダーでcomboboxが読み上げられることを確認する

スクリーンリーダーで正しく使える=一定のアクセシビリティを満たしている、と思います。comboboxのように矢印キー入力をJavaScriptで制御させる場合などは、スクリーンリーダーでも動きを確認すると、より正確にアクセシブルな実装ができると思います。

スクリーンリーダー大正こそこそ話 スクリーンリーダーについて調べてみると、必ずしもOS標準のスクリーンリーダーが使われるわけではないようで、複数のスクリーンリーダーを併用することがあるそうです。(初めて知った。
OS標準のスクリーンリーダーが使いづらいときなどはサードパーティのスクリーンリーダーを使ってみるといいかもしれません。

accessible-usable.net

おわりに

ヤプリではデザインシステムの実装を通してアクセシビリティについて学び、日々業務として取り組んでおります。このような活動にもしご興味を持っていただけたり、やってみたいと思った方は、是非カジュアル面談でお話しましょう!

open.talentio.com