🫶

UIライブラリの使用時も、セマンティックなHTMLでアクセシビリティを上げる

2023/10/15に公開

お疲れ様です。最近めっきり涼しくなってきてテンション爆上がりです。

はじめに

最近、所属している会社でWeb アプリケーションアクセシビリティ ── 今日から始める現場からの改善 (WEB+DB PRESS plus)の輪読会をしたり、Web アクセシビリティに造詣が深いエンジニアの方が参画してくれたおかげで、自分自身もアクセシビリティについて考える機会が増えてきました。そのこともあってか、まだまだアクセシビリティ初心者の私でも業務中に『あ、こうするだけでアクセシビリティが改善されるんや!』と体験したことがあったので、そのことについてメモがてら書き残そうと思います。

Web アクセシビリティとは

Web アクセシビリティを簡単に説明しますと、ウェブサイトやウェブアプリケーションが全ての人々、特に視覚障害・聴覚障害・運動障害・認知障害などのような障害を持った人々や高齢者にとっても利用しやすくなるように設計・開発されることを指します。

より詳しく知りたい方は、以下のリンクの Web エンジニアとしていま知っておきたい Web アクセシビリティ をご覧ください。自分もとても参考になりました。
https://zenn.dev/ymrl/articles/7f41ad2f39f714

それか、Web アプリケーションアクセシビリティ ── 今日から始める現場からの改善 (WEB+DB PRESS plus)を読んでみることをお勧めします。

次章からはどのように、改善できたかを見ていきます。

改善前の実装内容

React で テキストフィールドを動的に増やしたり、減らしたりできる機能を実装していました。UI ライブラリにはMui、フォームライブラリにReact Hook Formを使用しています。コードは以下です。

RhfTextFieldItem.tsx
import { FC } from "react";
import { Stack, TextField, IconButton } from "@mui/material";
import { Controller, Control } from "react-hook-form";
import { TrashCan as TrashCanIcon } from "mdi-material-ui";
import { FormType } from "../App";

type Props = {
  control: Control<FormType>;
  index: number;
  onRemove: () => void;
}

export const TextFieldItem: FC<Props> = ({
  control,
  index,
  onRemove
}) => {
  return (
    <Stack spacing={2} direction="row">
      <Controller
        name={`textFields.${index}.text`}
        control={control}
        render={({ field }) => {
          return (
            <TextField
              inputRef={field.ref}
              value={field.value}
              onBlur={field.onBlur}
              onChange={field.onChange}
              name={field.name}
            />
          );
        }}
      />
      <IconButton sx={{ alignSelf: "center" }} onClick={() => onRemove()}>
        <TrashCanIcon />
      </IconButton>
    </Stack>
  )
}
RhfTextFieldList.tsx
import { FC } from "react";
import { Stack, Button } from "@mui/material";
import { useFieldArray, Control } from "react-hook-form";
import { FormType } from "../App";
import { RhfTextFieldItem } from "./RhfTextFieldItem";

type Props = {
  control: Control<FormType>;
};

export const RhfTextFieldList: FC<Props> = ({ control }) => {
  const { fields, append, remove } = useFieldArray({
    control,
    name: "textFields"
  });

  return (
    <>
      <Stack spacing={2}>
        {fields.map((field, index) => {
          return (
            <RhfTextFieldItem
              key={field.id}
              control={control}
              index={index}
              onRemove={() => remove(index)}
            />
          )
        })}
      </Stack>
      <Button
        sx={{
          marginTop: 2
        }}
        onClick={() => append({ text: "" })}
      >
        追加
      </Button>
    </>
  )
}

App.tsx
import { useForm } from "react-hook-form";
import { Box } from "@mui/material";
import { RhfTextFieldList } from "./components/RhfTextFieldList";

export type FormType = {
  textFields: {
    text: string;
  }[];
};

export default function App() {
  const { control } = useForm<FormType>({
    defaultValues: {
      textFields: [
        {
          text: "サンプルテキスト"
        },
        {
          text: "サンプルテキスト"
        },
        {
          text: "サンプルテキスト"
        }
      ]
    }
  });
  return (
    <div className="App">
      <Box sx={{ p: 2 }}>
        <RhfTextFieldList control={control} />
      </Box>
    </div>
  );
}

