React18 の useId で a11y対応する
aria-controls
やaria-errormessage
・aria-labelledby
は、一意の ID 属性で 2 つの異なる要素を紐づける必要があります。
エラー文言付き Textbox
例として、以下の様な「errorMessage
を与えた時にエラー表示する」Textbox コンポーネントを見ていきます。
<Textbox
title="お名前"
inputProps={...register("name")}
errorMessage="正しく入力してください"
/>
対象となるのは「input 要素・エラー文言要素」です。この 2 者を紐づけるためには、エラー文言要素を指す ID 属性が必要です(aria-errormessage
として input 要素に指定)これを達成するには、props で ID 指定するか、内部的に ID を生成する必要があります。前者は面倒なので、後者で対応したいところです。
useId
React18 で追加された新しい Hook の useId
は、このようなケースに適した Hook です。下記引用の通り、ハイドレーション時の不整合が起きないことが担保されます。
useId はハイドレーション時の不整合を防ぎつつクライアントとサーバで一意な ID を生成するためのフックです。これは主に、一意な ID を必要とするアクセシビリティ API を組み込むようなコンポーネントライブラリで有用なものです。
以下の様にerrorMessageId
という ID を内部生成し、input 要素とエラー文言要素を紐づけます。props のerrorMessage?: string;
ひとつで、a11y に配慮した情報展開が可能なことが分かります。
import React, { useId } from "react";
import styles from "./style.module.scss";
export type Props = {
title: string;
inputProps: React.ComponentPropsWithoutRef<"input">;
errorMessage?: string;
};
export const Textbox = ({ title, inputProps, errorMessage }: Props) => {
const errorMessageId = useId(); // <- here
return (
<fieldset className={styles.module}>
<label>
<span role="heading">{title}</span>
<input
{...inputProps}
type="text"
aria-invalid={!!errorMessage}
aria-errormessage={errMessageId} // <- here
/>
</label>
{!!errorMessage && (
<p
id={errorMessageId} // <- here
className={styles.err}
>
{errorMessage}
</p>
)}
</fieldset>
);
};
a11y Testing
Storybook でエラー表示されるコンポーネントを作り、テストケースとして利用します。
export const ShowError: Story = {
storyName: "エラー表示",
args: { errorMessage: "正しく入力してください" },
};
@storybook/testing-react
のcomposeStorie
を利用します。次のテストは成功しますが、あまり良いテストではありません。もしこの様なテストコードが PR であがってきたら、a11y 観点で配慮されているか、確認が必要です。
const { ShowError } = composeStories(stories);
describe("components/molecules/Textbox", () => {
describe("errorMessage を与えたとき", () => {
test("エラー文言が表示されていること", async () => {
const { getByText } = render(<ShowError />);
// × 文言表示を確認しているのみで、a11y配慮が十分か不明
expect(getByText("正しく入力してください")).toBeInTheDocument();
});
});
});
同じコンポーネントのテストですが、次のテストは a11y 観点で配慮されているといえます。アクセシブルな名前({ name: "お名前" }
)で特定・invalid 判定し、toHaveErrorMessage でエラー文言補足されていることが分かります。
const { ShowError } = composeStories(stories);
describe("components/molecules/Textbox", () => {
describe("errorMessage を与えたとき", () => {
test("textbox のエラー詳細が補足されていること", () => {
const { getByRole } = render(<ShowError />);
// ◯ アクセシブルな名前で特定できる
const textbox = getByRole("textbox", { name: "お名前" });
// ◯ textbox に誤りがあることが分かる
expect(textbox).toBeInvalid();
// ◯ エラー文言で textbox が補足されていることが分かる
expect(textbox).toHaveErrorMessage("正しく入力してください");
});
});
});
FYI
aria-errormessage
でエラー文言を紐づけるサンプルは、こちらを参考にしました。
Discussion