RadioとCheckboxについて

React Ariaの実装読むぞ


目次
  1. 使用例
  2. 本題
    1. styling
    2. Tab フォーカス
    3. 2 種類のフォーカス移動
    4. TreeWalker API
  3. まとめ
Warn

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

Note

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

こんにちは、フロントエンドエンジニアの mehm8128 です。 今日は Radio と Checkbox について書いていきます。そろそろしんどいです。

https://react-spectrum.adobe.com/react-aria/useRadioGroup.html https://react-spectrum.adobe.com/react-aria/useCheckbox.html https://react-spectrum.adobe.com/react-aria/useCheckboxGroup.html

useRadioGroupuseCheckbox とは #

ラジオボタンやチェックボックス、またそれらのグループを作るための 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 はこちらです。

https://www.w3.org/WAI/ARIA/apg/patterns/radio/ https://www.w3.org/WAI/ARIA/apg/patterns/checkbox/

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と呼びます。

こちらはついさっき見つけたページなのでページ全体を読めているわけではないですが、 参考になりそうなので貼っておきます。 https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#kbd_roving_tabindex

もう 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;

ちなみにfocusabletabindex="0"などのノードはもちろん、tabindex="-1"で programmatically にフォーカス可能なノードも含み、tabbabletabindex="-1"は含まず、Tab キーによってフォーカス可能なノードのみを表します。

これを用いて「一番下のラジオボタンにフォーカスされているときに下矢印キーが押されたら一番上のラジオボタンにフォーカスする」などといった動作が実現されています。

まとめ #

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