📚

React Native アクセシビリティ対応をわかるための記事: 入門編

2021/12/03に公開

この記事は React Native アドベントカレンダー 2021 の 3 日目の記事です。
React Native のアクセシビリティ対応について知るための記事です。


対象のバージョン、環境

React Native 0.66.0 までの iOS と Android を対象としてます。
React Native for Web は対象外です。
実機での動作は iOS 14, Android 11 で確認しています。

また、サンプルは全て Expo Snack(Expo SDK 43, React Native 0.64.3 相当)で書かれています。
手元の iOS か Android 端末に Expo Go をインストールすることで、実機での動作を確認できます。


アクセシビリティとは

アクセシビリティは、「アクセスしやすい」を意味する言葉です。
「人、環境、状況に関係なくアプリが使える」状態を指すと考えるとわかりやすいです。
Web アクセシビリティについては、ymrl さんの記事が参考になります。
Webエンジニアとしていま知っておきたいWebアクセシビリティ


モバイルアクセシビリティを知る

まずはモバイルアクセシビリティを知ることから始めます。

ドキュメントを読んでみる

モバイルアプリ自体には、Web でいう WCAG(Web Content Accessibility Guidelines) のようなアクセシビリティのガイドラインがあるわけではありません[1]し、具体的な対応方法や使えるツールも異なります。
とは言っても、モバイルアプリにおけるアクセシビリティの考え方は、Web アクセシビリティとほとんど同じです。まずは、いくつかドキュメントを読んで、モバイルアプリでどのような対応があるのかを学ぶのが良さそうです。

この記事を書いた時点(2021年12月)での話ですが、最初に React Native 公式ドキュメントの Accessibility ページを読むのはオススメしません。
このページに書かれているのは React Native で使える機能のリファレンスでしかなく、「アクセシビリティ対応をなぜやるのか、どうやるのか」といった視点が欠けているからです。

まずは、iOS の Human Interface GuidelinesAndroid のアクセシビリティガイド を読んで、Web やモバイルアプリの開発で、どのような対応があるかを知ることをオススメします。
他にも Flutter のドキュメント などが参考になります。

React Native のドキュメントを最初に読みたい場合は、レビューが止まっているアクセシビリティドキュメントの PR がオススメです。
昨年に出されたもので、今あるドキュメントより良くまとまっています。

スクリーンリーダーを使ってみる

React Native アプリの実装で行うアクセシビリティ対応の多くは、「支援技術が UI にアクセスできるようにする」というものです[2]
支援技術には、スクリーンリーダーやスイッチコントロールなどがあります。それらは Web 上でキーボードの Tab キーを押した時のように、アクセス可能な要素にフォーカスします。
画面上のコンポーネントにフォーカスできなかったり、フォーカスできても情報が読みとれなければ、その UI は支援技術でアクセスできたことになりません。

身近な支援技術には、iOS の VoiceOver や Android の TalkBack と呼ばれるスクリーンリーダーがあります[3]
モバイルアプリでは、VoiceOver と TalkBack の使用率が上位を占めています[4]。これらに対応できていることを、最初の目標にするのが良いです。

VoiceOver や TalkBack はジェスチャーで操作します。実際に使った方がわかりやすいです。
手元に iOS がある場合は、VoiceOver が使えます。iPhoneでVoiceOverをオンにして練習する - Apple サポート (日本)iPhoneのVoiceOverジェスチャについて - Apple サポート (日本) を読みながら、Web でもいいので一通り操作してみましょう。
Android がある場合は、TalkBack が使えます。 TalkBack をオンまたはオフにする - Android のユーザー補助機能 ヘルプTalkBack ジェスチャーを利用する - Android のユーザー補助機能 ヘルプ を読みながら、Web でもいいので一通り操作してみましょう。


React Native アクセシビリティの基本

React Native には、コンポーネントで設定できるアクセシビリティ関連の Props と、端末の情報を取得したり、読み上げやフォーカスを制御する AccessibilityInfo API を持ちます。
それらを駆使して、UI の情報を支援技術に伝えたり、アクションさせたりします。

組み込みコンポーネントを使えばいいのか?

HTML では、「可能であればネイティブの要素を使用する」というお作法があります。
標準の要素とは、見出しであれば <h2> など、ボタンであれば <button> 要素などです。
要素や属性の組み合わせによってロール(役割)が決まり、ブラウザや支援技術に解釈されます。

<!-- 対応するロールなし(No corresponding role) -->
<div>カートの中身を確認する</div>

<!-- button 要素は button ロールを持つ -->
<button type="button">カートの中身を確認する</button>

