RadioとCheckboxについて - React Ariaの実装読むぞ
こんにちは、フロントエンドエンジニアの mehm8128 です。
今日は Radio と Checkbox について書いていきます。そろそろしんどいです。
useRadioGroupとuseCheckbox とは
ラジオボタンやチェックボックス、またそれらのグループを作るための hooks です。
使用例
ドキュメントからそのまま取ってきています。
let RadioContext = React.createContext(null);
function RadioGroup(props) {
let { children, label, description, errorMessage } = props;
let state = useRadioGroupState(props);
let { radioGroupProps, labelProps, descriptionProps, errorMessageProps } =
useRadioGroup(props, state);
return (
<div {...radioGroupProps}>
<span {...labelProps}>{label}</span>
<RadioContext.Provider value={state}>{children}</RadioContext.Provider>
{description && (
<div {...descriptionProps} style={{ fontSize: 12 }}>
{description}
</div>
)}
{errorMessage && state.isInvalid && (
<div {...errorMessageProps} style={{ color: "red", fontSize: 12 }}>
{errorMessage}
</div>
)}
</div>
);
}
function Radio(props) {
let { children } = props;
let state = React.useContext(RadioContext);
let ref = React.useRef(null);
let { inputProps } = useRadio(props, state, ref);
return (
<label style={{ display: "block" }}>
<input {...inputProps} ref={ref} />
{children}
</label>
);
}
本題
APG はこちらです。
styling
スタイリングしやすいように、visually hidden でinput要素を隠します。
VisuallyHidden コンポーネントがあるので、これでinput要素を wrap するだけで OK です。
Tab フォーカス
ラジオグループの場合、Tab フォーカスはグループの中で選択されているラジオボタンか、選択されているラジオボタンがなければ最後にフォーカスされたラジオボタンにあたり、それ以外は Tab ではなくて矢印キーで移動します。
ただ、APG には選択されているラジオボタンがなければ、グループ内の最初のラジオボタンにフォーカスされることが多いと書いてありました。
If none of the radio buttons are checked, focus is set on the first radio button in the group.
2 種類のフォーカス移動
APG の例では 2 種類の方法でグループ内のラジオボタンのフォーカスを移動する方法が紹介されています。
1 つはtabindexを変化させる方法です。これは React Aria で用いられている方法です。選択されている要素をtabindex="0"にし、選択されていない要素をtabindex="-1"にします。矢印キーが押されるたびにこれを変化させていくことで、選択されている要素にフォーカスを当てていくことができます。この方法をRoving tab indexと呼びます。
こちらはついさっき見つけたページなのでページ全体を読めているわけではないですが、
参考になりそうなので貼っておきます。
もう 1 つの方法は、aria-activedescendantを用いる方法です。フォーカスは常にgrouprole(ラジオならradiogrouprole)を持つ要素に当てておき、そのグループ内でアクティブな要素(選択されているラジオボタン)の id をaria-activedescendantに渡すことで、アクティブな要素をスクリーンリーダーが読み上げてくれます。
TreeWalker API
矢印キーが押されたときに次にフォーカスするべき要素を特定するために、getFocusableTreeWalker関数が用いられています。
getFocusableTreeWalker について簡単に説明していきます。
この関数では、HTML のノードを探索するために利用できる TreeWalker という API が使われています。
Document.createTreeWalker関数でwalkerを作成します。第一引数にルートの要素、第二引数にどのような種類のノードを探索するか(whatToShow)というフラグを組み合わせたビットマスクを指定します。ビットマスクなので、例えばElementノードとCommentノードをどちらも探索したい場合はNodeFilter.SHOW_ELEMENT + NodeFilter.SHOW_COMMENTを指定すればよいです。
そして第三引数には、第二引数で指定したノードを探索していく中でさらにどういう条件を満たすノードを含み、どういう条件を満たすノードを含まないのかを指定する acceptNodeという callback 関数を指定します。各ノードに対してNodeFilter.FILTER_ACCEPTを return するとこのノードを含み、NodeFilter.FILTER_REJECTを返すとこのノードとそのサブツリーの全てのノードを含まず、NodeFilter.FILTER_SKIPを返すとこのノードのみを含まないでサブツリーは探索を続けます。
今回の場合、onKeyDownが発火したタイミングで次にフォーカス可能なラジオボタンを探すのか、前のフォーカス可能なラジオボタンを探すのかを判定し、walker.nextNode()やwalker.previousNode()、もしくはwalker.firstChild()やwalker.lastChild()などを呼んでいます。このタイミングで先ほどのacceptNode関数を発火し、探索していきます。今回はフォーカス可能なノードを探すのでselectorを以下のように定義し、(node as Element).matches(selector)でフォーカス可能かどうかを判定しています。
let selector = opts?.tabbable
? TABBABLE_ELEMENT_SELECTOR
: FOCUSABLE_ELEMENT_SELECTOR;
ちなみにfocusableはtabindex="0"などのノードはもちろん、tabindex="-1"で programmatically にフォーカス可能なノードも含み、tabbableはtabindex="-1"は含まず、Tab キーによってフォーカス可能なノードのみを表します。
これを用いて「一番下のラジオボタンにフォーカスされているときに下矢印キーが押されたら一番上のラジオボタンにフォーカスする」などといった動作が実現されています。
まとめ
明日の担当は @mehm8128 さんで、 Tooltip についての記事です。お楽しみにー
Discussion