RhfTextFieldItemは、テキストフィールドと削除アイコンをまとめたコンポーネントです。このコンポーネントは、ユーザーが入力したテキストを表示し、そのテキストを削除するためのアイコンボタンを提供します。React-Hook-FormController を使用して、フォームの値の管理を効率的に行っています。

RhfTextFieldListは、複数のRhfTextFieldItemコンポーネントをリストとして表示するコンポーネントです。このコンポーネントは、useFieldArrayフックを使用して、動的にテキストフィールドのリストを管理します。また、新しいテキストフィールドを追加するための「追加」ボタンも提供しています。

App.tsxファイルでRhfTextFieldListを呼び出すようにしています。

実際に動くものを codesandbox で用意しまいた。

これで機能は満たしていますが、これだとアクセシビリティ的に不十分なのです。

アクセシビリティ的な問題点

では、先の実装のどういう部分がアクセシビリティ的に不十分なのか、見ていきましょう。

スクリーンリーダーの起動

私は Mac を使っているので、スクリーンリーダーに VoiceOver を使用します。VoiceOver の起動はデフォルトだと ⌘+F5キーです。諸々の操作方法は以下の Qiita の記事がとても参考になったので、ご覧ください。
https://qiita.com/tsmd/items/3d8e265ae60dfb1e187d

スクリーンリーダーで移動してみる

では、先ほど実装したものをスクリーンリーダーで移動してみます。
voiceOverで移動してみた

問題点

一見、問題ないように思えるかもしれませんが、以下の 2 点が問題として挙げられます。

  1. スクリーンリーダーのカーソルが何個目のテキストフィールドに当たっているか認識できない
  2. そのボタンが何を意図するボタンなのかが理解することができない

これらの対策は難しいように思うかもしれませんが、セマンティックな HTML を意識するだけで大幅に改善されます。

セマンティックな HTML とは

セマンティックは「意味や目的を持たせる」という意味で使用されます。要するに、セマンティックな HTML とは、<h1>や<ul>のような HTML タグを正しく使い分ける ことを指します。セマンティックな HTML が適切に使用されていると、スクリーンリーダーはページの構造やコンテンツの意味を正確に伝えることができます。

改善内容

先の章で挙げた問題点をどのように改善していくかを見ていきましょう!

1.スクリーンリーダーのカーソルが何個目のテキストフィールドに当たっているか認識できない

RhfTextFieldListのコードを確認します。

RhfTextFieldList.tsx
export const RhfTextFieldList: FC<Props> = ({ control }) => {
  // ...
  return (
    <>
      <Stack spacing={2}>
        {fields.map((field, index) => {
          return (
            <RhfTextFieldItem
              key={field.id}
              control={control}
              index={index}
              onRemove={() => remove(index)}
            />
          );
        })}
      </Stack>
      {/* ... **/}
    </>
  )
}

Mui のStackコンポーネントは、要素を垂直または水平に一定のスペーシングで配置できる便利なコンポーネントです。これを使ってRhfTextFieldItemを垂直に一定間隔で配置しています。ただ、このコンポーネントは画像のようにデフォルトだと <div>要素としてレンダリングされます。
Stackを使用した際の、devtool

<div>要素でレンダリングされているので、見た目はリストのようになっていても HTML の構造的には、単純なブロックが置いてあるだけと認識されるので、スクリーンリーダーで読み上げる時に何個目のテキストフィールドかわからないのです。では、これをセマンティックな HTML に変えてみましょう!

RhfTextFieldList.tsx
export const RhfTextFieldList: FC<Props> = ({ control }) => {
  // ...
  return (
    <>
-     <Stack spacing={2}>
+     <Stack
+       spacing={2}
+       component="ul"
+       sx={{
+         listStyleType: "none",
+         padding: 0,
+         margin: 0
+       }}
+     >
        {fields.map((field, index) => {
          return (
+           <li key={field.id}>
              <RhfTextFieldItem
-               key={field.id}
                control={control}
                index={index}
                onRemove={() => remove(index)}
              />
+           </li>
          );
        })}
      </Stack>
      {/* ... **/}
    </>
  )
}