HTML では WAI-ARIA というのも使用されます。WAI-ARIA は、要素に別のロールや追加の状態を指定して、支援技術に適切な意味を提供するものです。
ネイティブの要素だけでは表現しきれなかったり、既存のサイトをアクセス可能にするための処置などによって、 WAI-ARIA を付け足すケースは多いです。
ただし WAI-ARIA は、ネイティブの要素が持っている振る舞いを再現しないことがあります。
WAI-ARIA は魔法ではなく、可能な限りアクセス可能にするための手段です。

<!-- 良くはない -->
<div>カートの中身を確認する</div>

<!-- ボタンの役割を持つが、ブラウザがボタンに本来持っている振る舞いを再現しない -->
<div role="button">カートの中身を確認する</div>

<!-- 良いが、ブラウザがボタンに対して仕様に則ってスタイリングをする -->
<button type="button">カートの中身を確認する</button>

一方で React Native で使える組み込みコンポーネントは、普通に使うとまともになりません。
普通に使っても罠に引っかかることがあります[5]
この世界での WAI-ARIA は魔法です。ガンガン使っていきましょう。


ラベルを設定する

支援技術がコンポーネントにフォーカスした時、それにラベルがあり、どんな役割があって、今どんな状態や値を持っているのかが支援技術に伝われば、視覚的な情報に依存せずともアクセス可能に近づきます。
まずは、タップ可能である Pressable にラベルを設定してみます。

スクリーンリーダーでは Pressable のコンポーネントにフォーカスした時、中に Text があると、そのテキストを読み上げます。
つまり Text の内容が「ラベル」として解釈されています。

// Pressable にラベル(テキスト)がある
<Pressable onPress={props.handlePress}>
  <Text>カートの中身を確認する</Text>
</Pressable>

では Text ではなく Image がある場合は、どうなるのでしょうか。
ラベルと呼べるものがないので、支援技術に何の情報も伝わりません。

// Pressable にラベルがない
<Pressable onPress={props.handlePress}>
  <Image source={source} />
</Pressable>

そこでアクセシビリティの Props が役に立ちます。
accessibilityLabel を設定すると、テキストの代わりにその文字列をラベルとして解釈してくれるようになります。
この Props は、iOS の accessibilityLabel、Android の android:contentDescription に割り当てられるものです。

<Pressable
  accessibilityLabel="カートの中身を確認する"
  onPress={props.handlePress}
>
  <Image source={source} />
</Pressable>

どこをラベルとして認識するかについてですが、親に accessibilityLabel がある場合は、その内容がラベルとして認識されます。
親にラベルがなければ、子の Text または accessibilityLabel を探索してラベル化します。

// 親の「カートの中身を確認する」がラベルになる
<Pressable
  accessibilityLabel="カートの中身を確認する"
  onPress={props.handlePress}
>
  <Text>カート</Text>
</Pressable>

ここまでの挙動を確認するための Expo Snack を用意しました。実機のスクリーンリーダーで動かして、挙動を確認してみてください。


ロールとステートを設定する

次に、ロールとステートを設定してみます。
Web では WCAG 2.1 の Success Criterion 4.1.2 に "Name, Role, Value" という項目があります。
スクリーンリーダーなどの支援技術がコントロール可能な UI を認識するために、Name(今回の場合は Label[6]), Role, Value があるといいですよ、というお話です。
Label はコンポーネントのラベル、Role はコンポーネントのロール、Value は値や状態を指します。

React Native では、これらを抽象化した Props を設定し、UI に情報を与えられます。
大体の Props は、accessibility プレフィックスを伴って命名されています。
先ほど accessibilityLabel を設定した Pressable に, accessibilityRole, accessibilityState も設定してみます。

// Pressable にラベル、ロール、状態を追加
<Pressable
  accessibilityLabel="カートの中身を確認する"
  accessibilityRole="button"
  accessibilityState={{ disabled }}
  disabled={disabled}
>
  <Image source={source} />
</Pressable>

これで、このコンポーネントは「カートの中身を確認する」というラベルがあり、「ボタン」という役割を持ち、disabled として認識されることもある、アクセス可能なコンポーネントになりました。
使用できるロールは、公式ドキュメントの accessibilityRole の項目、ステートは accessibilityState の項目で確認できますが、全てを覚える必要はないです。

Web 開発者であれば、Pressable は button ロールを持ってるんじゃない?accessibilityRole いるの?という疑問を持つかもしれません。
Pressable というのは、実際には押下可能な View の Wrapper です。ボタンにならない場合もあるため、デフォルトでロールを持っていません。
そのため accessibilityRole を指定する必要があるのです。必ずしもロールが必要なわけではありませんが、ネイティブに従うのであればやっておきたいです。

