React Native アクセシビリティ対応をわかるための記事: 入門編
この記事は 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 Guidelines や Android のアクセシビリティガイド を読んで、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
は支援技術がフォーカスできるようになっていましたが、View
や Image
など、フォーカスできないコンポーネントもあります。
それらをフォーカス可能にする場合は 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
の値をラベルとして追加します。これは、ネイティブアプリにない余計なお世話なので注意してください。
しかも、accessibilityRole
と accessibilityHint
が指定されたコンポーネントにラベルが存在せず、子にラベルがある場合は、TalkBack でラベルが読み上げられなくなります。
非常に悲しいですが、accessibilityRole
と accessibilityHint
と accessibilityLabel
はセットで指定する必要があります。
ヒントの挙動を確認するための Expo Snack を用意しました。実機のスクリーンリーダーで動かして、挙動を確認してみてください。
accessibilityAction で、端末上の操作を取得する
モバイルの UI は、特殊なジェスチャーで操作できる場合があります。
例えば支援技術がスライダーにフォーカスしている場合、上下にスワイプして値を変更できます。Android であれば、音量ボタンで操作できる設定もあります。
他にも、iOS ではエスケープジェスチャ(画面を左側から横にジグザグ移動するジェスチャ。形で表すと Z の文字です)を使ってモーダルを閉じたりできます。
自前のコンポーネントでそれを再現する場合は、 accessibilityActions
と onAccessibilityAction
の Props が必要となります。
今回はスライダー UI を例に、ジェスチャーで操作するための設定をしてみます。
まずは、スライダーとなる View
に accessibilityActions
を指定します。
accessibilityActions は、 name
と label(optional)
のキーバリューを持つ object の配列です。
この配列に加えたアクションは、onAccessibilityAction
で listen できるようになります。
どのようなアクションがあるのかは、公式ドキュメントにある accessibilityActions の項目に載っています。
スライダーに必要な上下スワイプの操作は increment
と decrement
アクションとして割り当てられています。
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 つのことをやりました。
- モバイルでのアクセシビリティ対応を知る
- スクリーンリーダーを使う
- ラベル、ロール、状態を指定する
- ヒントを指定する
- ジェスチャーを取得する
- スクリーンリーダーの状態を取得する
詰め込みすぎましたが、これでアクセシビリティに関する基礎的な対応が大体学べました。
この状態で React Native の公式ドキュメントを読んだりすると、より理解が深まると思います。
また、accessible な UI ライブラリを見るのも良いです。
個人的にオススメなのは React Native Paper です。
次回「フォーカス編」では、基本ながらつらい部分でもある「支援技術がフォーカスするためのルール」について説明します。
-
次世代の WCAG3 ドラフトではモバイルアプリを含むことが明記されています。https://www.w3.org/TR/wcag-3.0/ ↩︎
-
機械が読み取り可能なデータ形式であることを"Machine-readable(マシンリーダブル)"と言います。 https://accessible-usable.net/2009/01/entry_090109.html ↩︎
-
端末によっては、オリジナルのスクリーンリーダーが搭載されている場合もあります。例えば、富士通の「らくらくスマートフォン」には独自の読み上げエンジンがありました。 http://www.android-group.jp/conference/abc2013a/files/2014/02/20131018_ABC2013Autumn.pdf ↩︎
-
Web AIM より。https://webaim.org/projects/screenreadersurvey9/#mobilescreenreaders ↩︎
-
昔 FlatList というリストを表示するコンポーネントで、逆順表示する処理が TransformY をいじっただけのものなので、スクリーンリーダーでの読み上げ順がおかしくなる悲しい実装があったりしました。 ↩︎
-
WCAG では Name と Label の定義がハッキリ分けられています。ただし、大体同じなので今回は Label と読ぶことにしています ↩︎
-
Android では、フォーム入力に
android:hint
を使って、入力向けのヒントが設定できます。また、TalkBack では、ボタンや入力フォームなどの役割を持つコンポーネントにフォーカスした時、「ダブルタップで実行します」などのヒントをアナウンスします。 ↩︎
Discussion