Open33

Accessible Vue 読書メモ

ピン留めされたアイテム

https://accessible-vue.com/

Marcus Herrmann 氏による Web アプリにおけるアクセシビリティについて、ヒントやコツ、ベストプラクティスな手法、Vue2, 3 でのインクルーシプなコンポーネントを作成する方法について書かれた本。

About this book

読了

  • 開口第一でVueはとってもアクセシブルとのご意見
    • ReactやAngularよりもカンタンに書ける(筆者談)
  • 別の点ではVueはアクセシブルではないとのご意見(どっちやねん)
    • ただこれはVueに限らず他のフレームワークでも同様の問題を抱えている
    • もともと各種フレームワークはユーザがアクセシビリティを意識したコードを書けるはず
  • 問題なのはアクセシビリティの認識や教育ができていないことである
    • Webサイトやアプリは開発者が想定した環境で皆が使えるわけではない
    • 多様なユーザがWebを使っている現実があるが、そこへの配慮が忘れ去られている
      • これは「多様性への非認識」であるとも言える
    • JavaScriptでつくられたWebアプリ以外にも昔ながらのWebサイト(アクセシビリティ配慮がされている)もインターネット上にはいくつもあるが、その事自体に注目がいっていない
    • Web開発がプロ化してきたことで様々な職務の人が参加できるようになった
    • デザイン性のあるものやDRYなバックエンドへの興味は高くなる一方でアウトプットされるもの(HTML、CSS)への敬意はうすれていっている
    • Eric氏の言うように「フレームワークファースト」から「ユーザーファースト」への考えを移行する必要がある

What are the limits of this book?

  • この本ではあくまでもVueにおけるアクセシビリティについてを取り扱う
  • アクセシビリティも多岐にわたる内容があるため表層的にしか取り扱えない
    • eslint や jest や Cypress といったものはそれ自体で1冊の本を書けてしまうくらい膨大
  • あくまでもこの本はそれらの有用なツールを使ってアクセシビリティを実践する最初の一歩を教えるだけにとどめている

Who is this book for?

  • 対象者について
  • Vueでアプリケーションを作って、Vueを使って作ったページやアプリからユーザーを排除したくない人向け
  • 本書の読者はWebアクセシビリティについて基本的な知識があると理想的である
    • そうでなければチャプター1をしっかり読んだほうがいい
    • アクセシビリティについて詳しく知ることは、それだけで価値のあるスキルだから
  • WEBアプリに関するアクセシビリティ、特にVueのアクセシビリティはドキュメントが不足している
  • ただそれ自体とは関係なくアクセシビリティは重要である

How do code examples in this book work?

  • Vueアプリを作成する方法はいくつもあるので古典的な方法で紹介する
    • シングル・ファイル・コンポーネント(SFC)
    • プロジェクトで扱うものはVue CLIで作成されたものと仮定
    • その結果、オプションAPIを使用している
    • コードサンプルは、Vue 3とVue 2の両方の構文で存在して、コードサンプルごとにCodeSandboxで提供

Who wrote this book?

  • 筆者についての紹介項目
    • Marcus Herrmann
    • Webアクセシビリティのスペシャリスト(IAAP)の資格がある
    • ドイツ・ベルリン出身のフリーランスのWeb開発者
  • Vue.jsを好むがすべての案件でVue.jsを使用するわけではない
  • しかしクライアントサイド駆動型のフレームワークを用いたほうがいい
  • WEBアクセシビリティとVueが両方交わる領域に魅力を感じているので本書をつくった

チャプター1:Cover accessibility basics first

WIP

チャプター2:Basic web app accessibility concepts

WIP

チャプター3:Using Vue’s strengths

読了

Conveying context with props

  • heading を例にした props に関する説明
    • context is king, and useful heading levels their crown.
    • そうなんだ…(感想)
  • HeadingComponent.vue は以下のように実装すると良い
  <component :is="headline">見出し</component>
  computed: {
    headline() {
      return "h" + this.headlineLevel;
    },
  },
  • <h1> みたいな形でハードコーディングしてしまうと特定の位置でのコンポーネントとしてでしか活用できない
  • 妥当な位置に妥当な見出しレベルでおけるようにすることが肝要

Requiring props

  • 必須propsを使うことでアクセシビリティを向上させる方法
  • 下記のような形で実装するのではなく(labelがないのでそもそもNG)
<template>
  <input type="text">
</template>
  • 下記のような形でマークアップしてpropsを必須にすることでアクセシビリティを維持しつつ、labelが入っていない場合はブラウザコンソールで警告に気づける
<label for="name"> {{ label }} </label>
<input id="name" name="name" type="text" > 
  props: {
    label: {
      type: String,
      required: true,
    }
  }
  • その他の活用事例
    • 画像コンポーネントへのalt属性
    • インラインSVGへのaria-label、title
    • アイコンボタンへのaria-label
    • fieldsetでのlegendタグ
    • 多言語対応のときのlang属性
    • tableでのsummaryタグ

Circumventing Vue 2’s one-root-element rule

  • Vue2でのrootエレメントは1つでないといけない問題への対処
    • Vue3では複数エレメントがおけるようになった
  • table の実装では以下のような実装ではエラーになる問題点がある
<template>
  <tr><th>行1</th><td>内容1</td></tr> 
  <tr><th>行2</th><td>内容2</td></tr> 
  <tr><th>行3</th><td>内容3</td></tr> 
</template>
  • そのためdivで挟みがちになりそうだが、以下はVueではコンパイル成功できるがHTML構文上はエラーになる
<template>
  <div> 
    <tr><th>行1</th><td>内容1</td></tr> 
    <tr><th>行2</th><td>内容2</td></tr> 
    <tr><th>行3</th><td>内容3</td></tr> 
  </div>
</template>
<template>
  <fragment> 
    <tr><th>行1</th><td>内容1</td></tr> 
    <tr><th>行2</th><td>内容2</td></tr> 
    <tr><th>行3</th><td>内容3</td></tr> 
  </fragment>
</template>
import { Fragment } from 'vue-fragment'

Facilitate focus management with $refs

  • $refs を使うと.querySelectorなどのDOMクエリを使用せずともDOMノードへの参照ができるようになる
  • アプリのフォーカス制御とかで便利になる
    • refsで要素を指定して.focus()で要素にフォーカスできる

Controlling where attributes get applied to

  • コンポーネントに属性を付与するとコンテナにそのまま追加される
  • たとえば以下のコンポーネントに指定すると意図せぬ属性が付与されたりする
    • 本来であればbutton にdisabledが付与されてほしい
<HogeButton label="name" disabled />
<template>
  <div disabled>
    <button>{{name}}</button>
  </div>
</template>
  • これを防ぐためにinheritAttrsをfalseにして付与したい対象にv-bind="$attrs"を書くとよい
    • Vue3ではinheritAttrsを明示的に書かなくても省略して指定できる

Visibility helper components

  • 視覚的に隠すテクニックとしてvisually-hiddenという選択肢がある(前章参考)
  • <VisuallyHidden />というヘルパーコンポーネントを作ってみる
    • <VisuallyHidden tag="h2">私はここにいるよ</VisuallyHidden>
  • focusableというboolean propを渡すことでフォーカス可能なものにできる
    • スキップリンクという手法で使われている

Accessible Base Components

  • コンポーネントをアクセシブルにすることも大事だけどそれを使いやすくすることも大事
    • グローバルなコンポーネントとして使えるようにしておくと良い
    • 何度も使うものや、アクセシブルな方法で提供したいものが適している
    • テキスト入力やボタン、SVGアイコン、テーブルなど
  • 1つめの手段はベースとなるApp.vueにコンポーネントを登録しておくこと
  • 2つめの手段はエントリーファイルにVue.componentでコンポーネントを登録しておくこと
  • チーム内で使うように促進しておくことが大事
    • アクセシブルなコンポーネントが曖昧なフォルダにあってインポートもされない場合、ユーザにとっては何の意味もないものになる