ラベルとロールの挙動で、ひとつ罠があります。
accessibilityRole が指定されているコンポーネント自体に accessibilityLabel が指定されていない(子にラベルがある)場合、TalkBack でロールとラベルの読み上げ順が逆になります
accessibilityRole が指定されているコンポーネントには、たとえ子にテキストなどのラベルがあっても accessibilityLabel が指定されていないと正しくなりません。

{/* Label と Role はセットでないといけない */}
<Pressable
  accessibilityLabel="カートの中身を確認する"
  accessibilityRole="button"
>
  <Text>カートの中身を確認する</Text>
</Pressable>

ここまでの挙動を確認するための Expo Snack を用意しました。実機のスクリーンリーダーで動かして、挙動を確認してみてください。


フォーカス可能にする

Pressable は支援技術がフォーカスできるようになっていましたが、ViewImage など、フォーカスできないコンポーネントもあります。
それらをフォーカス可能にする場合は accessible={true} を設定します。

<Image
  accessible
  accessibilityRole="image"
  accessibilityLabel="ショッピングカート"
  source={require('shopping_cart.png')}
/>

accessible は React Native の基礎中の基礎となるはずの Props ですが、深淵です。
詳しくは次回の記事で取り上げます。今は深く知らなくても大丈夫です。


ヒントを設定する

コンポーネントに accessibilityHint を設定すると、ヒントを指定できます。

<Pressable
  accessibilityHint="購入手続きへ進む"
  accessibilityLabel={item.name}
  accessibilityRole="button"
>
  {item.name}
</Pressable>

ヒントは、iOS の accessibilityHint を指します。
ヒントが何かというと「それ自体のアクションに対する結果が、コンテキストから予測できないコンポーネントに提供する説明」です。
なんのこっちゃ、という話ですが、複数のアイテムが一行ずつ並んでいる「リスト形式」などのケースで考えてみます。
モバイルアプリはレイアウトの都合上、リスト形式での表示が結構あります。リストのアイテムをタップすると、音楽を再生するかもしれませんし、商品をカートに入れるのかもしれません。
前後のコンテンツによっては、実行した結果どうなるかが伝わらないことが考えられます。
ヒントは、そのようなケースをカバーする際に使うものです。

accessibilityHint には癖があります。
Android には iOS のようなヒントがなく[7]、その代わりなのか React Native では accessibilityHint の値をラベルとして追加します。これは、ネイティブアプリにない余計なお世話なので注意してください。

しかも、accessibilityRoleaccessibilityHint が指定されたコンポーネントにラベルが存在せず、子にラベルがある場合は、TalkBack でラベルが読み上げられなくなります。
非常に悲しいですが、accessibilityRoleaccessibilityHintaccessibilityLabel はセットで指定する必要があります。

ヒントの挙動を確認するための Expo Snack を用意しました。実機のスクリーンリーダーで動かして、挙動を確認してみてください。


accessibilityAction で、端末上の操作を取得する

モバイルの UI は、特殊なジェスチャーで操作できる場合があります。
例えば支援技術がスライダーにフォーカスしている場合、上下にスワイプして値を変更できます。Android であれば、音量ボタンで操作できる設定もあります。
他にも、iOS ではエスケープジェスチャ(画面を左側から横にジグザグ移動するジェスチャ。形で表すと Z の文字です)を使ってモーダルを閉じたりできます。
自前のコンポーネントでそれを再現する場合は、 accessibilityActionsonAccessibilityAction の Props が必要となります。
今回はスライダー UI を例に、ジェスチャーで操作するための設定をしてみます。

まずは、スライダーとなる ViewaccessibilityActions を指定します。
accessibilityActions は、 namelabel(optional) のキーバリューを持つ object の配列です。
この配列に加えたアクションは、onAccessibilityAction で listen できるようになります。
どのようなアクションがあるのかは、公式ドキュメントにある accessibilityActions の項目に載っています。

スライダーに必要な上下スワイプの操作は incrementdecrement アクションとして割り当てられています。
increment と decrement アクションは、adjustable ロールを持つコンポーネントで検知できます。

<View
  accessible
  accessibilityRole="adjustable"
  accessibilityActions={[
    {
      name: 'increment',
    },
    {
      name: 'decrement',
    },
  ]}
/>

accessibilityActions に渡したアクションは、onAccessibilityAction で取得できます。
コールバックの event.nativeEvent.actionName に、取得したアクションの名前が入ってきます。
今回は increment か decrement のイベントが入ってくるはずなので、それらに対応したスライダーの処理を書きます。

  onAccessibilityAction={(event) => {
    const { actionName } = event.nativeEvent;
    if (actionName === 'increment' && val < 100) {
      setVal(val + 1);
    }
    if (actionName === 'decrement' && val > 0) {
      setVal(val - 1);
    }
  }}

