ヘッダーを固定した表のアクセシビリティ

この記事は [Web Accessibility Advent Calendar 2015](http://www.adventar.org/calendars/719) 8日目の記事です。

ヘッダーを固定した表のアクセシビリティについて、最近調べる機会があったので、その内容を報告します。
ヘッダーを固定した表とは、表のボディセルだけがスクロールし、ヘッダーはスクロールしない表のことです。

ヘッダーが行の場合はこんな感じ:
![行ヘッダーを固定した表の例](https://qiita-image-store.s3.amazonaws.com/0/42728/2a529f95-d105-66c6-07c1-6a0e173fc70e.png)

ヘッダーが列の場合はこんな感じです:
![列ヘッダーを固定した表の例](https://qiita-image-store.s3.amazonaws.com/0/42728/61252af4-c1f8-3d6b-8487-501c4db52b1c.png)

#まず考えるべきこと

実装にもよるため一概にはいえませんが、ヘッダーを固定した表は往々にしてアクセシビリティを低下させます。
後述するように、アクセシビリティを向上させる方法はありますが、ハックに近く、推奨できる方法ではありません。
まずはヘッダーを固定しない表を使えないか検討してください。

#よくある実装とその問題点

ヘッダーを固定した表でよくある実装は、ヘッダーとボディセルをそれぞれ別々のtable要素とする実装です。
例えばヘッダーが行の場合は、以下のように実装します:

```html
<table class="t1">
  <tr>
    <th>ヘッダー1</th>
    <th>ヘッダー2</th>
  </tr>
</table>

<table class="t2">
  <tr>
    <td>ボディセル1</td>
    <td>ボディセル2</td>
  </tr>
  <tr>
    <td>ボディセル3</td>
    <td>ボディセル4</td>
  </tr>
</table>
```

このHTMLに対してCSSを当てて、2つのtable要素が1つの表に見えるように調整します。ボディを表すtable要素(t2要素)の高さには上限を設け、スクロールできるようにします。

ヘッダーが列の場合も同様に、2つのtable要素を使ったうえでCSSを当てます:

```html
<table class="t1">
  <tr>
    <th scope="row">ヘッダー1</th>
  </tr>
  <tr>
    <th scope="row">ヘッダー2</th>
  </tr>
</table>

<table class="t2">
  <tr>
    <td>ボディセル1</td>
    <td>ボディセル2</td>
  </tr>
  <tr>
    <td>ボディセル3</td>
    <td>ボディセル4</td>
  </tr>
</table>
```

このような実装の場合、表示上は1つの表のように見えていたとしても、ブラウザや支援技術には2つの異なる表として解釈されてしまいます。例えばNVDAなどのスクリーンリーダーでは正しく読み上げられません。

# 修正方法

WAI-ARIAやCSSを組み合わせることで、ブラウザや支援技術に表を正しく解釈させることができます。
方法は、ヘッダーが行なのか列なのかによって異なります。

##ヘッダーが行の場合

まず、2つのtable要素にrole="presentation"をつけて、table以下の要素のロールをすべて削除します。
次に、すべての要素のロールを設定しなおします。具体的には、以下のように属性をつけます。

| 要素 | 属性 |
|:-----------|:------------|
| 2つのtableの親要素 | role="grid" aria-readonly="true" |
| tr要素 | role="row" |
| th要素 | role="columnheader" |
| td要素 | role="gridcell" |

```html
<div role="grid" aria-readonly="true">
  <table class="t1" role="presentation">
    <tr role="row">
      <th role="columnheader">ヘッダー1</th>
      <th role="columnheader">ヘッダー2</th>
    </tr>
  </table>

  <table class="t2" role="presentation">
    <tr role="row">
      <td role="gridcell">ボディセル1</td>
      <td role="gridcell">ボディセル2</td>
    </tr>
    <tr>
      <td role="gridcell">ボディセル3</td>
      <td role="gridcell">ボディセル4</td>
    </tr>
  </table>
</div>
```

上記のように実装することで、2つのtable要素の親要素(div)が、新たに「表」として解釈され、tr,th,td要素は、その表の「行」や「セル」として解釈されます。
この修正方法は、[W3Cのドキュメント](https://w3c.github.io/aria-in-html/)にも記載されています。

## ヘッダーが列の場合

ヘッダーが列の場合、ヘッダーが行の場合と同じ方法でセマンティクスを修正するのは困難です。表は行単位で記述していきますが、列を固定した表の場合は、行単位の記述になっていないためです。

以下のように修正することで正しいセマンティクスに修正できます。

 - ヘッダーを表すtableにaria-hidden="true"をつける。
 - ボディセルのtableにth要素を含め、CSSを使って非表示にする。


```html

<table class="t1" aria-hidden="true">
  <tr>
    <th scope="row">ヘッダー1</th>
  </tr>
  <tr>
    <th scope="row">ヘッダー2</th>
  </tr>
</table>

<table class="t2">
  <tr>
    <th scope="row" class="hidden-header-cell">ヘッダー1</th>
    <td>ボディセル1</td>
    <td>ボディセル2</td>
  </tr>
  <tr>
    <th scope="row" class="hidden-header-cell">ヘッダー2</th>
    <td>ボディセル3</td>
    <td>ボディセル4</td>
  </tr>
</table>
```

.hidden-header-cellのCSSは以下のように実装します。このCSSを適用した要素は、スクリーンリーダーなどの支援技術からは認識されますが、画面には表示されなくなります。[^1]

[^1]: このテクニックは[webAIMの記事](http://webaim.org/techniques/css/invisiblecontent/)で紹介されています。

```css
.hidden-header-cell {
  position: absolute !important; 
  clip: rect(1px 1px 1px 1px);
}
```

画面の表示と、支援技術の認識は、以下の表に示すとおりになります。画面からも支援技術からも、表を正しく読み取ることができます。

|  | t1要素 | t2要素のth要素 | t2要素のtd要素 | 
|:--|:--|:--|:--|
| 画面 | 表示される | 表示されない | 表示される |
| 支援技術 | 認識されない | 認識される | 認識される |