ListBoxについて - React Ariaの実装読むぞ
こんにちは、フロントエンドエンジニアの 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 はこちらです。
オプションのグルーピング
にあるように、useListBoxSection
でグループ化ができます。
実装的にはgroup
role でグループ化して、presentation
role にした heading でgroup
role の要素に accessible name を与えています。
Techincally, listbox cannot contain headings according to ARIA.
については、WAI-ARIA の listbox
role の項目のAllowed Accessibility Child Roles
を見てください。子要素の role として単純なオプションとなるoption
role か、オプションをグルーピングするためのgroup
role しか許可されていないので、グルーピングしたセクションの見出しにheading
role を用いることができないという意味です。
グルーピングすることで、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-setsize
とaria-posinset
Virtual Scroll する場合に利用します。aria-setsize
が ListBox 全体のオプションの数、aria-posinset
がそのオプションが全体の何番目のオプションなのかを表すものです。
実装はこのあたりです。
オプションのラベル
APG の最初の方に、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 としてallowsDifferentPressOrigin
とshouldSelectOnPressUp
をtrue
で渡すと、「メニューのトリガーボタン上で pointer down し、そのままメニュー内のボタンにカーソルを移動して pointer up する」というような、一回のクリックでメニューを開いてそのままメニュー内のボタンを発火させる操作ができるようになっています。以下のコードだと、271 行目のonSelect
が発火します。
2 日目の記事で説明した Pointer Events APIが役に立っています。
まとめ
明日の担当は @mehm8128 さんで、 GridList についての記事です。お楽しみにー
Discussion