アクセシビリティ

アクセシビリティは、スクリーンリーダーを使用しているユーザーや、キーボードのみの機能に依存しているユーザーにとって、目の見えるマウスユーザーと同等の体験を保証するために重要です。

このページには、私たちが従うべきガイドラインが記載されています。

簡単な要約

ダメなARIAよりダメなARIAの方が良いので、aria-*roletabindex を使う前に、以下の推奨事項をレビューしてください。アクセシビリティ・セマンティクスを組み込んだセマンティックHTMLを使い、スクリーンリーダーとブラウザの関連する組み合わせでテストするのが理想的です。

WebAIMのトップ100万のホームページのアクセシビリティ分析では、「ARIAは、検出可能なエラーの増加と相関している」ことがわかりました。ARIAの誤用がエラー増加の大きな原因である可能性が高いので、疑わしい場合は、aria-*roletabindex を使用せず、セマンティックHTMLにこだわってください。

MacOSでキーボードナビゲーションを有効化

デフォルトでは、MacOS はタブ キーをテキストボックスとリストのみに制限しています。フルキーボードナビゲーションを有効にするには

  1. システム環境設定を開きます。
  2. キーボード」を選択します。
  3. ショートカット」タブを開きます。
  4. コントロール間のフォーカスの移動にキーボードナビゲーションを使用する]設定を有効にします。

ブラウザ固有のキーボードナビゲーションを有効にする方法については、a11yprojectを参照してください。

簡単なチェックリスト

良い文書のアウトラインを提供してください

見出しは、スクリーン・リーダーのユーザーがコンテンツをナビゲートするために使用する主なメカニズムです。したがって、ページ上の見出しの構造は、優れた目次のように理にかなっていなければなりません。そのためには

  • ページには、h1 要素が1つしかありません。
  • 見出しレベルはスキップされません。
  • 見出しレベルは正しくネストされます。

スクリーン・リーダーのためのアクセシブルな名前の提供

