多くのお客様から、Instacartを利用して貴重な時間を取り戻していると言われていますが、特に体が不自由なユーザーが私達の利用して、必要な食料品を入手するため大切な方法として私達に信頼を寄せています。 Instacartのカスタマーエンジニアリングチームは、すべてのお客様のためにシームレスなアプリ内購入体験を構築するために一生懸命努力しています。そして特に障害のあるお客様に対して、我々はアプリ内購入のアクセシビリティーを真剣に考えています。
私たちが直面している最大のアクセシビリティの課題の1つは、エレガントな(そして実用的)Webモーダルを構築することです。 スタンダードな状況では、私たちはReact-Modalを使います。 問題が解決しました。 残念ながら、それほど単純ではありません。 デザインや機能的な状況によっては、カスタムモーダルが必要です。 このカスタム作業では、アクセシビリティ(ユーザー支援)の問題に正面から取り組む必要があります。 これらのベストプラクティスに従うことで、私たちのモーダルは、顧客が必要とし、レベルの高いアクセシビリティを確実に提供することに役立ちます。
- モーダルDOMの初期に利用可能な閉じる(close)ボタンがあることを確認してください。
- フォーカストラップ(focus trap)を維持し、ESCが押されたら閉じる
- 閉じると元の要素にフォーカスを戻す
- 正確な役割とラベルを導入する
- 適切なheadingを含める
- スクリーンリーダーを使用したテスト
モーダルDOMの早い段階で閉じる(close)ボタンを利用できるようにする
明確な終了仕組み(手順)がない場合、標準モーダルから離れることはキーボードユーザーにとって非常に難しくてイライラする可能性があります。 モーダルは簡単に終了するための閉じるボタンを提供するべきです。 ユーザーがプロセス終了するために多数の要素をタブで移動する必要がないように、ボタンはコンテンツの早い段階で配置する必要があります。
キーボードのユーザーエクスペリエンスを最適化するために、モーダルを開くときにフォーカスする方がいいです。
場合によっては、モーダル内またはコンテンツの冒頭に閉じるボタンを追加すると、理想的な製品デザインと矛盾します。 便利な方法の1つは、フォーカスがあるときに表示される隠しボタンを作成することです。
画面外の位置に設定し、フォーカスを受け取ったときに画面上に表示させ、見えないボタンを作成します。 これにより、これにより、キーボードユーザーは要素に到達したときに簡単に終了することができます。 私たちは同時に優れたアクセシビリティ体験を提供しながら、コアデザインの目的は維持されます!
CloseButton.propTypes = { hideRetailerChooser: PropTypes.func.isRequired, } function CloseButton({ hideRetailerChooser }) { return ( <button aria-label="close store chooser" onClick={hideRetailerChooser} style={styles.button}> <SVGIcon name="x" /> </button> ) } const buttonStyles = { top: -9999, left: -9999, border: 0, background: 'none', position: 'absolute', color: colors.GRAY_46, ':focus': { top: 0, left: 0 }, }
フォーカストラップ(focus trap)を維持し、「ESC」が押されたら閉じる
フォーカストラップ(focus trap)は、モーダルのもう1つの重要なユーザー補助機能です。 トラップは、明示的に閉じられるまでフォーカスがモーダルを離れることができないようにします。 モーダルをオープンの状態でタブ移動すると、モーダル内のすべてのインタラクティブ要素が順番に表示され、最後に到達すると最初の要素に戻ります。
Ackbar提督(SF映画『スター・ウォーズ』シリーズに登場するキャラクター)
の言葉では「それは罠」であり、これにより、ユーザーは関連コンテンツ内に留まります。
これを実現するには、ユーザーが最後の要素を超えてタブ移動した場合、フォーカスをモーダルの最初の要素に明示的に戻す必要があります。 同じように、最初の要素を超えてタブ移動すると、フォーカスは最後の要素に移動する必要があります。 ReactではonKeyDownイベントを処理することで達成できます。
import React, { createRef, PureComponent } from 'react' import PropTypes from 'prop-types' function getFocusableElements(container) { return container.querySelectorAll( // https://gomakethings.com/how-to-get-the-first-and-last-focusable-elements-in-the-dom/ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ) } function nextFocusableElement(container, backward) { const focusable = getFocusableElements(container) const first = focusable[0] const last = focusable[focusable.length - 1] if (backward && document.activeElement === first) return last if (!backward && document.activeElement === last) return first } class FocusKeeper extends PureComponent { static propTypes = { children: PropTypes.node, contentLabel: PropTypes.string.isRequired, disabled: PropTypes.bool, onEscape: PropTypes.func.isRequired, } static defaultProps = { disabled: false, } wrapper = createRef() componentDidMount() { if (!this.props.disabled) { this.wrapper.current.focus() } } handleKeyDown = event => { switch (event.key) { case 'Escape': { this.props.onEscape() return } case 'Tab': { this.handleTab(event) } } } handleTab = event => { const backward = event.shiftKey const nextFocus = nextFocusableElement(this.wrapper.current, backward) if (nextFocus) { event.preventDefault() nextFocus.focus() } } render() { if (this.props.disabled) { return this.props.children } return ( <div onKeyDown={this.handleKeyDown} ref={this.wrapper} tabIndex={-1} aria-label={this.props.contentLabel} role="dialog" data-testid="wrapper" > {this.props.children} </div> ) } } export default FocusKeeper
この例では、querySelectorAllを使ってインタラクティブ要素を見つけ、document.activeElementを最初または最後のインタラクティブ要素と比べて、強制的にトラップにフォーカスする必要があるかどうかを確認します。
クローズ時にフォーカスを元の要素に戻す
モーダルが開いているとき、フォーカストラップはUXを最適化します。 モーダルが閉じたときにシームレスな流れを維持することも重要です。 モーダルを開いた元の要素にフォーカスを戻すことで、キーボードおよび補助技術(assisted technology) を利用するユーザーが自分の位置を見失われないように確保します。 戻りフォーカス制御なし。 ユーザーは前のプロセスが終了後にページ内で自分の位置をもう一度確認して、移動しなければなりません。これは確実に良くないナビゲーション体験を与えます。
コンポーネントマウント時にモーダルを開くアクティブ要素をステートに格納することで、リターンフォーカス制御を実現します。 コンポーネントがアンマウントされると、保存されている要素に再び焦点が当てられます。 このシンプルなロジックは、モーダルがユーザーにとって自然なフロー体験であることを保証します。
componentDidMount() {
this.setState({ returnElement: document.activeElement })
}
componentWillUnmount() {
this.state.returnElement.focus()
}
正しい役割とラベルを導入する
前述の最善な手法は、モーダルのアクティブナビゲーションの制御に集中することです。 アクセシビリティのためにアプリを最適化するときには、モーダルのさまざまな宣言属性を慎重に扱うことも重要です。
アクセシビリティソフトウェアがそれらをユーザーに正しく表示するように、モーダルには適切な役割とラベルを設定する必要があります。 これらの値は適切なレベルに配置する必要があり、繰り返さないでください。 ここにいくつかの重要な属性があります:
- role:dialog – スクリーンリーダー用のモーダル要素のコンテナを識別する。
- aria-modal:true – スクリーンリーダーは、このコンテンツが他のページコンテンツとは別のものであることを認識できます。
-
tabindex="0"
— プログラムでダイアログに焦点を当てることができます。 - 適切なaria-labelまたはaria-labelledby – モーダルの内容を識別します
W3Cはこれらの属性とそれらがどのようにしてアクセシビリティを向上させるかについて詳細な説明を挙げています:https://www.w3.org/TR/wai-aria-practices/examples/dialog-modal/dialog.html
適切なheadingを含める
役割とラベルに加えて;潜在的なビュー (underlying view)から意図的に分離されているので、モーダルはその内容を識別するために適切なheadingを必要とします。 headingはコンテンツの先頭に置き、必要に応じて各サブセクションに含めてください。
既存のデザインや技術的な制約により、モーダルの新しいheadingが妨げられることがあります。 これはスタンダードなアプリケーションのレンダリングには問題ないかもしれませんが、アクセシビリティ機能を使用するユーザーにとっては、使い勝手が悪くなります。 幸いなことに、このような状況にはベストプラクティスがあります。それは目に見えないheadingです。
スクリーンリーダーのみのheadingは、次のスタイルを使用して実現できます。
{ position: 'absolute', clip: 'rect(1px, 1px, 1px, 1px)', overflow: 'hidden !important', }
ここのコツはヘッダー位置を切り取ってオーバーフローを隠すことです。 これにより、要素は標準のアプリケーションビューでは見えなくなりますが、スクリーンリーダーには表示されます。 これはウィンウィンです。
スクリーンリーダーを使ってテストする
補助テクノロジーを利用するユーザーのための最高の経験を確保するため。 共感(empathy)は絶対必要です。 1ブロック、1マイル、時にはマラソンであろうとなかろうと、ユーザーの苦労を理解するには、実際に自分でその苦労を味わうことよりも優れた方法がいくつかあります。
アクセシビリティ開発の最後のステップは、スクリーンリーダー(VoiceOver、NVDA、JAWSなど)を使って完全なエクスペリエンスを徹底的にテストすることです。 あなたは絶対自分がアプリに組み込んだその簡単な閉じるボタン、健全なモーダルトラップ、そして分かりやすいheading情報に感謝するでしょう。 私達はそうしました。 たぶん、あなたはこれらのベストプラクティスを超えた他のアクセシビリティ課題に気付き、そして新しい最先端の解決策を追加するでしょう。 何よりも、あなたはあなたのユーザーに最適化な体験を提供することができ、そしてすべての顧客にシームレスなアプリ内購入経験を提供することでもう一つのマイルストーンに達成できるでしょう。 😄
私とInstacartのカスタマーエンジニアリングチームと一緒にもっとアクセスしやすいアプリを作りたいですか。 現在、募集中です。すぐに求人情報をチェックしてください。
原文タイトル:Making an Accessible Web Modal
原文作者:Logan Murdock
原文リンク:https://tech.instacart.com/making-an-accessible-web-modal-48e4e9d8c284