その他、accessibilityValue の値などを入れます。
支援技術でアクセス可能なスライダーができました。

<View
  accessible
  accessibilityActions={[
    {
      name: 'increment',
    },
    {
      name: 'decrement',
    },
  ]}
  onAccessibilityAction={(event) => {
    const { actionName } = event.nativeEvent;
    if (actionName === 'increment' && val < 100) {
      setVal(val + 1);
    }
    if (actionName === 'decrement' && val > 0) {
      setVal(val - 1);
    }
  }}
  accessibilityLabel="スライダーのサンプル"
  accessibilityRole="adjustable"
  accessibilityValue={{
    max: 100,
    min: 0,
    now: val,
  }}
  style={styles.track}>
  <View style={[styles.thumb, { width: `${val}%` }]} />
</View>

accessibilityAction の挙動を確認するための Expo Snack を用意しました。実機のスクリーンリーダーで動かして、挙動を確認してみてください。タップ操作には対応していません。


AccessibilityInfo API で、アクセシビリティの設定を取得する

端末から、何らかのアクセシビリティ設定を取得したい場合があります。
例えば、スクリーンリーダーを有効にしているかなどです。
設定を取得するには、AccessibilityInfo API を使用します。

今回は、スクリーンリーダーが有効か無効かをチェックするために screenReaderChanged を EventListener に追加してみます。
Discord でスクリーンリーダーをオンにすると専用のダイアログが出てきますが、あれと同じようなことができます。

const [screenReaderEnabled, setScreenReaderEnabled] = React.useState(false);
React.useEffect(() => {
 const screenReaderChangedSubscription = AccessibilityInfo.addEventListener(
   'screenReaderChanged',
   (screenReaderEnabled) => {
   setScreenReaderEnabled(screenReaderEnabled);
   }
 );
 return () => {
  screenReaderChangedSubscription.remove();
 };
}, []);
return (
  <View style={styles.container}>
    <Text style={styles.text}>スクリーンリーダーが{screenReaderEnabled ? '有効' : '無効'}</Text>
  </View>
);

AccessibilityInfo API では他に特有の使い道があるのですが、この記事では省略します。

AccessibilityInfo API の挙動を確認するための Expo Snack を用意しました。実機でスクリーンリーダーを有効にしているかによって、文字列が「スクリーンリーダーが有効」か「スクリーンリーダーが無効」に変わるようになっています。


まとめ

この記事では、6 つのことをやりました。

  1. モバイルでのアクセシビリティ対応を知る
  2. スクリーンリーダーを使う
  3. ラベル、ロール、状態を指定する
  4. ヒントを指定する
  5. ジェスチャーを取得する
  6. スクリーンリーダーの状態を取得する

詰め込みすぎましたが、これでアクセシビリティに関する基礎的な対応が大体学べました。
この状態で React Native の公式ドキュメントを読んだりすると、より理解が深まると思います。
また、accessible な UI ライブラリを見るのも良いです。
個人的にオススメなのは React Native Paper です。

次回「フォーカス編」では、基本ながらつらい部分でもある「支援技術がフォーカスするためのルール」について説明します。

脚注
  1. 次世代の WCAG3 ドラフトではモバイルアプリを含むことが明記されています。https://www.w3.org/TR/wcag-3.0/ ↩︎

  2. 機械が読み取り可能なデータ形式であることを"Machine-readable(マシンリーダブル)"と言います。 https://accessible-usable.net/2009/01/entry_090109.html ↩︎

  3. 端末によっては、オリジナルのスクリーンリーダーが搭載されている場合もあります。例えば、富士通の「らくらくスマートフォン」には独自の読み上げエンジンがありました。 http://www.android-group.jp/conference/abc2013a/files/2014/02/20131018_ABC2013Autumn.pdf ↩︎

  4. Web AIM より。https://webaim.org/projects/screenreadersurvey9/#mobilescreenreaders ↩︎

  5. 昔 FlatList というリストを表示するコンポーネントで、逆順表示する処理が TransformY をいじっただけのものなので、スクリーンリーダーでの読み上げ順がおかしくなる悲しい実装があったりしました。 ↩︎

  6. WCAG では Name と Label の定義がハッキリ分けられています。ただし、大体同じなので今回は Label と読ぶことにしています ↩︎

  7. Android では、フォーム入力に android:hint を使って、入力向けのヒントが設定できます。また、TalkBack では、ボタンや入力フォームなどの役割を持つコンポーネントにフォーカスした時、「ダブルタップで実行します」などのヒントをアナウンスします。 ↩︎

Discussion