【アクセシビリティ】アクセシビリティを意識したディスクロージャの作り方

## はじめに
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/390720/f16c2439-9bfa-f8cf-3930-c30b931efa96.png)

みなさんアクセシビリティを意識して開発できていますか？

必要なところに`role属性`を記述したり、`tabキー`でフォーカスができるようにしたりなど、意識しないといけないことも多いです。
そのため、アクセシビリティを完璧にやろうとするのは一苦労です。

ただ、コンポーネントごとに区切って、アクセシビリティを理解しておけば、実装するタイミングに思い出しやすく、アクセシビリティも意識しやすいと思います。

そのため、この記事では「**ディスクロージャ**」に焦点を当てて、アクセシビリティを意識したディスクロージャの実装方法とディスクロージャで意識した方がいいアクセシビリティを解説しようと思います。

## アクセシビリティを意識したディスクロージャの仕様
### ⚪︎ ディスクロージャとは？
ディスクロージャは、コンテンツを折りたたんだり（非表示）、展開させる（表示）ことができる要素です。
ディスクロージには、表示/非表示を制御するボンタンと、表示/非表示する要素の2要素があります。

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

+ **`Enterキー`**
  + コンテンツの表示/非表示を制御するボタンにフォーカスがある場合、コンテンツの表示/非表示を制御します
+ **`Spaceキー`**
  + コンテンツの表示/非表示を制御するボタンにフォーカスがある場合、コンテンツの表示/非表示を制御します

### ⚪︎ WAI-ARIA の役割、状態、プロパティ

+ コンテンツの表示/非表示を制御するボタンには、`role="button"` を設定します
+ コンテンツが表示されている場合、ボタン要素に `aria-expanded="true"` を設定します
+ コンテンツが非表示の場合、ボタン要素に `aria-expanded="false"` を設定します
+ （任意）ボタン要素に、コンテンツ要素のIDを `aria-controls` に設定します