Action Steps for this chapter

  • 作成したVueアプリケーションにおける見出しをチェックしてみる
  • 作ったコンポーネントがアクセシブルなものかをチェックしてみる
  • W3Cの公式バリデータを使用してみてチェックしてみる

チャプター4:Make typical components accessible

WIP

  • フォーカスマネジメントが大事
    • 本来、フォーカスはユーザーが自由にコントロールできるもので、それを変更するのは混乱を招きかねない
    • なのでプログラムでフォーカスを変更することは、やりすぎてはいけない
    • とはいえ意図的にフォーカス変更することが必要な場合もある
  • モーダルが開かれた時、スクリーンリーダーのユーザーはモーダルに移動することを期待する
  • この際にフォーカスがモーダルへ移動することも大事だが、モーダルの中でフォーカス移動を保つようにすることも大事である
  • モーダルは「モード変更」であり、他の要素を受け入れない「ブロッキング部分」でもあるため

ブロッキング部分を実装する方法について紹介

  • aria-modal
    • モーダルのコンテナ部分にaria-modal="true"を付与する
    • スクリーンリーダーにaria-modal以外のものを「裏側」のものとして認識させる
  • Disabling parts of the DOM
    • DOM を2つ用意する
      • モーダルダイアログ用のDOM
      • モーダル以外のDOM
        • モーダルが開いているときはアクセス不可視のものにする必要がある
      • aria-hidden="true" で見えないようにする
        • かなり強い制御であることを留意する
        • display:none と同一であるが、aria-hidden がある状態だと display: block もアクセスできないものになる
  • 上記2点の実装方法でどちらがいいかというと現時点ではDOMを2つ用意する方法
    • aria-modal はMac、iPadOS、iOSのVoiceOverでバグがある
  • その他、ESCキーやオーバーレイのクリックでも閉じる挙動ができることも実装できるようにしたい

Vueでモーダルを作成する方法

<a11y-dialog
  disable-native
  id="app-dialog"
  app-root="#app"
  dialog-root="#dialog-root"
  @dialog-ref="assignDialogRef">
  <template v-slot:title >
    <span>タイトル</span>
  </template>
  <div>
    <p>コンテンツ</p>
  </div>
