この記事の概要
React 18で新しくuseIdというhooksが使えるようになりました。
コンポーネントにWAI-ARIAを導入するにあたって、便利になると思ったのでいくつかのパターンを紹介します。
また、Qiita Engineer Festa 2022の「React 18、あなたならどう使いこなす?」への投稿記事でもあります。
記事を書こうと思った理由——useId登場以前の迷い
実例を紹介する前に、なぜこの記事を書こうと思ったかについて説明します。
例えばaria-labelledby
を使う場合、以下のようになります。
(コードはMDN Docsのaria-labelledbyのページより拝借)
<span role="checkbox" aria-checked="false" tabindex="0" aria-labelledby="tac"></span>
<span id="tac">I agree to the Terms and Conditions.</span>
簡単に説明すると、まずrole="checkbox"
のspanはaria-labelledby="tac"
の記述によってid="tac"
のspanを参照しています。
そのためスクリーンリーダー使用時にrole="checkbox"
のspanにフォーカスを当てるとI agree to the Terms and Conditions.
と読み上げられます。
aria-labelledby
が無かった場合、見た目としては紐付いていてもスクリーンリーダーはそれを検知できません。
マシンからしたら、謎のチェックボックスがぽつんと置いてある感じ……とでも言えるのでしょうか。
aria-labelledby
も含めて、この後紹介する属性はすべて他要素のidを参照して読み上げなどを支援します。
ここで、idを使用するため若干の難しさが生まれます。
コンポーネントを作るにあたって、どの場所で何回使われるか?は考慮に入れるべきではありません。
しかしidは1ページで1つしか使ってはいけないので、単に固定した文字列を指定すると、他要素と重複してしまう可能性があります。
回避するために、propsとして毎回idを指定する、nanoidなどのライブラリを使用する、といった選択肢が挙げられるでしょう。
前者は結局人間が指定するのでうっかり重複してしまうかもしれませんし、後者は(気持ちの問題ですが)このために依存関係を増やすのは……となっていました。
上記のような悩みがあったため、React自体がユニークなidを生成する仕組みを提供してくれたのはありがたいです。
useIdを使う
useIdを使う場合、以下のように書きます。(ほぼReact公式ドキュメントのuseIdの説明のままです。)
特に迷うことはありませんね。
import { useId } from "react";
export function Checkbox() {
const id = useId();
return (
<>
<label htmlFor={id}>Do you like React?</label>
<input id={id} type="checkbox" name="react" />
</>
);
}
ここから、2つの例を紹介します。
基本はMDN DocsにあるHTMLのコードをReactに置き換えて記載しています。
1つのコンポーネントに1つのidで済むパターン
上で書いた内容、ほぼそのままです。
import { useId } from "react";
export function Checkbox({ description }) {
const id = useId();
return (
<>
<span
role="checkbox"
aria-checked={isChecked} // 何かしらでコントロール。記事の内容とは直接関係ないので省略。
tabIndex={0}
aria-labelledby={id}
></span>
<span id={id}>{description}</span>
</>
);
}
例ではaria-labelledby
を使いましたが、書き方はaria-describedby
やaria-details
、aria-owns
なども同じです。
参考:
1つのコンポーネントに複数のidが必要なパターン
inputコンポーネントを作るとして、label要素とエラーメッセージもセットにしておきたいものです。
すると、labelのhtmlFor
で指定するidと、inputのaria-errormessage
で指定するためのidが2つ必要になります。
このように1つのコンポーネント内で複数のidが必要な場合はid + 接尾辞
の形式をとってください。
これもReact公式ドキュメントのuseIdの説明に記載があります。
と言っても、2回以上useId()
を呼んでもエラーなどは出ません。
パフォーマンスなどの問題があるのでしょうか……調べきれなかったため、もし分かる方がいたら是非コメントで教えてください。
import { useId } from "react";
export function Input({ label, errorMessage, ...props }) {
const id = useId();
return (
<p>
<label htmlFor={`${id}-input`}>{label}</label>
<input
type={props.type}
name={props.name}
id={`${id}-input`}
aria-invalid={!!errorMessage}
aria-errormessage={`${id}-errorMessage`}
/>
{!!errorMessage && <span id={`${id}-errorMessage`}>{errorMessage}</span>}
</p>
);
}
もう少し複雑なパターンだと、タブリストのUIなんかがあるでしょう。
このままでは全然役に立たなそうなコンポーネントですが、イメージは掴めると思うので例として示します。
import { useId } from "react";
export function TabList({ firstTab, secondTab }) {
const id = useId();
return (
<div>
<div role="tablist" aria-label="Sample Tabs">
<span
role="tab"
aria-selected={isSelected} // 何かしらでコントロール。記事の内容とは直接関係ないので省略。
aria-controls={`${id}-panel-1`}
id={`${id}-tab-1`}
tabIndex={tabIndex} // 何かしらでコントロール。記事の内容とは直接関係ないので省略。
>
First Tab
</span>
<span
role="tab"
aria-selected={isSelected} // 同上
aria-controls={`${id}-panel-2`}
id={`${id}-tab-2`}
tabIndex={tabIndex} // 同上
>
Second Tab
</span>
</div>
<div
id={`${id}-panel-1`}
role="tabpanel"
tabIndex={0}
aria-labelledby={`${id}-tab-1`}
hidden={isHidden} // 同上
>
<p>{firstTab}</p>
</div>
<div
id={`${id}-panel-2`}
role="tabpanel"
tabIndex={0}
aria-labelledby={`${id}-tab-2`}
hidden={isHidden} // 同上
>
<p>{secondTab}</p>
</div>
</div>
);
}
参考:
最後まで読んでくださってありがとうございます!
Twitterでも情報を発信しているので、良かったらフォローお願いします!