📜

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

2024/12/09に公開

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

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

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/

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

https://react-spectrum.adobe.com/react-aria/useListBox.html#sections

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

https://github.com/adobe/react-spectrum/blob/5ed06068ee2742f32e066ffa8eb55fd93a083123/packages/%40react-aria/listbox/src/useListBoxSection.ts#L45-L59

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がそのオプションが全体の何番目のオプションなのかを表すものです。

https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-setsize

https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-posinset

実装はこのあたりです。

https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/listbox/src/useOption.ts#L121-L125

オプションのラベル

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 が実装されています。

https://github.com/adobe/react-spectrum/blob/5ed06068ee2742f32e066ffa8eb55fd93a083123/packages/%40react-aria/selection/src/useTypeSelect.ts#L44-L47

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

https://github.com/adobe/react-spectrum/blob/5ed06068ee2742f32e066ffa8eb55fd93a083123/packages/%40react-aria/selection/src/useSelectableList.ts#L62

shouldSelectOnPressUp

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

https://github.com/adobe/react-spectrum/blob/8228e4efd9be99973058a1f90fc7f7377e673f78/packages/%40react-aria/selection/src/useSelectableItem.ts#L237-L298

まとめ

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

Discussion