アクセシビリティに対応したTooltipってどんな作り？ ― 【前編】 Tooltipの基本と挙動

## これはなに

W3Cの[ARIA Authoring Practices Guide (APG) | Tooltip Pattern](https://www.w3.org/WAI/ARIA/apg/) を読み解きながら、アクセシビリティに対応したTooltipの実装を確認したいと思います。

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

参考にするといった、[ARIA Authoring Practices Guide (APG) | Tooltip Pattern](https://www.w3.org/WAI/ARIA/apg/) ですが、ページの出だしから 「Work in progress」 とあるように、まだ確定はしていません。

>This design pattern is work in progress; it does not yet have task force consensus. 

だからこそ読み解きながら、どうすべきか考えるきっかけになればと思い記事にまとめました。

長くなるので、前編・後編に分けたいと思います。

前編では、Tooltipの基本と挙動、それについての実装方法を紹介します。
後編では、`aria` 属性を使った読み上げについて紹介します。

## Toolipとは

Tooltipとはキーボードフォーカスまたはマウスオーバーされた時に、関連する情報を表示するポップアップです。

Qiitaでは下記の画像のようなUIをしています。

|Qiitaで使われいるTooltip|
| :---: |
|![Qiitaで使われいるTooltipのスクリーンショット](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215239/490d0e5b-0b7d-ec9d-398b-441a2f0cb82e.png)|

## Tooltipを使わずに表現できるなら使わないほうが良い

調べれば調べるほど、アクセシビリティを重視するなら、Tooltipは使わないほうが良いと感じました。

Tooltipは限られたスペースで、情報を伝える手段として用いられるUIだと思いますが、様々なユーザーに同じ情報を伝えるためにはかなり超えなくては行けない課題が多いUIだと思います。

- キーボードユーザーは製作者の意図どおりTooltipの情報を閲覧できますか？
- タッチデバイスでTooltipの情報は閲覧できますか？
- 読み上げの支援技術を使っているユーザーはTooltipの情報を認知できますか？
- 拡大表示を必要とするユーザーは、Tooltipによって他の要素が隠されていませんか？

そもそも伝えたい情報は最初から隠さずに表示しておくべきではないでしょうか？
今回Tooltipを調べてみて、Tooltipを使わずに表現できるなら使わないほうが良いと私は思わされました。
どうしてもTooltipという選択肢をとる時は、補足として使うようにすべきだと思います。

感覚的ですが、万が一読み飛ばしてもユーザーの不利益にならないような作りを考えるとちょうどよい使い方になりそうです。

## Tooltipで使用するWAI-ARIA

WAI-ARIA | 用途
--- | ---
`role="tooltip"` | Tooltipのポップアップとして表示される要素のroleとして使用します
`aria-describedby` | Tooltipがフォーカスを受け取る要素に、Tooltipの内容を参照情報として渡します
`aria-labelledby` | Tooltipがフォーカスを受け取る要素に、Tooltipの内容をメインの読み上げ要素として渡します

このあたりは読み上げに関わるので、詳しくは後編で触れたいと思います。

## Tooltipの挙動

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

ARIA Authoring Practices Guideでは、下記のように記載がありました。
（私の解釈でまとめています。）

- キーボードフォーカス、またはマウスオーバーでTooltipのポップアップを表示
- フォーカスアウト、マウスアウト、`Escape`キーによって、表示が消える
- Tooltipはフォーカスを受け取らない
    - フォーカス可能な要素を含む場合は、非モーダルダイアログで実装する

### より使いやすいTooltipにするために

また、実装してみて下記のページの「Best practices summary」に共感できた箇所があるので、実装時意識するとより良いTooltipにすることができると思います。

https://sarahmhigley.com/writing/tooltips-in-wcag-21/#best-practices-summary

- Tooltipのトリガーはインタラクティブな要素のみにする
    - キーボードフォーカスを受取らない要素では、読み飛ばされやすいです
    - マウスオーバーでTooltipを開くにしても、非インタラクティブ要素にわざわざマウスを持ってくる機会は少ないので気がつきにくいです
- `aria-describedby` または `aria-labelledby` を使用して、UIコントロールをツールチップに関連付ける
    - くわしくは後編でふれたいとおみおます
- Tooltipに重要な情報を入れない
    - そもそも重要な情報は隠さずにすべてのユーザーが閲覧できる状態を作るべきだと思います

### 【閑話休題】title属性をTooltipとして扱うことは非推奨

すこし話はそれますが、 `title` 属性でもTooltipのような挙動をします。
`title` 属性を持つ要素にマウスオーバーを数秒していると、Tooltipの表示が出ます。

Qiitaも記事ページのサイドバーの「いいね」ボタンにtitle属性を持っているので、数秒マウスオーバーしていると下記のようなUIが表示されます。
Tooltipのためと言うよりは、読み上げの意図で実装しています。

|Qiitaのいいねボタンのtitle属性で表示されたTooltip|
|:---:|
|![Qiitaのいいねボタンのtitle属性で表示されたTooltipのスクリーンショット](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215239/70aca876-d02c-67d0-33e4-067d4e0c5929.png)|


これをTooltipとして利用できそうですが、HTML Living Standard の `title` 属性についての記述には非推奨だと記述がありました。

https://html.spec.whatwg.org/multipage/dom.html#the-title-attribute

>Relying on the title attribute is currently discouraged as many user agents do not expose the attribute in an accessible manner as required by this specification (e.g., requiring a pointing device such as a mouse to cause a tooltip to appear, which excludes keyboard-only users and touch-only users, such as anyone with a modern phone or tablet).

簡単に訳すと、「マウスなどのポインティングデバイスではツールチップを表示することはできるが、キーボードユーザーやタッチデバイスのユーザーでは表示することができないため、推奨されていません」とのことです。

アクセシブルなTooltipは、作る必要があります。

## 実装

こちらのコードをお借りして、読み解きたいと思います。
本記事内では抜粋なので、きちんと動くコードを確認する場合、下記のページの該当のソースをご確認ください。

紹介する下記のGitHubのコードは、 [Develop example of tooltip design pattern · Issue #127 · w3c/aria-practices](https://github.com/w3c/aria-practices/issues/127) で取り上げられた課題に対してクリアできるように作られています。

大変学びが深いと思いますので、本家をぜひご覧いただければと思います。

https://github.com/ZoeBijl/apg-tooltip?tab=readme-ov-file

Tooltipのデモページは下記から閲覧できます。

https://zoebijl.github.io/apg-tooltip/

### HTML

Tooltipの読み上げの対応を含めるといくつかサンプルコードがあるのですが、そのあたりの詳細は後編に紹介するとし、基本形として下記を参考にしたいと思います。
`button` タグにTooltipが適応される例です。

Tooltipとトリガーになる要素が `div` でラップされています。

`aria-describedby`、 `id` `role="tooltip"` は読み上げに関わってきます。

```html:apg-tooltip/index.html 19L-24L
<div class="tooltip-container">
  <button type="button" aria-describedby="description">
    Settings
  </button>
  <p id="description" role="tooltip" class="hidden">View and manage settings</p>
</div>
```

### CSS

表示位置による装飾の記述も多いのですが、基本となるスタイルはこのあたりです。
`button` タグに `position: relative;` を適応して、周囲に表示できるようにしてあります。

Tooltipが非表示のときは `display: none;` で読み上げもフォーカスも当たらないようになっていました。

```css:apg-tooltip/tooltips.css 51L-7０L
:root {
  --tooltip-thingy-height: .5em;
}

...略...

/* Tooltip styles */
[role="tooltip"] {
  position: absolute;
  top: calc(100% + calc(var(--tooltip-thingy-height) * 2));
  left: 50%;
  transform: translateX(-50%);
  margin: 0;
  padding: .5em 1em;
  border-radius: .25em;
  color: white;
  background: black;
  min-width: max-content;
  max-width: 10em;
  box-shadow: 0 1px 2px hsl(0, 0%, 0%);
}

/* Hides the tooltip */
.hidden {
  display: none;
}

```

### Javascript

最後にJavascirptです。

主に2Lから82Lまでが、[ARIA Authoring Practices Guide (APG) | Tooltip Pattern](https://www.w3.org/WAI/ARIA/apg/) のTooltipの挙動に関わる記述です。

マウス、タッチ、キーボードに対する指定と、EscapeキーでTooltipを閉じる記述があります。

```javascript:apg-tooltip/tooltips.js 2L-82L
  constructor(element) {
    this.container = element
    this.trigger = element.querySelector('[data-tooltip-trigger]')
    this.tooltip = element.querySelector('[role=tooltip]')
    this.tooltipPosition = this.getTooltipPosition()
    this.globalEscapeBound = this.globalEscape.bind(this)
    this.globalPointerDownBound = this.globalPointerDown.bind(this)
    this.initialiseClassList()
    this.bindEvents()
  }

  // Basic actions
  openTooltip() {
    this.showTooltip()
    this.checkBoundingBox()
    this.attachGlobalListener()
  }

  closeTooltip() {
    this.hideTooltip()
    this.resetBoundingBox()
    this.removeGlobalListener()
  }

  // Binding event listteners
  bindEvents() {
    // Events that trigger openTooltip()
    // Open on mouse hover
    this.container.addEventListener('mouseenter', this.openTooltip.bind(this))
    // Open when a touch is detected
    this.container.addEventListener('touchstart', this.openTooltip.bind(this))
    // Open when the trigger gets focus
    this.trigger.addEventListener('focus', this.openTooltip.bind(this))

    // Events that trigger closeTooltip()
    // Close when the mouse cursor leaves the trigger or tooltip area
    this.container.addEventListener('mouseleave', this.closeTooltip.bind(this))
    // Close when the trigger loses focus
    this.trigger.addEventListener('blur', this.closeTooltip.bind(this))
  }

  attachGlobalListener() {
    document.addEventListener('keydown', this.globalEscapeBound)
    document.addEventListener('pointerdown', this.globalPointerDownBound)
  }

  removeGlobalListener() {
    document.removeEventListener('keydown', this.globalEscapeBound)
    document.removeEventListener('pointerdown', this.globalPointerDownBound)
  }

  globalEscape(event) {
    if (event.key === 'Escape' || event.key === 'Esc') {
      this.closeTooltip()
    }
  }

  // Close the tooltip if the target is anything other than the components within the tooltip widget
  globalPointerDown(event) {
    switch (event.target) {
      case this.container:
      case this.trigger:
      case this.tooltip:
        event.preventDefault()
        break
      default:
        this.closeTooltip()
        this.trigger.blur()
    }
  }

  // Show or hide the tooltip
  showTooltip() {
    this.container.classList.add('tooltip-visible')
    this.tooltip.classList.remove('hidden')
  }

  hideTooltip() {
    this.container.classList.remove('tooltip-visible')
    this.tooltip.classList.add('hidden')
  }
```

## 【前編】Tooltipの基本と挙動のまとめ

- W3Cの[ARIA Authoring Practices Guide (APG) | Tooltip Pattern](https://www.w3.org/WAI/ARIA/apg/) でもTooltipのアクセシビリティ対応については、まだ確定されていない
- Tooltipを使わずに表現できるなら使わないほうが良い
- Tooltipの挙動は
    - キーボードフォーカス、またはマウスオーバーでTooltipのポップアップを表示
    - フォーカスアウト、マウスアウト、`Escape`キーによって、表示が消える
    - Tooltipはフォーカスを受け取らない
        - フォーカス可能な要素を含む場合は、非モーダルダイアログで実装する

## 後半

後半に続きます

https://qiita.com/gilly/items/70e319165383d4b9221a

## 参照

- https://www.w3.org/WAI/ARIA/apg/patterns/tooltip/
- https://sarahmhigley.com/writing/tooltips-in-wcag-21
- https://zoebijl.github.io/apg-tooltip/
- https://github.com/ZoeBijl/apg-tooltip?tab=readme-ov-file
- https://html.spec.whatwg.org/multipage/dom.html#the-title-attribute