ListBoxについて

React Ariaの実装読むぞ


目次
  1. とは
  2. 使用例
  3. 本題
    1. オプションのグルーピング
    2. オプションのラベル
    3. Typeahead
    4. shouldSelectOnPressUp
  4. まとめ
Warn

この記事は他サイトから移行したものです。

Note

この記事は React Aria の実装読むぞ - Qiita Advent Calendar 2024 の 9 日目の記事です。

こんにちは、フロントエンドエンジニアの mehm8128 です。 今日は ListBox について書いていきます。

useListBox とは #

select要素のようなセレクトボックスを作るための hook です。

使用例 #

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

function ListBox<T extends object>(props: AriaListBoxProps<T>) {
  let state = useListState(props);

  let ref = React.useRef(null);
  let { listBoxProps, labelProps } = useListBox(props, state, ref);

  return (
    <>
      <div {...labelProps}>{props.label}</div>
      <ul {...listBoxProps} ref={ref}>
        {[...state.collection].map((item) =>
          item.type === "section" ? (
            <ListBoxSection key={item.key} section={item} state={state} />
          ) : (
            <Option key={item.key} item={item} state={state} />
          )
        )}
      </ul>
    </>
  );
}

function Option({ item, state }) {
  let ref = React.useRef(null);
  let { optionProps } = useOption({ key: item.key }, state, ref);

  let { isFocusVisible, focusProps } = useFocusRing();

  return (
    <li
      {...mergeProps(optionProps, focusProps)}
      ref={ref}
      data-focus-visible={isFocusVisible}
    >
      {item.rendered}
    </li>
  );
}

本題 #

APG はこちらです。 https://www.w3.org/WAI/ARIA/apg/patterns/listbox/

オプションのグルーピング #

にあるように、useListBoxSectionでグループ化ができます。 実装的にはgrouprole でグループ化して、presentationrole にした heading でgrouprole の要素に accessible name を与えています。

Techincally, listbox cannot contain headings according to ARIA.については、WAI-ARIA の listboxrole の項目Allowed Accessibility Child Rolesを見てください。子要素の role として単純なオプションとなるoptionrole か、オプションをグルーピングするためのgrouprole しか許可されていないので、グルーピングしたセクションの見出しにheadingrole を用いることができないという意味です。

グルーピングすることで、Static itemsの例だと以下のように読み上げられます。

Choose sandwich contents  リスト
Veggies  グループ
Lettuce  9の1
Tomato  選択なし  9の2
Onion  選択なし  9の3
Protein  グループ
Ham  選択なし  9の4
Tuna  選択なし  9の5
Tofu  選択なし  9の6
Condiments  グループ
Mayonaise  選択なし  9の7
Mustard  選択なし  9の8
Ranch  選択なし  9の9

グループに入ったタイミングで一度だけグループ名が読み上げられます。

aria-setsizearia-posinset #

Virtual Scroll する場合に利用します。aria-setsizeが ListBox 全体のオプションの数、aria-posinsetがそのオプションが全体の何番目のオプションなのかを表すものです。

実装はこのあたりです。

オプションのラベル #

APG の最初の方に、ListBox の各オプションのラベルについて言及がありました。 https://www.w3.org/WAI/ARIA/apg/patterns/listbox/

Avoiding very long option names facilitates understandability and perceivability for screen reader users.

長いラベル名はやめましょう。

Sets of options where each option name starts with the same word or phrase can also significantly degrade usability for keyboard and screen reader users.

各オプションのラベルの最初が同じだと、毎回同じものが読み上げられて探しにくい。

みたいな感じのことが書かれています。

後者は例えば「日本 東京都」という選択肢と「日本 大阪府」という選択肢があると、「日本」までは同じなのでこれが毎回読み上げられると目当てのものを探すのが大変、という話ですね。こういう場合は国名と都市名で別で ListBox を用意するのがよい、とのことです。

Typeahead #

useListBoxの中で使われているuseSelectableListの中で使われているuseSelectableCollectionの中で使われているuseTypeSelectで、Typeahead が実装されています。

またこのために、useSelectableList内でuseCollatorを用いて i18n 対応もされています。これについては i18n の回で説明します。

shouldSelectOnPressUp #

props としてallowsDifferentPressOriginshouldSelectOnPressUptrueで渡すと、「メニューのトリガーボタン上で pointer down し、そのままメニュー内のボタンにカーソルを移動して pointer up する」というような、一回のクリックでメニューを開いてそのままメニュー内のボタンを発火させる操作ができるようになっています。以下のコードだと、271 行目のonSelectが発火します。 2 日目の記事で説明した Pointer Events APIが役に立っています。

まとめ #

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