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
を用いる方法です。フォーカスは常にgroup
role(ラジオならradiogroup
role)を持つ要素に当てておき、そのグループ内でアクティブな要素(選択されているラジオボタン)の 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