Splideのアクセシビリティの意識が高くてついていけなかったのでゆっくり咀嚼する

## はじめに

Splideは「**アクセシビリティに配慮した軽量・高機能スライダー**」と謳っています。
使ってみると、その謳い文句に劣らず、確かにアクセシビリティに非常に気をつかわれていることが分かります。

このSplideの公式ドキュメントにはアクセシビリティに関するページが単独で存在し、（当たり前ですが）そもそもWebアクセシビリティに明るくないと半分も理解できません。少なくとも私は*完全に理解した*状態でした。

https://ja.splidejs.com/guides/accessibility/

そこで、W3Cのドキュメント等も踏まえカルーセルのアクセシビリティについて理解を深めつつ、Splideについても噛み砕いていきたいと思います。（かなり時間がかかった）

## カルーセルのW3Cデザインパターンを確認

> バージョン4から、W3CのCarousel Design Patternに準拠した上で、スクリーンリーダーが動的にコンテンツを読み上げられるようライブリージョンも導入しました。

まずは、Splideが準拠しているという、W3Cのカルーセルのデザインパターンを確認していきます。
今後の話は、これを前提に進められていきますので、ここが理解できないことには何も始まりません。

https://www.w3.org/WAI/ARIA/apg/patterns/carousel/

WAI-ARIAについても記載がありますが、こちらのセクションにはまとめず、Splideの説明に組み込みながら見ていきます。

### “十分な”カルーセル制御に必要なもの

カルーセルに必要な機能は以下の通りであると示されています。

- 前と次のスライドを表示するためのボタン
- オプションとして、表示する特定のスライドを選択するためのコントロール、またはコントロールのグループ
- カルーセルを自動スクロールさせる場合
  - 回転を停止・再開するためのボタンがある
  - キーボードのフォーカスがカルーセルに入ると回転が停止する
  - マウスがカルーセルの上に乗っている間は回転を停止する

### キーボードインタラクション

#### 自動スクロールさせる場合

カルーセル内の要素にキーボードのフォーカスが当たった時は、スライドの自動スクロールは停止させます。
ユーザが自動スクロールの再生・停止ボタンを押さない限り、回転は再開されません。

#### TabキーとShift + Tab

通常のタブ順序によって指定されたカルーセルのインタラクティブ要素にフォーカスを移動します。
Tabキーのためのスクリプトは必要ありません。

#### ボタン要素

