🖱️

Buttonについて - React Ariaの実装読むぞ

2024/12/02に公開

こんにちは、フロントエンドエンジニアの mehm8128 です。

それでは今日は Button について書いていきます。

https://react-spectrum.adobe.com/react-aria/useButton.html

useButton とは

ボタンを作るための hook で、マウスやタッチ、キーボードによるインタラクションをサポートします。

使用例

ドキュメントからそのまま取ってきています。

function Button(props) {
  let ref = useRef<HTMLButtonElement | null>(null);
  let { buttonProps } = useButton(props, ref);
  let { children } = props;

  return (
    <button {...buttonProps} ref={ref}>
      {children}
    </button>
  );
}

本題

APG はこちらです。

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

usePressについて

useButtonで利用されているusePresshook では様々な a11y 対応がされていて、ブログ記事にまとめられています。
https://react-spectrum.adobe.com/blog/building-a-button-part-1.html

簡単に概要をまとめます。

主にタッチデバイスへの対応の話が書かれていて、マウスでできる操作がタッチイベントだと難しかったり、:active:hoverの動作がユーザーの期待と一致しない場合があって UX が悪くなりがちだったりといった問題が挙げられています。

そこで React Aria では Pointer Events API が利用されています。この API はマウス、タッチ、ペンによる操作に対応していて、pointerTypeプロパティによってどの機器によってイベントが発火されたのかも知ることができます。
usePressではこれを用いて上記の問題の解決や、その他タッチキャンセルやテキスト選択、キーボード操作時にキーを押しっぱなしにすることによるイベントの複数発火防止などにも対応しています。usePressuseButton以外にもいくつかの hooks で用いられています。

実装を読みたい人はこちらから。実装を読むとかいうタイトルのアドベントカレンダーですが、僕はほんのちょっとしか読んでいません(読んだ部分については明日書きます)。

https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/interactions/src/usePress.ts

関連して、ホバーについては別の記事と、これを簡潔にまとめたまっつーさんの記事が参考になります。後者の記事には Pointer Events API のサンプルコードもあります。

https://react-spectrum.adobe.com/blog/building-a-button-part-2.html

https://zenn.dev/cybozu_frontend/articles/hover-style-on-supported-devices

isPendingについて

こちらは hooks にはないのですが、React Aria Components に含まれている props です。
データの送信中などにボタンを一時的に pending にしておくことができます。
isPending={true}のときはボタンにaria-disabled="true"がつきます。また、即座に ProgressBar(role="progressbar"のもの)をアクセシビリティツリーに含める必要があります。

https://react-spectrum.adobe.com/react-aria/Button.html#accessibility

progressbarrole はデフォルトでライブリージョンのように振る舞うので、isPendingになった瞬間に ProgressBar のラベルが読み上げられます。

https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions#roles_with_implicit_live_region_attributes

と思っていたのですが、読み上げられなさそうで React Aria では手動で読み上げが実装されていました...。

https://github.com/adobe/react-spectrum/blob/b0f15697245de74ebc99ab3d687f5eb3733d3a34/packages/react-aria-components/src/Button.tsx#L150-L158

ローディング状態になったときに if 文の 1 つ目のブロックが実行され、ローディング状態が解除されたときにelse ifのブロックが実行されます。
announce関数ではLiveAnnouncerという独自のオブジェクトが利用されていて、visually hidden なdiv要素を用意しておいて、そこにaria-liveなどをつけた要素を入れておきます。さらにその中に読み上げさせたいテキストをdiv要素で追加する、もしくはaria-labelledbyでテキストを指定したければ、aria-labelledbyでテキストへの参照をつけたimgrole(おそらくaria-labelledbyがつけられる role ならなんでも OK)のdiv要素を追加する、という実装になっています。

https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/live-announcer/src/LiveAnnouncer.tsx

実際にレンダリングされる HTML を見た方が分かりやすいと思うので、大体こんな感じのコードです。
実際の HTML が見たい方はドキュメントのデモとか開発用 Storybook とかで HTML 内をdata-live-announcerで検索かけると出てきます。

<div data-live-announcer="true" style="visually hiddenにするstyle">
  <div role="log" aria-live="assertive" aria-relevant="additions">
    <!--`announce`関数発火時にここに要素が追加される-->
  </div>
  <div role="log" aria-live="polite" aria-relevant="additions">
    <!--例えばこんな感じ-->
    <div>読み上げたいテキスト</div>
    <!--もしくはこう-->
    <div role="img" aria-labelledby="読み上げたいテキストの要素のid"></div>
  </div>
</div>

ちなみにaria-について補足しておくと、aria-liveは要素が更新されたときにスクリーンリーダーに読み上げさせるためのもので、値として、指定していないときと同じoff、更新されたらすぐに通知するassertive、他の読み上げが終わってから通知するpoliteを指定できます。今回はassertiveが指定されています。
aria-relevant="additions"は要素が追加されたとき通知することを表し、他には要素が削除されたときに通知するremovalsなどがあります。今回はaria-rivearia-relevantをつけているdivタグの中に、読み上げさせたいタイミングで読み上げテキストを含む要素を一時的に追加するような運用をしているのでadditionsが指定されています。

まとめ

明日の担当は @mehm8128 さんで、 Link についての記事です。お楽しみにー

Discussion