detailsタグでなんでもアコーディオンにしていいのか考える

## はじめに

最近、アコーディオンUIと言えば、`details` `summary`を使った実装がアクセシビリティもバッチリで良いと聞きます。

しかしながら、全ての状況においてこの実装が適しているのかというとそうとも言えないんじゃないか、とふと思いました。

MDNの例や、このタグを紹介されているページなどを見ると、Q&Aや、何かのリストをアコーディオンに格納するために使っているものが多く、実際そのような使い方には特に抵抗感はありません。

しかし、「ページが長くなるから」などのデザイン的意図を含んだ理由で、`section`で囲われるような大きなコンテンツをまるっとアコーディオンの中に格納する場合などは、details（詳細）とsummary（概要）という英語から想像するに、適していないんじゃないか？と感じてしまいます。

```html:こういうやつは合法なの？
<section>
  <h2>許されざる呪文</h2>
  <p>
    死の呪い、磔の呪い、服従の呪文を指す言葉です。<br>
    これらの呪いは1717年に使用が禁止され、服従の呪文の支配下だった場合を除いて、終身刑という厳しい処罰が課されました。
  </p>
</section>

<details>
  <summary>豆知識</summary>
  <section>
    <h3>許されざる呪文は“合法”だった</h3>
    <p>
      これら3つの呪文は、1717年に使用禁止となりました。<br>
      逆に言えば、それまでは使用されていたようで、決闘時などは人気の呪いだったようです。<br>
      1970年代の第一次魔法戦争時は、死喰い人が使用しており、これに対抗するため闇祓いは死喰い人に対する使用を許可した特例も存在しました。<br>
      その後、第二次魔法戦争で魔法省が陥落した際も、戦争が終結するまでは合法化されたことがあります。
    </p>
  </section>
  <section>
    <h3>3つの呪文を経験し生還した魔法使い</h3>
    <p>
      これら3つの呪文をかけられ、反対呪文が存在しないはずの死の呪文も2回も生き延びた魔法使いが、ハリー・ポッターです。<br>
      また、服従の呪文に関しては対抗する術さえ身に着けています。
    </p>
  </section>
</details>
```