Stackコンポーネントのcomponentプロパティを使用することで、異なる HTML 要素や React コンポーネントに変更することができるので、リストにするために<ul>要素を設定し、RhfTextFieldItem<li>要素で括ることによってリストの各項目を定義します。このようにすることで、<ul>要素<li>要素でレンダリングされます。
Stackを<ul>でレンダリングするようにした際の、devtool

これでセマンティックな HTML になりました。ここで、もう一度 voiceOver を使用してみて、改善前のものと見比べてみましょう。

改善前 改善後
改善前のものでvoiceOverで移動してみた 改善前のものでvoiceOverで移動してみた

改善後の voiceOver ではリストの場所にカーソルが入ってきた時に、「リスト 3 項目」と読み上げられます。そして、リストのアイテムにカーソルが入る時に、「3 の 1」と読み上げられ、今カーソルが当たっているリストの場所がわかるようになります。 🎉

2. そのボタンが何を意図するボタンなのかが理解することができない

RhfTextFieldItemの削除アイコン部分のコードを確認します。

RhfTextFieldItem.tsx
// ...
export const RhfTextFieldItem: FC<Props> = ({ control, index, onRemove }) => {
  return (
    <Stack spacing={2} direction="row">
      {/** ... */}
      <IconButton
        sx={{ alignSelf: "center" }}
        onClick={() => onRemove()}
      >
        <TrashCanIcon />
      </IconButton>
    </Stack>
  );
};

IconButtonの children に<TrashCanIcon />を設定しています。視覚的にはアイコンからこのアイコンをクリックしたら何が起こるか推測できますが、スクリーンリーダーのユーザーにはその情報は伝わりません。voiceOver だと 「ボタン」としか読み上げられません。
voiceOverでIconButtonを読み上げる時

これをスクリーンリーダーユーザーにも伝わるようにするには、aria-labelというものを使います。aria-labelを簡単に説明しますと、要素の目的や機能をテキスト形式で明示的に示すためのものです。

RhfTextFieldItem.tsx
// ...
export const RhfTextFieldItem: FC<Props> = ({ control, index, onRemove }) => {
  return (
    <Stack spacing={2} direction="row">
      {/** ... */}
      <IconButton
        sx={{ alignSelf: "center" }}
        onClick={() => onRemove()}
+       aria-label="削除"
      >
        <TrashCanIcon />
      </IconButton>
    </Stack>
  );
};

このようにaria-labelを付与すると、以下のようにスクリーンリーダーに読み上げられます。改善前と比較してみます。

改善前 改善後
改善前のvoiceOverでIconButtonを読み上げる時 改善後のvoiceOverでIconButtonを読み上げる時

アイコンボタンにカーソルが当たった時に、「削除、ボタン」 と読み上げられるようになり、スクリーンリーダーで伝わるようになります!

おわりに

以上が、アクセシビリティの改善内容です。自分の感覚ですが、UI ライブラリを使用する際、ライブラリが提供するコンポーネントをそのまま利用することで、簡単に目的のスタイルを実現できるため、セマンティックな HTML の重要性を見失いがちです。しかし、UI ライブラリを使用する時こそ、手を抜かずにセマンティックな HTML を追求することが、アクセシビリティの向上に直結すると思いました。

まだまだ、アクセシビリティについては初心者で知識が不足していますが、最低限セマンティックな HTML は目指していき、これからも学んでいけたらなと思います。

最後に、アクセシビリティを意識した codesandbox と、そうでない codesandbox を貼っておきますので、スクリーンリーダーを使って確かめてみてください!読んでいただき、ありがとうございました。

アクセシビリティ意識なし

アクセシビリティ意識あり

参考記事、書籍

https://zenn.dev/ymrl/articles/7f41ad2f39f714
https://www.amazon.co.jp/Webアプリケーションアクセシビリティ──今日から始める現場からの改善-WEB-DB-PRESS-plus/dp/4297133660
https://qiita.com/tsmd/items/3d8e265ae60dfb1e187d

GitHubで編集を提案

Discussion