## アクセシビリティを意識したディスクロージャの完成形
<p class="codepen" data-height="500" data-default-tab="result" data-slug-hash="YzBwVRN" data-editable="true" data-user="sp_degu" style="height: 500px; 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/sp_degu/pen/YzBwVRN">
  Disclosure Accessibility</a> by でぐぅー | Qiita (<a href="https://codepen.io/sp_degu">@sp_degu</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

## アクセシビリティを意識したディスクロージャの作り方
### 1. HTMLを実装する
```sample.html
<div class="disclosure">
  <button class="disclosure-button" type="button" aria-expanded="false" aria-controls="faq1_desc">
    <span class="material-symbols-outlined">chevron_right</span>
    Qiitaとは？
  </button>
  <div id="faq1_desc" class="desc">
    Qiita (キータ) は、エンジニアに関する知識を記録・共有するためのサービスです。
  </div>
  <button class="disclosure-button" type="button" aria-expanded="false" aria-controls="faq2_desc">
    <span class="material-symbols-outlined">chevron_right</span>
    バッジ機能とは？
  </button>
  <div id="faq2_desc" class="desc">
    バッジ機能は、イベントへの参加やユーザー表彰プログラムの達成など、 特定の条件を達成した方にバッジをお送りする機能です。
  </div>
</div>
```

### ２. CSSを実装する
```sample.css
body {
  background-color: #212529;
  color: #fff;
  display: grid;
  height: calc(100vh - 40px);
  margin: 0;
  padding: 20px 0;
  place-items: center;
  width: 100vw;
}

.disclosure {
  background-color: rgb(128 128 128 / .3);
  border-radius: 24px;
  padding: 16px;
  position: relative;
  width: 400px;
  background-blend-mode: luminosity;
  backdrop-filter: blur(50px);
  &::before {
    background: linear-gradient(135deg, rgb(255 255 255 / .4) 0, rgb(255 255 255 / 0) 40%, rgb(255 255 255 / 0) 60%, rgb(255 255 255 / .1) 100%);
    border: 1.4px solid transparent;
    border-radius: 24px;
    content: "";
    inset: 0;
    position: absolute;
    -webkit-mask: linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0) border-box;
    -webkit-mask-composite: destination-out;
    mask: linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0) border-box;
    mask-composite: exclude;
    z-index: -1;
  }
}

button {
  align-items: center;
  background: none;
  border: none;
  color: #ffffff;
  display: flex;
}

.disclosure-button {
  cursor: pointer;
  font-size: 16px;
  padding: 8px;
  width: 100%;
  &[aria-expanded="true"] > .material-symbols-outlined {
    transform: rotate(90deg);
  }
}

.material-symbols-outlined {
  padding: 4px;
}

.desc {
  margin: 0 8px;
  padding: 8px 16px;
  border-radius: 8px;
  background: radial-gradient(34.12% 136.61% at 50% 100%, rgba(94, 94, 94, 0.14) 0%, rgba(94, 94, 94, 0.00) 73.85%), radial-gradient(50% 164.29% at 50% 100%, rgba(255, 255, 255, 0.07) 0%, rgba(255, 255, 255, 0.00) 60.33%), linear-gradient(0deg, rgba(94, 94, 94, 0.18) 0%, rgba(94, 94, 94, 0.18) 100%), rgba(255, 255, 255, 0.06);
  background-blend-mode: color-dodge, normal, color-dodge, lighten;
}
```

### 3. JavaScriptを実装する
```sample.js
class DisclosureButton {
  constructor(buttonNode) {
    this.buttonNode = buttonNode;
    this.controlledNode = false;

    var id = this.buttonNode.getAttribute('aria-controls');

    if (id) {
      this.controlledNode = document.getElementById(id);
    }

    this.buttonNode.setAttribute('aria-expanded', 'false');
    this.hideContent();

    this.buttonNode.addEventListener('click', this.onClick.bind(this));
    this.buttonNode.addEventListener('focus', this.onFocus.bind(this));
    this.buttonNode.addEventListener('blur', this.onBlur.bind(this));
  }

  showContent() {
    if (this.controlledNode) {
      this.controlledNode.style.display = 'block';
    }
  }

  hideContent() {
    if (this.controlledNode) {
      this.controlledNode.style.display = 'none';
    }
  }

  toggleExpand() {
    if (this.buttonNode.getAttribute('aria-expanded') === 'true') {
      this.buttonNode.setAttribute('aria-expanded', 'false');
      this.hideContent();
    } else {
      this.buttonNode.setAttribute('aria-expanded', 'true');
      this.showContent();
    }
  }

  onClick() {
    this.toggleExpand();
  }

  onFocus() {
    this.buttonNode.classList.add('focus');
  }

  onBlur() {
    this.buttonNode.classList.remove('focus');
  }
}

window.addEventListener(
  'load',
  function () {
    var buttons = document.querySelectorAll(
      'button[aria-expanded][aria-controls]'
    );

    for (var i = 0; i < buttons.length; i++) {
      new DisclosureButton(buttons[i]);
    }
  },
  false
);
```

## まとめ
この記事では、「**ディスクロージャ**」に焦点を当てて、アクセシビリティを意識したディスクロージャの実装方法とディスクロージャで意識した方がいいアクセシビリティを解説しました。

ぜひこの記事をストックして、ディスクロージャを実装する時にアクセシビリティについて思い出してもらえると嬉しいです。

Advent Calendar 2023では、他のコンポーネントにも焦点を当てて、アクセシビリティについても解説しているので、ぜひ購読していてください。

https://qiita.com/advent-calendar/2023/degudegu2510

---

最後まで読んでくださってありがとうございます！

普段はデザインやフロントエンドを中心にQiitaに記事を投稿しているので、ぜひQiitaのフォローとX(Twitter)のフォローをお願いします。

https://twitter.com/degudegu2510