<p class="codepen" data-height="600" data-theme-id="dark" data-default-tab="result" data-slug-hash="VwBVLwZ" data-preview="true" data-user="heeroo-ymsw" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/heeroo-ymsw/pen/VwBVLwZ">
  Accordion Vanilla</a> by heeroo-ymsw (<a href="https://codepen.io/heeroo-ymsw">@heeroo-ymsw</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

もし、そんなこと気にしなくて良いなら、ぜひ積極的にこのタグを使っていきたいので、調査してみます。

## 調査

### 結論

- アコーディオンUIを作りたいときは、多くの場合 **`details` / `summary`で問題ない**
- ただし、折りたたまれた状態だと、**格納した要素がスクリーンリーダーの目次には表示されない**ことを留意する

### エビデンス探し

#### `details`に含めることができるコンテンツの制約

では、MDNを見ていきます。

https://developer.mozilla.org/ja/docs/Web/HTML/Element/details

> `<details>`: 詳細折りたたみ要素
`<details>` は HTML の要素で、ウィジェットが「開いた」状態になった時のみ情報が表示される折りたたみウィジェットを作成します。概要やラベルは `<summary>` 要素を使用して提供する必要があります。

この「折りたたみウィジェット」の制約については、何も記載がありませんでした。

許可されているコンテンツを見ると、「1つの`summary`要素と、それに続くフローコンテンツ」とあり、こちらも`summary`を入れること以外にほとんど**制約はない**ようです。

よって、HTML要素的には、**`details`の中に`body`配下に入れるような一般的なタグなら何を突っ込もうが問題はない**ようです。

#### ARIAロール

では、ARIAロールはどうでしょう？

`details`に暗黙的に設定されたARIAロールは**group**となっています。
また、**許可されているARIAロールはありません**。

##### `group`ロールとは

支援技術によってページの要約や目次に**含まれないようにする**ユーザインタフェースオブジェクトであるとされています。

`group`はページ上で**主要な知覚可能セクションとするべきではありません**。

「例えば、ツリーウィジェットの子ウィジェットが階層構造の兄弟の集まりを形成するように、ウィジェット内のアイテムの論理的な集まりを形成するためにグループを使用すべき」とある通り、`details`もここで言うウィジェットに含まれるわけなので、暗黙的にgroupが設定されているようです。

また、もし、そのセクションがWebページの目次に含めるに値するほど重要である場合、その**セクションに`region`や標準的なランドマークロールを割り当てるべき**と記載があります。

標準的なランドマークロールは以下の通りありますが、この中でより明確に重要なセクションだと伝えられるロールは`region`です。

|ロール名|説明|
|:-|:-|
|banner|ページ固有のコンテンツではない、バナー類|
|complementary|ドキュメントの補助的セクション、メインコンテンツから分離しても意味を持つ|
|contentinfo|親文書に関する情報を含む、知覚可能な大きな領域<br>著作権やプライバシーポリシーへのリンクなど|
|form|フォームの役割を持つ要素|
|main|文書内の主要なコンテンツ|
|navigation|ナビゲーション要素（通常はリンク）の集合|
|region|ページの要約にリストアップされるべきコンテンツ<br>ユーザが支援技術などでそのセクションに簡単に移動できる|
|search|検索機能を構成するアイテムやオブジェクト|

##### `region`ロールをつけた時のスクリーンリーダーの挙動

`region`ロールは、「目次に羅列されるべき」「ページの要約にリストアップされるべき」コンテンツを明示するとあります。

`section`タグなんかはこの目次に羅列されるべきコンテンツに当たるのかと思い、確認してみました。

https://developer.mozilla.org/ja/docs/Web/HTML/Element/section

暗黙のARIAロールは、要素にアクセシブルな名前がある場合は`region`、それ以外の場合は**対応するロールなし**とあります。

「対応するロールなし」要素は、暗黙のARIAセマンティクスは無いものの、ちゃんと**意味を持っており**、ARIAが提供しないロール、ステート、プロパティで表され、アクセシビリティAPIを介して支援技術利用のユーザーに知覚される可能性があるものです。
つまり、`section`はHTML仕様上で意味を持っているため、わざわざARIAロールを指定する必要がないということです。

そもそも`div`や`span`のような、セマンティクス的に中立な要素に`role`属性を追加することが推奨されています。
つまり、**できるだけHTML標準のタグを使え[^1]**、ということです。

[^1]: [Read Me First - No ARIA is better than Bad ARIA](https://www.w3.org/WAI/ARIA/apg/practices/read-me-first/#noariaisbetterthanbadaria)

そのため、例えば、以下のようなHTMLがあって、***本来ARIAのルール上やってはいけませんが***、仮に`details`に`region`ロールをつけて、

```html:regionロールを付与したdetails例（やらないでね）
<h1>Heading 1</h1>

<section>
  <h2>Heading 2</h2>
  <p>content text</p>
</section>

<section>
  <h2>Heading 2</h2>
  <p>content text</p>
</section>

<details role="region" aria-label="title">
  <summary>disclosure widget</summary>
  <section>
    <h2>Heading 2</h2>
    <section>
      <h3>Heading 3</h3>
      <p>content text</p>
    </section>
  </section>
</details>
```

macOSのVoiceOverを使うと、ランドマークはこんな感じになります。

```
ランドマーク
本文
title 地域
```

`section`にランドマークロールがついていないため、`details`だけが逆に悪目立ちしてしまいました。

ちなみに、目次は以下のように、折りたたまれている状態だと中のheadingは表示されません。

```
目次
1. Heading 1
2. Heading 2
2. Heading 2
```

##### 目次に表示させたいから`details`に`heading`ロール付与させるのはアリ？

**ナシよりのナシ**です。

たしかに`heading`ロールを付与すると見出しとして扱われキーボードインタラクションも可能になりますが、前述の通り、`details`に許可されているARIAロールはないため、無理やり目次に表示させようと`heading`ロールを付与するのは許可されません。

```html:headingロールを与えるのはダメ
<details role="heading" aria-level="2" aria-label="title">
  <summary>disclosure widget</summary>
  <section>
    <h2>Heading 2</h2>
    <section>
      <h3>Heading 3</h3>
      <p>content text</p>
    </section>
  </section>
</details>
```

##### 分かった、じゃあ`summary`に`heading`ロールを…

**ナシよりのナシ**です。

`summary`の暗黙のARIAロールは`button`、許可されたARIAロールはありませんから、こちらもARIAロールを付与することはできません。

```html:これもアカン
<details>
  <summary role="heading" aria-level="2" aria-label="title">disclosure widget</summary>
  <section>
    <h2>Heading 2</h2>
    <section>
      <h3>Heading 3</h3>
      <p>content text</p>
    </section>
  </section>
</details>
```

#### ARIA Authoring Practices Guide

ARIA的に正しいアコーディオンの設計パターンを見てみると、`details` / `summary`を使った実装とは少々異なることが確認できます。

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

例えば、『各アコーディオンのヘッダボタンは、ページ構造に適した`aria-level`の値を設定した`heading`ロール を持つ要素にラップされます』とあります。
これは、`summary`にはロールを与えられない、という仕様とはまた違った設計です。

しかしながら、あくまで、『これはARIA仕様に準拠した一例であり、セマンティックなHTMLを最大限に活かすことでアクセシビリティの最適化を行うことができる[^2]』と記載がある通り、**今は`details` / `summary`を使った実装を最優先に考えるべき**であり、これらの仕様から逸脱する使い方をしなければいけない場合は、一度立ち止まって本当にそのUIを実現するべきなのかを考える必要があるでしょう。

[^2]: [ Accordion Example | APG | WAI | W3C](https://www.w3.org/WAI/ARIA/apg/patterns/accordion/examples/accordion/) Read This Firstより
Robust accessibility can be further optimized by choosing implementation patterns that maximize use of semantic HTML and heeding the warning that No ARIA is better than Bad ARIA.

#### WAI-ARIAまとめ

- `details`、`summary`ともに`role`属性を付与することは許可されてない
  - ARIAでごにゃごにゃすることはできず、素のHTMLで勝負するしかない
- `details` / `summary`で実装すると中のコンテンツはスクリーンリーダーの目次上では読み飛ばされるため、**ページコンテンツの構成上で重要な項目を`details`の中に含めることは好ましくない**
- ARIAで推奨される設計パターンは`details` / `summary`で実装したときとは異なるが、セマンティックなHTMLで実装できるならそちらの方が良い

## 使い方の考察

- ただ長々しいコンテンツを隠すために`details` `summary`を用いることは、やってはいけないこと、**ではない**
- アコーディオンUIを使う以上、格納するコンテンツまでもが完全にアクセシブルである必要があるかと言えば、そうではないかも
  - 無理にアクセシブルにするためにARIAロールを用いることはご法度
- アコーディオンのアニメーションが必要な場合は、JavaScriptが必要なため、**HTMLだけでアコーディオンが実装できるという良さは消える**
  - ARIAの設計パターンに準拠して、`div`などにARIA属性をつけて実装しても、結局同じかも
- できるだけセマンティックなHTMLで実装するというARIAの基本的な考え方でいけば、`details`で実装するのが最適解
  - わざわざARIAを使うために**HTMLタグを意味のないものに書き換える程でもない**気がする
- 隠したいコンテンツがページ構成上重要なものである場合は、本当にアコーディオンで実装すべきなのかを見直すべき
  - もちろんウィジェットを開くためのボタンはアクセシブルである必要があるが、その中身まで考慮するのは難しそう

## 実装

細かな実装については以下の記事で事細かに記載されています。
問題児くんSafariのバグも考慮されていたり、非常に実践的ですので、ぜひこちらを参照してください。

https://ics.media/entry/220901/

### ちょっとしたアレンジ

上記の例で十分実用的なのですが、Chrome / Edgeだと「検索して、その単語がアコーディオン内にある場合、自動でアコーディオンが開く」という`details`の良さが`preventDefault()`によって無くなっています。

せっかくなので、こちらも実装しておきます。

閉じるアニメーションの完了時に`open`属性を取っているため、連打しすぎるとアニメーションが正しく表示されませんが、表示はされるので良しとします。それより検索して自動でアコーディオンが開く方を重視します。

```js:JavaScript
/**
 * detailsを閉じるアニメーション関数
 * @param {HTMLElement} content アニメーションさせるコンテンツ
 * @param {HTMLElement} accordion detailsに当たる要素
 */
const closeAnimation = (content, accordion) => {
  gsap.to(content, {
    height: 0,
    duration: 0.3,
    ease: 'power3.out',
    // 連打対策
    overwrite: true,
    onComplete: () => {
      if (accordion.hasAttribute('open')) {
        accordion.removeAttribute('open');
      }
    },
  });
};

/**
 * detailsを開くアニメーション関数
 * @param {HTMLElement} content アニメーションさせるコンテンツ
 * @param {HTMLElement} accordion detailsに当たる要素
 */
const openAnimation = (content, accordion) => {
  gsap.fromTo(content,
    // fromは初回アニメーションに必要
    {
      height: 0,
    },
    {
      height: 'auto',
      duration: 0.3,
      ease: 'power3.out',
      // 連打対策
      overwrite: true,
      onComplete: () => {
        if (!accordion.hasAttribute('open')) {
          accordion.setAttribute('open', '');
        }
      },
    }
  );
};

/**
 * MutationObserverを使用して、DOMの変更を検知
 * @param {Array} mutationRecords - DOMの変更を表すMutationRecordの配列
 */
const observer = new MutationObserver((mutationRecords) => {
  for (const record of mutationRecords) {
    const contentElem = record.target.querySelector('.js_content');

    if (record.target.hasAttribute('open')) {
      record.target.classList.add('is_open');
      openAnimation(contentElem, record.target);
    }
  }
});

const accordionElems = document.querySelectorAll('.js_details');

for (const accordionElem of accordionElems) {
  const summaryElem = accordionElem.querySelector('.js_summary');
  const contentElem = accordionElem.querySelector('.js_content');

  summaryElem.addEventListener('click', e => {
    if (accordionElem.classList.contains('is_open')) {
      // closeのときはすぐopen属性が消えるとアニメーションが効かないためpreventDefault()
      e.preventDefault();
      accordionElem.classList.remove('is_open');

      closeAnimation(contentElem, accordionElem);
    } else {
      accordionElem.classList.add('is_open');

      openAnimation(contentElem, accordionElem);
    }
  });

  observer.observe(accordionElem, { attributes: true, attributeFilter: ['open'] });
}
```

## まとめ

`details` `summary`は、アコーディオンUIを実装する際、特に制限なく使えることが分かりました。
また、使う際は、重要なコンテンツは格納しないようにする必要があることが分かりました。

使用する際は、インタラクションなどの仕様を理解した上で、格納するコンテンツは隠してもページ構成上問題ないものなのか判断する必要があります。
問題がある場合は、デザイン意図を汲みつつ、アコーディオン以外のUI実装を考えたりする必要があります。