ボタン要素は、[ボタンパターン](https://www.w3.org/WAI/ARIA/apg/patterns/button/)で定義されたキーボードインタラクションを実装します。

自動スクロールの開始・停止、スライドを次・前へ動かすボタンは、何度でも繰り返し押せるようにします。

#### 自動スクロールの開始・停止コントロール

自動スクロールコントロールは、カルーセル内のタブ順序中で最初の要素にします。
**回転するコンテンツの前**に置くことが重要です。

#### ページネーションをタブ要素で使う場合

ここで言うページネーションは、カルーセルの下についてるドットなどで表示される、任意のスライドに移動できるコントロールのことです。

これをタブ要素で使う場合は、[Tabsパターン](https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/)で定義されたキーボードインタラクションを実装します。

### 3つのカルーセルタイプ

|タイプ|概要|
|:-|:-|
|基本(Basic)|回転、前へ次へのボタンを持つが、ページネーションを持たない|
|タブ(Tabbed)|基本的なコントロールと、[タブパターン](https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/)を使用して実装されたページネーションのための単一のタブストップを持つ|
|グループ(Grouped)|基本的なコントロールと、ページネーションのグループ内にタブストップを持つ<br>※ページネーションの各ボタンはページのタブ順序に追加されるため、キーボードユーザーにとって最も不親切|

## Splideのアクセシビリティ

さて、ここからはW3Cのデザインパターンを踏まえてSplideのアクセシビリティについて見ていきます。

### カルーセルのタイプ

Splideでは、ページネーションの有無によって、基本(Basic)かタブ(Tabbed)として実装されています。

|タイプ|概要|
|:-|:-|
|基本(Basic)|ページネーションを持たない|
|タブ(Tabbed)|ページネーションを持つ。ページネーション自体はタブパターンに準拠|
|グループ(Grouped)|該当なし|

タブ型は、カルーセルをタブUIと見なすため、それぞれのスライドを`tabpanel`、ページネーションを`tab`として実装する必要があります。

### ランドマーク化

カルーセルにはメインコンテンツと関連性の高いものが含まれることが多く、バナーや、写真のギャラリーなど、基本的にはその中だけで情報が完結します。

Splideでは**カルーセルをランドマークにする**ことにより、支援技術に対してそれがメインコンテンツの一部であることを伝えます。

#### ランドマークとは

ページセクションにランドマークとしてラベル付けすることで、人間にはレイアウトなどによって伝わる**構造情報を、マークアップ上で表現する**ことができるものです。

このランドマークによって、スクリーンリーダーでセクションへの移動が可能になります。

HTML要素の`main`、`header`、`footer`、`nav`、`aside`、`section`がデフォルトでランドマークロールをもっています。

#### regionランドマーク

ランドマークにはいくつかのロールが存在し、そのうちの1つが**region**です。

regionランドマークは、ユーザがそのセクションに移動できるようにするために必要なセクションを示します。

特徴として以下があります。

- regionランドマークはラベルを持たなければならない
- 複数のregionランドマークを含む場合は、それぞれ固有のラベルが必要
- 名前付きランドマークでは適切に表現できないコンテンツを識別するために使用できる

`section`要素は、アクセシブルな名前（`aria-labelledby`、`aria-label`、`title`など）を持つ場合、regionランドマークとして機能します。

そのため、Splideではこの仕組みに準拠し、基本的に`section`でカルーセルをマークアップするように推奨していますが、`div`を用いた場合も、`role="region"`が自動的挿入されるため、いずれにせよルート要素はregionランドマークとなるようです。

#### ランドマークとは見なせないカルーセルの場合

メインコンテンツとは関連性のないものが含まれていたり、単に装飾目的のカルーセルである場合は、ランドマーク化はふさわしくありません。

その場合は、[基本的な使い方](https://ja.splidejs.com/guides/getting-started/)ページにも記載がある通り、代わりにgroupロールを使用します。

```html
<div class="splide" role="group" aria-label="Splideの基本的なHTML">
  <div class="splide__track">
    <ul class="splide__list">
      <li class="splide__slide">Slide 01</li>
      <li class="splide__slide">Slide 02</li>
      <li class="splide__slide">Slide 03</li>
    </ul>
  </div>
</div>
```

HTMLではなく、JavaScriptのオプションからでも変更できます。

```js
new Splide('.splide', { role: 'group' });
```

### キーボード操作

バージョン4では、矢印キーでスライダーを移動するなどの、独自のキーボード操作はデフォルトで無効になりました。
その代わりに、W3Cで定義されれている[タブのデザインパターン](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/)に必要なショートカットが有効になっています。

W3Cによれば、タブリストに必要とされるキーボードインタラクションは以下の通りです。

|キー|動き|
|:-|:-|
|Tab|タブリストにフォーカスが移動すると、アクティブなタブ要素にフォーカスを置く。<br>タブリストにフォーカスされている場合、タブパネルにフォーカスを移す。|
|←<br>左矢印|フォーカスを前のタブに移動する。<br>最初のタブにフォーカスがある場合、最後のタブにフォーカスを移動する。|
|→<br>右矢印|次のタブにフォーカスを移動する。<br>最後のタブ要素にフォーカスがある場合、最初のタブにフォーカスを移動する。|
|Home(オプション)|最初のタブにフォーカスを移動する。|
|End(オプション)|最後のタブにフォーカスを移動する。|
|Space または Enter|フォーカスが当たったタブが自動的にアクティブにならなかった場合、そのタブをアクティブにする。|

タブリストが垂直方向に配置されている場合、右矢印は下矢印に、左矢印は上矢印と読み替えられます。

これらはSplideでは以下のように設定されています。

|キー|説明|
|:-|:-|
|←<br>左矢印|横並びのページネーションにおいて、フォーカスを前のボタン (RTLでは次のボタン)に移動すると同時に、対応するスライドを表示する|
|→<br>右矢印|横並びのページネーションにおいて、フォーカスを次のボタン (RTLでは前のボタン)に移動すると同時に、対応するスライドを表示する|
|↑<br>上矢印|縦並びのページネーションにおいて、フォーカスを前のボタンに移動すると同時に、対応するスライドを表示する|
|↓<br>下矢印|縦並びのページネーションにおいて、フォーカスを次のボタンに移動すると同時に、対応するスライドを表示する|
|Space/Enter|対応するスライドを表示する|
|Home|フォーカスを最初のボタンに移動し、最初のスライドを表示する|
|End|フォーカスを最後のボタンに移動し、最後のスライドを表示する|

`keyboard`および`paginationKeyboard`オプションによって、これらのショートカットを有効にするかどうかを変更できます。
また、ページネーションのボタンがキーによってフォーカスされると、対応するスライドは自動的に表示されますが、`waitForTransition`が有効な場合は、フォーカスだけが先行することがあります。

### ロービング・タブインデックス

Splideは、ページネーションのフォーカスは[ロービング・タブインデックス](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#kbd_roving_tabindex)を用いられています。

ロービング・タブインデックスは、タブシーケンスに含まれてアクティブな要素は`tabindex="0"`を持ち、その他すべてのフォーカス可能な要素は`tabindex="-1"`を持つようにすることです。

フォーカス管理には`aria-activedescendant`というWAI-ARIAを用いることもできます。
これは、現在アクティブな子孫要素を指定するものです。

これではなくロービング・タブインデックスを用いる利点は、ユーザーエージェントが**新しくフォーカスされた要素をスクロールして表示させる**ことができることです。

Splideでこれを採用することによる利点は以下の通りと説明されています。

- ページネーションから離れた後、再度戻ってきた際、**以前選択した位置から操作を再開できる**
- **Tabを一度押すだけでページネーションを抜けられる**
  スライドが10枚ある場合、この機能がないページネーションでは10回押さないと抜けられない

### 自動再生の制御（WCAG 2.2.1, 2.2.2）

#### WCAG 2.2.1 Timing Adjustable より

https://www.w3.org/WAI/WCAG21/Understanding/timing-adjustable.html

コンテンツで設定される時間制限に対して、少なくとも以下のいずれかが当てはまる。

オフにする：
ユーザーはその時間制限に遭遇する前に、時間制限をオフにすることができる。

調整する：
ユーザーはその時間制限に遭遇する前に、デフォルト設定の長さの10倍以上の範囲で時間制限を調整することができる。

延長する：
ユーザーは時間制限が切れる前に警告を受け取り、少なくとも20秒間の猶予が与えられて、簡単なアクション（例えば「スペースバーを押す」）で時間制限を少なくとも10回延長することができる。

リアルタイム例外：
時間制限がリアルタイムのイベント（例：オークション）の必須部分であり、時間制限に代替手段が存在しない場合。

必須例外：
時間制限が重要であり、それを延長するとアクティビティが無効になる場合。

20時間例外：
時間制限が20時間を超える場合。

#### WCAG 2.2.2 Pause, Stop, Hide より

https://www.w3.org/WAI/WCAG21/Understanding/pause-stop-hide.html

移動、点滅、スクロール、または自動更新される情報について、以下のすべて当てはまる。

**移動、点滅、スクロール**
以下の条件をすべて満たす場合、自動的に始まり、5秒以上続く、他のコンテンツと並行して表示されるいかなる移動、点滅、またはスクロール情報に対して、ユーザーが一時停止、停止、または非表示にするための仕組みが必要（ただし、それが不可欠なアクティビティの一部である場合を除く）。

**自動更新**
以下の条件をすべて満たす場合、自動的に始まり、他のコンテンツと並行して表示される自動更新される情報に対して、ユーザーが一時停止、停止、または非表示にするための仕組みが必要。または更新の頻度を制御するための仕組みが必要（ただし、それが不可欠なアクティビティの一部である場合を除く）。

#### Splideでは

自動再生するスライダーは以下3つの機能を持つ必要があります。

- 自動再生を再生・停止するボタンを提供する
- スライダー内の要素がフォーカスした際は、自動再生を停止する
- スライダー上にマウスがある間は、自動再生を停止する

Splideでは再生・停止ボタンを設置するには、以下のコードを貼り付けるだけで完了します。

```html:再生・停止ボタン
<button class="splide__toggle" type="button">
  <span class="splide__toggle__play">Play</span>
  <span class="splide__toggle__pause">Pause</span>
</button>
```

その他はオプションでデフォルトで有効になっているため、気にしないでOK。素晴らしい。

### 視覚効果の低減（WCAG 2.3.3）

#### WCAG 2.3.3 Animation from Interactions より

https://www.w3.org/WAI/WCAG21/Understanding/animation-from-interactions.html

アニメーションコンテンツにより、一部のユーザーが気を散らされたり、吐き気を感じることがあります。
例えば、ページをスクロールすると要素が移動する場合（スクロールに関連する本質的な移動を除く）は、前庭（内耳）の障害によって、めまい、吐き気、頭痛を引き起こす可能性があります。
ウェブページの機能や情報に不可欠なアニメーションは許可されていますが、それ以外の必要不可欠でないものは、インタラクションによって引き起こされるモーションアニメーションを無効にできるようにします。

#### Splideでは

CSSの[メディア特性](https://developer.mozilla.org/ja/docs/Web/CSS/@media#%E3%83%A1%E3%83%87%E3%82%A3%E3%82%A2%E7%89%B9%E6%80%A7)の1つに、`prefers-reduced-motion`というものがあります。
これは、ユーザがページで不必要な動きを望まないかどうかを検出します。

OSで設定できる一例を示します。

- Windows 10: 設定 > 簡単操作 > ディスプレイ > アニメーションを表示する
- macOS: システム設定 > アクセシビリティ > 表示 > 動きの抑制
- iOS: 設定 > 一般 > アクセシビリティ > 視覚効果を減らす
- Android 9 以上: 設定 > ユーザー補助 > アニメーションの削除

Splideでは、`prefers-reduced-motion: reduce`を検出した際、**`speed`と`rewindSpeed`を0で上書き、`autoplay`を`paused`で上書き**します。

この結果、スライドのトランジションは再生されず、瞬時に表示されるようになりますが、ドラッグやスワイプ後のトランジションは必要なモーションであると判断されているため、無効になりません。

このオプションのデフォルト値は、[`reducedMotion`](
これは、-1に設定した後に再びスライドが表示された際、0（あるいはその他の整数値）に戻す処理が必要になるためです。

スライダーの中でtabindexを用いてフォーカスを制御することはないと信じたいですが、仮にそのような対処が必要になった場合は、visibleおよびhiddenイベントを利用すればインデックスの切り替えを実装できます。)オプションで設定できます。

### フォーカスの可視化（WCAG 2.4.7）

#### WCAG 2.4.7 Focus Visible より

https://www.w3.org/WAI/WCAG21/Understanding/focus-visible.html

ユーザーエージェントは、キーボードが使用されるときだけフォーカスインジケータを表示するなど、フォーカスインジケータが表示されるタイミングを最適化することができます。

また、フォーカスが見える操作モードを少なくとも一つは提供しなければなりません。
フォーカス・インジケータは時間制限があってはならず、キーボード・フォーカスが表示されているときは、そのままでなければなりません。

#### Splideでは

キーボードユーザに対してのみこのスタイルを有効にするには、`:focus-visible`疑似クラスを利用する必要があります。

Safariは、2022/03/15にリリースされたバージョン15.4より前ではこの疑似クラスが使用できないという制約があるため、代わりに`.is-focus-in`クラスが組み込まれています。
ただし、このクラスはフォーカスが外れてもすぐに取り除かれるわけではないため、`:focus`擬似クラスと併用する必要があります。

```css
/* :focus-visibleをサポートしているブラウザのためのCSS */
.splide__arrow:focus-visible {
  outline: 3px solid tomato;
}

/* その他のブラウザのためのスタイル */
.splide.is-focus-in .splide_arrow:focus {
  outline: 3px solid tomato;
}
```

### フォーカス可能な要素（WCAG 1.3.1）

#### WCAG 1.3.1 Info and Relationships より

https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships

レンダリングされたコンテンツを通じて伝達される情報、構造、関係性は、プログラムによって解釈できるし、テキストで取得できるものにします。

#### Splideでは

スライダーの表示領域外のスライドに対して`aria-hidden`属性を割り当て、スクリーンリーダなどの**支援技術が表示されていないスライドを認識しない**ように設定されています。

また、スライド自体にフォーカス可能な要素が含まれている場合、その要素に対して`tabindex="-1"`を適用することで、**一時的にフォーカスできない**ようにしています。

ただし、以下の要素を含む場合は追加対応が必要です。

- `<area>`
- `<audio controls>`
- `<summary>`
- `<video controls>`
- `contenteditable`属性が有効になっている要素
- `tabindex="0"`を持った要素

どの要素に対してtabindex="-1"を適用するかは、[`focusableNodes`](https://ja.splidejs.com/guides/options/#focusablenodes)オプションで変更できます。

```js
{
  focusableNodes: 'a, button, ..., area, [contenteditable]',
}
```

`tabindex="0"`の要素がある場合は、`-1`に設定した後に再びスライドが表示された際、`0`に戻すような処理が必要になります。

### ARIA属性（WCAG 4.1.2）

インタラクション可能なすべての要素に、ARIA属性が設定されています。

ただし、属性値はすべて英語のため、ラベル等に割り当てられている値はページを閲覧する対象ユーザに合わせて、[`i18n`](https://ja.splidejs.com/guides/i18n/)オプションを利用してローカライズしてあげます。

# 終わりに

W3Cのページも踏まえつつ、Splideのアクセシビリティページに記載されているものについて、できるだけ噛み砕いてみました。

そもそもSplideの本家ページが結構分かりやすく書かれているので、それを読めば事足りる人も多いかと思いますが、自分の理解度を高めるためにも調べ直してみました。

理解の一助となれば幸いです。