</a11y-dialog> 
  • ダイアログを開くためのメソッドがある(openDialog()
  • assignDialogRef メソッドを使ってどの要素を開くか伝える
  • マウントするdivを別途容易する
<div id="app"></div>
<div id="dialog-root"></div> 

フォーカス位置を正しく設置する

  • モーダルが開かれた時、a11y-modal では閉じるボタンにフォーカスが移動する
  • WAI-Authoring Practice では最初のテキストに移動するのがよいとされている
  • 開いたモーダルダイアログで最初のインタラクティブ要素にフォーカスを当たるのはそれはそうとも言える
  • しかしスクリーンリーダーユーザーは開いた直後にフォーカスしたものが「閉じる」だとイラッとすることがある
  • その場合、モーダルダイアログ自体にフォーカスがあたることが好ましいのでは、と考える専門家もいる
<div id="dialog" aria-labelledby="DialogHeadlineId" role="dialog" tabindex ="-1"></div> 
  • フォーカスを正しく置くことについては現実解がない
  • Adrian Roselliの記事では多くのユーザにテストしてもらう必要がある結論になっている
    • Authoring Practice に乗り続けることは一部のユーザを刺激しかねないため
    • 自分の中で仮定しているものをユーザーテストしてもらおう

<dialog> は壊れている

  • a11y-dialog のコードにdisable-nativeというのがあるのが気になった
  • Using ARIAでもネイティブのものを使ってほしいという話が載っているので<dialog>を使うべきでは?と思うかもしれない
  • <dialog>は現在アクセシビリティ観点で大きい問題があるため使用しないほうがよいとされているらしい。
  • Webアクセシビリティの専門家でもあるScott O'Haraの記事でも "But with its current issues, it presently makes little sense to use the dialog element at all." と述べている。
  • スキップリンクはマウスを使わずに操作する人のためのアンカーリンク
    • ほとんどの場合、フォーカスしたときに出現する
    • ドキュメントの最初の部分にフォーカスが当たるようにする
  • Vueで使う場合はvue-a11y/vue-skip-toというものがある
    • Vue2: npm install @vue-a11y/skip-to --save-dev
    • Vue3: npm install vue-skip-to-next --save-dev
  • <VueSkipTo /> というグローバルコンポーネントで使う
    • toで飛び先idを指定、labelでスキップリンクのテキストを指定する
    • デフォルトでto="#main", label="Skip to main content"と指定されている

Slide-In Navigation

WIP

Tab Component

WIP

Aside: Component libraries

  • コンポーネントライブラリにアクセシビリティ機能を売り込むクリエイターは稀
    • 軽量だったり見た目の美しさを強調していたりする(これ自体は悪いことではない)
  • アクセシビリティに関する内容がドキュメントの奥深くまで探さないといけないことが問題
    • さらにアクセシビリティ対応しているのはごくわずか
  • 悲しいことに Web 開発全体においてアクセシビリティの優先順位は低く、コンポーネントライブラリも例外ではない
  • WAI-ARIA オーサリング・プラクティスに準じて、それを忠実に再現しようとしていたりするものがある
    • 第2章を読み直すとなぜそれではうまくいかないかが分かる
  • アクセシビリティテストもされておらず、ページ自体のセマンティクスやスタイリング、動作で大きい問題を起こしかねない
  • コンポーネントライブラリにおける分析をして以下は結論になる
    • 「plug and play」ライブラリのコンセプトみたいに単純に約束できるものではない

  1. コンポーネントライブラリの候補をアクセシビリティの観点から吟味しよう。
  2. 一般的にアクセシブルとされていて、実際に障害者と一緒に定期的にテストされているコンポーネントライブラリに注目しよう。Vue で提供されていない場合、Vanilla JavaScript で提供されているものを探す。あとはそれを Vue に変換するだけ
  3. 合理的で、アクセシブルで、完璧な、インストールが簡単なVue用のコンポーネントがすぐに登場することを確信しています。

Action Steps

  • Vuetify 、Buefy や Bootstrap-Vue を使用している場合、ライブラリのアクセシビリティについてを調査する
  • 既存の Vue プロジェクトにスキップリンクがないか確認する
    • 万一ない場合は、少なくとも1つ(#main)追加することを検討してください。
  • オフキャンバスやタブコンポーネント自体を構築しないように検討してみてください。

チャプター5:Convey changes of state to screen-readers

WIP

チャプター6:Testing for accessibility

WIP

  • アクセシビリティのテスト手法
    • シナリオAはアクセシブルなテストスイートを書く
    • シナリオBはビジネスロジックを書く前にアクセシビリティテストを書く
  • 自動テストについてはすべてグリーンになったとしてもアクセシブルなものを保証できるとは限らない
    • 自動検出されるものは限られている
  • アクセシビリティとは多様性とこれまでの経験に関わる部分である

Automated tests are no miracle workers

  • 自動テストは開発の助けにはなりますが、確実にアクセシブルになるものかというと違う
  • 例えば alt について
    • alt属性が付与されているかどうかというのは機械的に自動テストをかけることができます
    • 挿入されたaltテキストが適しているかどうかの判定まではテストできない
  • アクセシブルデザインについてのチェックは機械ではなく人間がするものである
  • 自動的にアクセシビリティの問題点を抽出するのは全体で見て30%しかできない
  • その結果を知ってがっかりするかもしれないけど、それでもテストをしないよりはマシ

Accessibility Testing as education

  • アクセシビリティテストは2つの点において通常のテストと同様貢献するものがある
    • 重要なコンテンツをカバーできていると個人やチームが拡張したりリファクタリングするときにも壊すことがない
    • ドキュメントとしての機能
  • アクセシビリティテストに失敗した面々はそこから学ぶきっかけを得られる
  • とはいっても自動テストが完璧ではないことは否定できないし、したくない
    • 偽陽性や偽陰性が発生する可能性がある
  • しかしそれを上回るメリットがあると信じてる
    • アクセシビリティがチームにとって重要であることを伝えることができる
    • プロジェクトのアクセシビリティレベルが維持される価値がある証拠になる
      • さらに良い方向へ拡大することもできる?
    • 初めて触れるチームメンバーに出発点を提示することができる

Linting

  • アクセシビリティリンターの紹介
    • 一般的なものは ESLint
    • 有用なものとして eslint-plugin-vuejs-accessibility があげられる
      • .vue ファイルのアクセシビリティチェックをしてくれる
    • npm install eslint-plugin-vuejs-accessibility --save-dev

Checks in your browser

Unit Tests

  • もし jest を使っている環境であれば、jest-axe を使ってみる
    • (1) jest-axe を読み込んで
    • (2) expect で toHaveNoViolations で使用できるか確認する
const { axe , toHaveNoViolations } = require('jest-axe');  // (1)
expect . extend ( toHaveNoViolations );  // (2)

img に alt が入っていないかをチェックするテスト

it ('matcher の使い方を示す' , async () => {
  const render = () => '<img src="#"/>' ; 
  const html = render(); 
  expect(await axe(html)).toHaveNoViolations(); 
})

上記はエラーが出て、解決方法として deque のリンクが提示される。
Vue のテスト方法については Vue Test Utils Edd Yerburgh 氏の Testing Vue.js Applications を参考にされたし。

フォーカスのチェックをするテストは以下参考

expect(document.activeElement).toBe(container.querySelector('input')) 

フォーカス可能な要素かの一覧:Focusable Elements - Browser Compatibility Table

End-to-end, or inter-component testing

  • ユニットテストだけでは不十分なことがある
    • 単一のコンポーネントに限定されるわけではない
    • パーツの相互作用がインクルーシブを担保することもある
  • モーダルにおいてはモーダル内でのフォーカスだけではなく、モーダルから外れた後のフォーカス管理のテストも必要である
  • コンポーネントレベルに限定するテストツールでは相互作用でアクセシビリティが守られているかをテストはできない
  • その代わりにブラウザ上でエミュレートする Cypress テストが適している
  • vue-a11y-dialog をテストするにあたり以下のような spec を記述する
describe('最初のテスト', () => {
  it('モーダルの開閉時にフォーカスが管理されるかどうかをチェック', () => {
    cy.visit('/');
    cy.get('button#modaltrigger').click();
    // 閉じるボタンをクリック
    cy.get('[data-a11y-dialog-hide]').focused().click();
    cy.get('button#modaltrigger').focused();
  })
}) 

上記テストは以下の流れをテストしている

  1. ホームページにアクセス
  2. modaltrigger というIDのボタン要素を探す
  3. そのボタンをクリックする
  4. モーダルのクローズボタンがフォーカスされることか確認
  5. 閉じるボタンをクリック
  6. modaltrigger の id を持つ button 要素にフォーカスが戻るか確認

上記例は単純なものなので、Cypress でテストするほどのものではないが、コンポーネントテストで実施できなかったテストのギャップや不足分を埋めるためのものである。

チャプター7:Keep on learning

WIP

作成者以外のコメントは許可されていません