フロントエンドエンジニアのこん(@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、および関連技術で開発されたダイナミック・コンテンツや高度なユーザー・インターフェース・コントロールに役立ちます。
WAI=Web Accessibility Initiativeで、ARIA=Accessible Rich Internet Applicationsなことがわかりました。WAI-ARIAとはW3Cで定義されるWebアクセシビリティの仕様です。どんな人でもWebコンテンツを利用しやすくするための仕様が定義されています。
続いてRoleです。WAI-ARIAにはRoles, States, Propertiesの3つの概念が存在します。
ここで説明するよりも100倍わかりやすい記事があるのでおすすめです。
なんでアクセシビリティを考えるの?
はじめはメンターの方にアクセシビリティについてあれこれ説明いただき、なんとなくやらなきゃだな〜と思いながら、一方で正直に言えば、実装するの大変だな〜と思っていました。
このもやもやを払拭するとてもいい言葉がありましたので紹介します。
「アクセシビリティが必要な人とそうでない人がいる、のではない。アクセシビリティがすでに十分届いている人と、まだ届いていない人がいるということなのだ」
これを読んでから、アクセシビリティを前向きに捉える事ができるようになりました。
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.
コンボボックスは、関連するポップアップメニューを持つ入力ウィジェットであり、ユーザーがコンボボックスの値を選択するための可能な値のコレクションから選択することができる機能を提供します。
コンボボックス、と一口に言ってもいくつかのパターンがあるようです。
今回はその中でも、オートコンプリートできるコンボボックスを実装してみます。
考えること
言わずもがな今回使うロールはcomboboxです。ですが、オートコンプリートを実現するにあたって関連するロールが2つあります。
- listbox
- option
1はオートコンプリートを表示するポップアップメニューの入力ウィジェットのためのロールです。
2はlistboxロールに関連するロールで、ポップアップメニュー内の選択肢(オートコンプリートの候補)として利用します。
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://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などが要素ごとに細かく見れます。まずはここで正しくロールやステートが設定されているかを確認しましょう。
今回はフォーカス時に表示されるポップアップもあるため、そういうときは開発者ツールでEscを押しConsoleドロワーを開きます。︙のMore ToolsからRenderingタブを選択し、Emulate a focused pageをチェックすると、開発者ツールなどを触ってフォーカスが外れてもポップアップが表示され続けます。
aria-activedescendant
などは動的に変わる値のため、正しく指定されるか確認してみましょう。
スクリーンリーダーでcomboboxを使ってみよう
MacのVoiceOverなら⌘+F5、WindowsのナレーターはWindows+Ctrl+Eで使えます。細かい操作方法については今回省略します。
スクリーンリーダーで正しく使える=一定のアクセシビリティを満たしている、と思います。comboboxのように矢印キー入力をJavaScriptで制御させる場合などは、スクリーンリーダーでも動きを確認すると、より正確にアクセシブルな実装ができると思います。
スクリーンリーダー大正こそこそ話
スクリーンリーダーについて調べてみると、必ずしもOS標準のスクリーンリーダーが使われるわけではないようで、複数のスクリーンリーダーを併用することがあるそうです。(初めて知った。
OS標準のスクリーンリーダーが使いづらいときなどはサードパーティのスクリーンリーダーを使ってみるといいかもしれません。
おわりに
ヤプリではデザインシステムの実装を通してアクセシビリティについて学び、日々業務として取り組んでおります。このような活動にもしご興味を持っていただけたり、やってみたいと思った方は、是非カジュアル面談でお話しましょう!