NVDA 行く年くる年

この記事は Web Accessibility Advent Calender 2017 の10日目の記事です。
オープンソースの Windows 用スクリーンリーダー NVDA に関するこの1年の活動を振り返るポエムで乗り切ろうと思ったのですが、ゆうべ広島フロントエンド勉強会で喋ったら気が変わってしまいました。
11月11日 JAC Vol.1 の講演「アクセシビリティ検証ツールとしてのNVDA入門」での失敗をフォローします。
決済サイト試作の入力バリデーション画面例

アクセシブルな決済システムの試作

アクセシビリティで難しそう、めんどくさそう、スクリーンリーダーで実際に検証しないと不安なポイントはやっぱりセキュリティだろう。
試してみたくなったのがクレジット決済 API を使うサイトの開発だった。
11月11日に見せようとした試作サイトは、以下のような方針で開発した。

決済サービスの選択

  • テストモード、つまり、ダミーのカード情報を送信して実際にWebアプリのテストができること
  • カード情報を Web アプリが保存しなくてよい、できれば受け取る必要すらないこと(情報漏洩のリスクを回避)
  • Python から簡単に使えること(個人的な好みです)

実際に使った決済サービスの仕様

  • JavaScript ライブラリがあり、ブラウザが決済APIサーバと直接通信を行い、カードと紐付いたトークン情報だけが返ってくる
  • そのトークン情報を Web アプリが受け取って、実際の課金を行うことが(Python ライブラリから)できる

つまり、ブラウザの中で「ユーザーにカード情報を入力させて、決済サーバに送って、カードのトークンを受け取る」という処理を、画面遷移せずに完結させたい、ということになる。
これは、セキュリティのために「動的なコンテンツを作らざるを得ない」ということで、アクセシビリティの題材としてはよさそうだと思った。

11月11日の前に作っていたもの

コードの抜粋はこちら
環境は Google App Engine for Python + Flask
(1) ログインする前の画面
ログインしてください、という表示になり、ログインのリンクがある。
実際にログインで使っているのは Google アカウント認証。
これが使いたかったので GAE を選んだ。本人確認されたユーザーのメールアドレスを取得。Google のログイン画面はスクリーンリーダーでちゃんと使えるはず。
ログインする前とログインしたあとの画面は Jinja2 (Python のテンプレートエンジン)で分岐している。
(2) ログインした直後の画面
ログインすると最初の画面が入力フォームになっている。
上から順に、以下のものがある。

  • テキスト「カード情報を入力して「確認」を押してください」
  • 4個の入力フィールド:カード番号、有効期限(年)、有効期限(月)、CVC番号
  • 「確認」ボタン(有効状態)
  • 「購入」ボタン(無効状態)
  • 「キャンセル」ボタン(無効状態)

ただし、デモなので、入力フィールドには決済サービスのテスト用カード番号があらかじめ入れてある。
確認ボタンを押すと URL が遷移するのではなく、JavaScript の処理が実行されて、コンテンツが動的に変化する。
(3) カード情報が正しくない場合の「確認」結果
「カード番号が正しくありません」などが表示され、入力フィールドの修正と「確認」ボタンしか有効にならない。
何が正しくないかというメッセージはテキスト( aria-live=”assertive” 属性の div )として表示され、スクリーンリーダーで読み上げられる。
つまり動的コンテンツだがバリデーション結果をアクセシブルに提示できる。
(4) カード情報が受理された場合の「確認」結果
「カード情報が確認できました。「購入する」を押すと***円のお支払いが完了します。」
のように表示され、「購入」ボタンと「キャンセル」ボタンが有効になり、入力フィールドと「確認」ボタンが無効になる。
購入ボタンを押すとカードのトークンが submit されて URL が遷移する。Python アプリで決済 API をたたいて結果を取得、次のページに「購入完了」みたいな表示を出す。
「キャンセル」を押すと、入力フィールドの値がふたたび変更可能になる。

本番でやったドジなこと

確認ボタンを押して入力フィールドが無効になった状態で、「じゃあ今度は値をわざと変更して正しくない入力内容にしてみましょう」とか言いながら、無効になっている入力フィールドのテキストを書き換えようとしてうまくいかず、デモを中断してしまった。

そのあとで考えたこと

aria-live=”assertive” とか、フォーカスの制御とか、invalid とか、いろいろ盛り込んだのだが。。
そもそも「自分がうっかり間違えるような UI 設計がよくない」と考え直した。

最近作り直したもの

(1) ログインする前の画面
ログインしてください、という表示になり、ログインのリンクがある。
(2) ログインした直後の画面
ログインすると最初の画面が入力フォームになっている。
上から順に、以下のものがある。

  • テキストA「カード情報を入力して「確認」を押してください」
  • 4個の入力フィールド:カード番号、有効期限(年)、有効期限(月)、CVC番号
  • 「確認」ボタン(有効状態)

(3) カード情報が正しくない場合の「確認」結果
テキストA「カード番号が正しくありません」などが表示され、入力フィールドの修正と「確認」ボタンが使える状態のまま
(4) カード情報が受理された場合の「確認」結果

  • テキストB「カード情報が確認できました。「購入する」を押すと***円のお支払いが完了します。」
  • 「購入」ボタン
  • 「キャンセル」ボタン

その他のものは非表示になる。
「キャンセル」を押すと、テキストAと入力フィールドだけの表示に戻る。

振り返って思うこと

なんのことはない、これはひとつの HTML ファイルにダイアログAとダイアログBがあって、交互に表示・非表示になる、一種のモーダルである。
たぶん最初の「非表示・無効」だらけの画面を作っていたときには、「この操作を実際に行うと、次にどういう段階に進むのか」みたいな見通しを伝えたかった。
だから、あとから有効になるけどいま無効なものをたくさん作ってしまった。
その流れで「さっきは有効だったけどいまは無効になった」ものも、たくさん残してしまった。
だが、けっきょくのところ「ひとつひとつの段階で迷わせない」「無関係なものを見せない」ということのほうが大事だったと思われる。
カード情報が確認できたときに、もういちどカード番号を表示する、みたいなことは、本当はやった方がいいのか、やっぱり不要なのか。。

こまかいこと

今回のデモでは invalid の判定には2種類ある。
(1) HTML の required と pattern を使う処理
(2) 決済APIサーバーのエラーメッセージに基づく処理
後者の場合には、JavaScript のコールバックで

<div id="input_guide" class="alert alert-primary" role="status" aria-live="assertive"></div>
<div class="col-12">
  <label for="card_number">カード番号</label>
  <input type="text" id="card_number" class="form-control" aria-describedby="card_number_help" value="5555555555554444" required aria-required="true" pattern="[0-9]{13,16}" />
  <small id="card_number_help" class="form-text text-muted">半角数字 <span aria-hidden="true">省略できません</span></small>
</div>
if (response.error.code === 'invalid_number') {
  $('#card_number').attr('aria-invalid', true).focus();
  $('#input_guide').text('カード番号が正しくありません').addClass('alert-warning').removeClass('alert-success');
}

みたいなことをする(jQuery を使っている)。
JavaScript 2行目の最後の .focus() がなければライブリージョンの「カード番号が正しくありません」が読み上げられる。
一方で、この .focus() をつけてしまうと card_number 要素の読み上げしか聞くことができない。
NVDA 2018.1jp-beta-171206x + Firefox ESR 52.5.2 でスピーチビューアを使って確認すると:

ブラウズモード
カード番号が正しくありません
カード番号  エディット  正しくない入力内容  必須  オートコンプリート  半角数字 省略できません
55555555555544 選択
フォーカスモード

つまり、ライブリージョンの読み上げの直後にフォーカス位置の読み上げイベントが発生して、直前の読み上げを止めてしまうのだ。
(「フォーカスモードとブラウズモードの切り替えを音で報告」はチェックなし。後述の追記2を参照のこと)
実は最初のバージョンはこれが気に入らなくて(だって live region のデモにならないから)フォーカスを移動しなかったのだが、考えてみるとライブリージョンを読むことよりもフォーカスをエラー箇所に移動してあげることのほうがずっと親切で、ユニバーサルな気がする。
さらに、フォーカスを読み上げるときに「正しくない入力内容」もちゃんと読み上げているのだ。
本当にスクリーンリーダーで検証するべき「アクセシビリティのディティール」というのはこういうことなんだろうな、と思っている。

ここから追記

追記:NVDA の live region の実装がおかしいのではないか、と言われそうな気もするので(そうかも知れないけど)ちゃんと制御できるケースも紹介しておく。

$('#card_number').attr('aria-invalid', true);
$('#get_token_btn').focus();
$('#input_guide').text('カード番号が正しくありません').addClass('alert-warning').removeClass('alert-success');

こんなふうにボタンにフォーカスを移動して、直後に live region の内容を更新すれば、「カード番号が正しくありません」をちゃんと聞くことができる。
(このテクニックは最初のバージョンで使っていた)
追記2:さらに調べたら、NVDA の「フォーカスの変化を追跡する自動フォーカスモード」をチェックなしにすると、エディットにフォーカスが移動したときに live region を最後まで読み上げて、それからフォーカスを読み上げるようになった。
ボタンとエディットで挙動が違うのだから、もしやと思ったら、やっぱりそういうことだったか。。

おことわり

勘のいい人は shop.nishimotz.net というサイトを見つけたかも知れませんが、このサイトは「本当に何かを販売して決済を行うサイト」に作り替える予定の場所です。
明日は tosssaurus さんの「社内じんわりアクセシビリティ普及活動報告」だそうです。お楽しみに。