アクセシブルな名前を持つマークアップを提供するために、すべての:

  • 入力にはlabel](#examples-of-providing-accessible-names)に関連する[があります。
  • buttonとlinkには可視テキストがあり、コンテンツのないアイコンボタンのように可視テキストがない場合はaria-label
  • imageはalt 属性を持ちます。
  • fieldsetlegend を最初の子として持ちます。
  • figurefigcaption を最初の子として持ちます。
  • tablecaption を最初の子として持ちます。

チェックボックスとラジオ入力のグループは、fieldset でまとめてくださいlegend。 . legendはチェックボックスとラジオ入力のグループにラベルを与えます。

label 、子テキスト、または子要素が視覚的に望まれない場合は、.gl-sr-only を使用してスクリーン・リーダー以外から要素を非表示にしてください。

アクセシブルな名前を提供する例

以下のサブセクションでは、HTML要素をアクセシブルな名前で表示するマークアップの例を示します。

なお、GlFormGroup

  • label プロップのみを渡すと、label の値を含むlegend を持つfieldset がレンダリングされます。
  • labellabel-for の両方を渡すと、同じlabel-for ID を持つフォーム入力を指すlabel がレンダリングされます。

アクセス可能な名前を持つテキスト入力

GlFormGroup を使う場合、label プロップだけでは入力にアクセシブルな名前をつけることはできません。入力にアクセシブルな名前をつけるには、label-for プロパも指定しなければなりません。

テキスト入力の例

<!-- Input with label -->
<gl-form-group :label="__('Issue title')" label-for="issue-title">
  <gl-form-input id="issue-title" v-model="title" />
</gl-form-group>

<!-- Input with hidden label -->
<gl-form-group :label="__('Issue title')" label-for="issue-title" label-sr-only>
  <gl-form-input id="issue-title" v-model="title" />
</gl-form-group>

textarea

<!-- textarea with label -->
<gl-form-group :label="__('Issue description')" label-for="issue-description">
  <gl-form-textarea id="issue-description" v-model="description" />
</gl-form-group>

<!-- textarea with hidden label -->
<gl-form-group :label="__('Issue description')" label-for="issue-description" label-sr-only>
  <gl-form-textarea id="issue-description" v-model="description" />
</gl-form-group>

あるいは、label 要素を使うこともできます:

<!-- Input with label using `label` -->
<label for="issue-title">{{ __('Issue title') }}</label>
<gl-form-input id="issue-title" v-model="title" />

<!-- Input with hidden label using `label` -->
<label for="issue-title" class="gl-sr-only">{{ __('Issue title') }}</label>
<gl-form-input id="issue-title" v-model="title" />

アクセス可能な名前で入力を選択

入力例を選択します:

<!-- Select input with label -->
<gl-form-group :label="__('Issue status')" label-for="issue-status">
  <gl-form-select id="issue-status" v-model="status" :options="options" />
</gl-form-group>

<!-- Select input with hidden label -->
<gl-form-group :label="__('Issue status')" label-for="issue-status" label-sr-only>
  <gl-form-select id="issue-status" v-model="status" :options="options" />
</gl-form-group>

アクセス可能な名前のチェックボックス入力

単一のチェックボックス:

<!-- Single checkbox with label -->
<gl-form-checkbox v-model="status" value="task-complete">
  {{ __('Task complete') }}
</gl-form-checkbox>

<!-- Single checkbox with hidden label -->
<gl-form-checkbox v-model="status" value="task-complete">
  <span class="gl-sr-only">{{ __('Task complete') }}</span>
</gl-form-checkbox>

複数のチェックボックス

<!-- Multiple labeled checkboxes grouped within a fieldset -->
<gl-form-group :label="__('Task list')">
  <gl-form-checkbox value="task-1">{{ __('Task 1') }}</gl-form-checkbox>
  <gl-form-checkbox value="task-2">{{ __('Task 2') }}</gl-form-checkbox>
</gl-form-group>

<!-- Or -->
<gl-form-group :label="__('Task list')">
  <gl-form-checkbox-group v-model="selected" :options="options" />
</gl-form-group>

<!-- Multiple labeled checkboxes grouped within a fieldset with hidden legend -->
<gl-form-group :label="__('Task list')" label-sr-only>
  <gl-form-checkbox value="task-1">{{ __('Task 1') }}</gl-form-checkbox>
  <gl-form-checkbox value="task-2">{{ __('Task 2') }}</gl-form-checkbox>
</gl-form-group>

<!-- Or -->
<gl-form-group :label="__('Task list')" label-sr-only>
  <gl-form-checkbox-group v-model="selected" :options="options" />
</gl-form-group>

アクセス可能な名前のラジオ入力

単一のラジオ入力:

<!-- Single radio with a label -->
<gl-form-radio v-model="status" value="opened">
  {{ __('Opened') }}
</gl-form-radio>

<!-- Single radio with a hidden label -->
<gl-form-radio v-model="status" value="opened">
  <span class="gl-sr-only">{{ __('Opened') }}</span>
</gl-form-radio>

複数の無線入力

<!-- Multiple labeled radio inputs grouped within a fieldset -->
<gl-form-group :label="__('Issue status')">
  <gl-form-radio value="opened">{{ __('Opened') }}</gl-form-radio>
  <gl-form-radio value="closed">{{ __('Closed') }}</gl-form-radio>
</gl-form-group>

<!-- Or -->
<gl-form-group :label="__('Issue status')">
  <gl-form-radio-group v-model="selected" :options="options" />
</gl-form-group>

<!-- Multiple labeled radio inputs grouped within a fieldset with hidden legend -->
<gl-form-group :label="__('Issue status')" label-sr-only>
  <gl-form-radio value="opened">{{ __('Opened') }}</gl-form-radio>
  <gl-form-radio value="closed">{{ __('Closed') }}</gl-form-radio>
</gl-form-group>

<!-- Or -->
<gl-form-group :label="__('Issue status')" label-sr-only>
  <gl-form-radio-group v-model="selected" :options="options" />
</gl-form-group>

アクセス可能な名前のファイル入力

ファイル入力の例

<!-- File input with a label -->
<label for="attach-file">{{ __('Attach a file') }}</label>
<input id="attach-file" type="file" />

<!-- File input with a hidden label -->
<label for="attach-file" class="gl-sr-only">{{ __('Attach a file') }}</label>
<input id="attach-file" type="file" />

アクセス可能な名前を持つ GlToggle コンポーネント

GlToggle

<!-- GlToggle with label -->
<gl-toggle v-model="notifications" :label="__('Notifications')" />

<!-- GlToggle with hidden label -->
<gl-toggle v-model="notifications" :label="__('Notifications')" label-position="hidden" />

アクセス可能な名前を持つ GlFormCombobox コンポーネント

GlFormCombobox

<!-- GlFormCombobox with label -->
<gl-form-combobox :label-text="__('Key')" :token-list="$options.tokenList" />

アクセス可能な名前の画像

画像の例

<img :src="imagePath" :alt="__('A description of the image')" />

<!-- SVGs implicitly have a graphics role so if it is semantically an image we should apply `role="img"` -->
<svg role="img" :alt="__('A description of the image')" />

<!-- A decorative image, hidden from screen readers -->
<img :src="imagePath" :alt="" />

ボタンやリンクには、単独でも理解できるような説明的なアクセシブルな名前をつけるべきです。

<!-- bad -->
<gl-button @click="handleClick">{{ __('Submit') }}</gl-button>

<gl-link :href="url">{{ __('page') }}</gl-link>

<!-- good -->
<gl-button @click="handleClick">{{ __('Submit review') }}</gl-button>

<gl-link :href="url">{{ __("GitLab's accessibility page") }}</gl-link>

リンクはGlButton を使ってボタンのようにスタイルすることができます。

 <gl-button :href="url">{{ __('Link styled as a button') }}</gl-button>

ロール

一般的に、role. role代わりにrole暗黙のうちに持つセマンティックなHTML要素を使用して roleください。

悪い良好
<div role="button"><button>
<div role="img"><img>
<div role="link"><a>
<div role="header"> <h1><h6>
<div role="textbox"> <input> または<textarea>
<div role="article"><article>
<div role="list"> <ol> または<ul>
<div role="listitem"><li>
<div role="table"><table>
<div role="rowgroup"> <thead> <tbody> または<tfoot>
<div role="row"><tr>
<div role="columnheader"><th>
<div role="cell"><td>

キーボードのみの使用をサポート

キーボード・ユーザーは、ページのどこにいるのかを理解するために、フォーカスのアウトラインに依存しています。従って、要素がインタラクティブである場合は、次のことを保証する必要があります:

  • キーボード・フォーカスを受け取れること。
  • 可視フォーカスの状態です。

a (GlLink) やbutton (GlButton) のようなセマンティックHTMLを使用してください。

次のことに注意してください:

  • TabShift-Tab は静的なコンテンツではなく、インタラクティブな要素間を移動するものでなければなりません。
  • :hover スタイルを追加するとき、ほとんどの場合、:focus スタイルも追加して、マウスとキーボードの両方のユーザーにスタイルが適用されるようにしてください。
  • インタラクティブ要素のoutline を削除する場合、box-shadow などの別の方法で視覚的なフォーカス状態を維持するようにしてください。

詳しくは、Pajamas Keyboard-only pageをご覧ください。

tabindex

Preferno tabindex usetabindex, since:

  • button (GlButton) のようなセマンティック HTML を使うことは、暗黙的にtabindex="0" を提供することになります。
  • タブ順は視覚的な読み順と一致すべきであり、tabindexはこれを妨げます。

要素をインタラクティブにするためにtabindex="0" を使うことは避けてください。

divspan タグの代わりにインタラクティブ要素を使用してください。例えば

  • 要素がクリック可能な場合、button (GlButton) を使用してください。
  • 要素がテキスト編集可能な場合は、input またはtextarea を使ってください。

マークアップがセマンティックに完成したら、CSSを使って望ましい視覚状態に更新します。

<!-- bad -->
<div role="button" tabindex="0" @click="expand">Expand</div>

<!-- good -->
<gl-button class="gl-p-0!" category="tertiary" @click="expand">Expand</gl-button>

インタラクティブな要素にはtabindex="0" を使用しないでください。

インタラクティブ要素はすでにタブでアクセス可能なので、tabindex を追加することは冗長です。

<!-- bad -->
<gl-link href="help" tabindex="0">Help</gl-link>
<gl-button tabindex="0">Submit</gl-button>

<!-- good -->
<gl-link href="help">Help</gl-link>
<gl-button>Submit</gl-button>

スクリーン・リーダーが読むための要素にtabindex="0"

スクリーン・リーダーは、タブ・アクセシブルでないテキストも読むことができます。tabindex="0" の使用は不必要であり、スクリーン・リーダーのユーザーがそれを操作できることを期待するため、問題を引き起こす可能性があります。

<!-- bad -->
<p tabindex="0" :aria-label="message">{{ message }}</p>

<!-- good -->
<p>{{ message }}</p>

ポジティブなtabindex

tabindex="1" 以上の使用は避けてください。

アイコン

アイコンは3つのタイプに分けられます:

  • 装飾的なアイコン
  • 意味を伝えるアイコン
  • クリックできるアイコン

装飾的なアイコン

アイコンが装飾的であるのは、UIから取り除いてもユーザーにとって情報の損失がない場合です。

GitLab内部のアイコンの大部分は装飾的なので、GlIcon 、スクリーンリーダーからレンダリングされたアイコンは自動的に非表示になります。したがって、aria-hidden="true"GlIcon に追加する必要はありません。これは冗長だからです。

<!-- unnecessary — gl-icon hides icons from screen readers by default -->
<gl-icon name="rocket" aria-hidden="true" />`

<!-- good -->
<gl-icon name="rocket" />`

情報を伝えるアイコン

アイコンは、UIから取り除かれたときにユーザーに対する情報の損失がある場合、情報を伝えます。

例えば、イシューが機密であることを伝える機密アイコンで、その横に「Confidential」というテキストがない場合です。

情報を伝えるアイコンには、スクリーン・リーダーのユーザーにも情報が伝わるように、アクセシブルな名前が必要です。

<!-- bad -->
<gl-icon name="eye-slash" />`

<!-- good -->
<gl-icon name="eye-slash" :aria-label="__('Confidential issue')" />`

クリックできるアイコン

クリック可能なアイコンは意味的にボタンなので、アクセス可能な名前を持つボタンとしてレンダリングされるべきです。

<!-- bad -->
<gl-icon name="close" :aria-label="__('Close')" @click="handleClick" />

<!-- good -->
<gl-button icon="close" category="tertiary" :aria-label="__('Close')" @click="handleClick" />

ツールチップ

ツールチップを追加する場合、キーボードユーザーがツールチップを見ることができるように、ツールチップのある要素がフォーカスを受けられるようにしなければなりません。アイコンのような静的な要素であれば、アイコンにtabindex=0 を追加する必要はありません。

次のコード・スニペットは、ツールチップ付きのアイコンの良い例です。

  • ボタンなので、自動的にフォーカス可能です。
  • テキストを持たないボタンなので、aria-label でアクセス可能な名前が与えられます。
  • ホバー時にボタンの背景を灰色にしたくない場合は、gl-hover-bg-transparent! クラスを使用できます。
  • 必要であれば、gl-p-0! クラスを使用してボタンのパディングを取り除くことができます。
<gl-button
  v-gl-tooltip
  class="gl-hover-bg-transparent! gl-p-0!"
  icon="warning"
  category="tertiary"
  :title="tooltipText"
  :aria-label="__('Warning')"
/>

要素の非表示

ユーザーから要素を隠すには、以下の表を使用してください。

視覚ユーザーから隠すスクリーン・リーダーからの非表示視覚ユーザーとスクリーンリーダーユーザーの両方から非表示にします。
.gl-sr-onlyaria-hidden="true" display: none,visibility: hidden, またはhidden 属性

スクリーン・リーダーから装飾画像を隠す

スクリーン・リーダー・ユーザーのノイズを減らすには、alt="" を使って装飾画像を非表示にしてください。画像がインラインSVGなどのimg 要素でない場合は、role="img"alt=""の両方を追加することで非表示にできます。

gl-icon コンポーネントは、スクリーン・リーダーからアイコンを自動的に隠すので、gl-icon を使用する場合、aria-hidden="true" は不要です。

<!-- good - decorative images hidden from screen readers -->

<img src="decorative.jpg" alt="">

<svg role="img" alt="" />

<gl-icon name="epic" />

ARIAを使用する場合

セマンティックHTMLはすでにアクセシビリティを組み込んでいるので、ARIAを使う必要はありません。

しかし、セマンティックHTMLに相当するものがないUIパターンもあります。一般的な例としては、ダイアログ(モーダル)やタブがあります。GitLab特有の例としては、担当者やラベルのドロップダウンがあります。このようなウィジェットを作るには、スクリーンリーダーが理解できるようにするためにARIAが必要です。WCAGへの準拠を確実にするために、適切な調査とテストを行う必要があります。

axeによる自動アクセシビリティ・テスト

axe-coreの gemsを使用して、機能テストで自動アクセシビリティテストを実行します。

World Wide Web Consortium (W3C) の Web Content Accessibility Guidelines 2.1 のレベル AA に準拠することを目指しています。

アクセシビリティ・テストを追加するタイミング

アプリケーションに新しいビューを追加するとき、機能テストにアクセシビリティ・チェックを含めるようにしてください。すべてのビューを完全にカバーすることを目指します。

機能テストにおけるテストの利点の1つは、単独のコンポーネントだけでなく、さまざまな状態をチェックできることです。

以下に、アクセシビリティのチェックのアプローチ方法の例をいくつか示します。

空の状態

ビューの中には、デフォルトのビューとは異なるページ構造になる空の状態を持つものがあります。また、最初のイシューの作成や機能の有効化など、何らかのアクションを提供する場合もあります。この場合、空の状態とデフォルトのビューの両方にアサーションを追加します。

ユーザーとのインタラクションの前にコンプライアンスを保証します。

多くの場合、ユーザーが実行すると想定されるいくつかのステップに対してテストを行います。この場合、どのステップもシミュレートされる前の早い段階でチェックを入れるようにしてください。こうすることで、ユーザーに期待することに障害がないことを確認できます。

ページ構造変更後のコンプライアンス確保

ユーザーのインタラクションによって、ページ構造が大幅に変更されることがあります。例えば、モーダルが表示されたり、新しいセクションがレンダリングされたりします。そのような場合は、変更後にアサーションを追加してください。ユーザーが利用可能なすべてのコンポーネントと対話できることを確認したいと思います。

広範なテストスイート用の別ファイル

ビューによっては、機能テストは複数のファイルにまたがります。マージリクエストの機能テストを見てみましょう。カバーする必要のあるユーザーインタラクションの数は、1 つのテストファイルに収めるには大きすぎます。結果として、複数の機能テストが、異なるユーザー権限もしくはデータセットで、1つのビューをカバーします。それらのすべてにアクセシビリティのチェックを含めると、ビューの同じ状態を何度もカバーすることになり、実行時間が大幅に増加する可能性があります。また、アサーションが多くのファイルに散らばってしまうと、アクセシビリティのカバレッジを決定するのが難しくなります。

その場合は、アクセシビリティ専用のテストファイルを1つ作成することを検討してください。同じディレクトリに置き、accessibility_spec.rb という名前をつけます。例えば、spec/features/merge_request/accessibility_spec.rb です。

共有の例

多くの場合、機能テストには、いくつかのシナリオの共有例が含まれます。提供されるデータが異なるだけで、同じユーザー・インタラクションに基づいている場合、共有例の外部でアクセシビリティの準拠をチェックすることができます。こうすることで、チェックを一度だけ実行し、リソースを節約することができます。

アクセシビリティ・テストの追加方法

Axeはカスタムマッチャーbe_axe_cleanを提供しています:

# spec/features/settings_spec.rb
it 'passes axe automated accessibility testing', :js do
  visit_settings_page

  wait_for_requests # ensures page is fully loaded

  expect(page).to be_axe_clean.according_to :wcag21aa
end

アクセシビリティ・スタンダードを必ず:wcag21aaと指定してください。

必要であれば、withinを使って、ページの特定の領域にテストの範囲を設定することができます。

Axeはまた、例えば、特定の句を提供します:

expect(page).to be_axe_clean.within '[data-testid="element"]'

# run only WCAG 2.0 Level AA rules
expect(page).to be_axe_clean.according_to :wcag21aa

# specifies which rule to skip
expect(page).to be_axe_clean.skipping :'link-in-text-block'

# clauses can be chained
expect(page).to be_axe_clean.within('[data-testid="element"]')
                            .according_to(:wcag21aa)

Axe は非アクティブなメニューやモーダルウィンドウのような非表示領域をテストしません。非表示領域のアクセシビリティをテストするには、その領域をアクティブにする、あるいは可視にするテストを記述し、再度 matcher を実行してください。

アクセシビリティテストは、他の機能テストと同じようにローカルで実行できます。

既知のアクセシビリティ違反

このセクションでは、推奨が設計システムと異なる違反について文書化します:

  • link-in-text-block:今のところ、skipping 節を使用して、:'link-in-text-block' 規則をスキップして違反を修正してください。イシュー1444の一部としてこれが修正され、GlLink コンポーネントに下線が追加された後、この条項は削除できます。

リソース

ブラウザアクセシビリティツリーの表示

ブラウザ拡張機能

ウェブアクセシビリティのテストには2つのオプションがあります: