NumberFieldについて

React Ariaの実装読むぞ


目次
  1. とは
  2. 使用例
  3. 本題
    1. role
    2. 様々な format
    3. タッチパッド の
    4. inputMode
  4. まとめ
Warn

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

Note

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

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

useNumberField とは #

数値の入力欄を作るための hook で、様々な format や i18n をサポートしています。

使用例 #

ドキュメントからそのまま取ってきています。

import { useNumberFieldState } from "react-stately";
import { useLocale, useNumberField } from "react-aria";

// Reuse the Button from your component library. See below for details.
import { Button } from "your-component-library";

function NumberField(props) {
  let { locale } = useLocale();
  let state = useNumberFieldState({ ...props, locale });
  let inputRef = React.useRef(null);
  let {
    labelProps,
    groupProps,
    inputProps,
    incrementButtonProps,
    decrementButtonProps,
  } = useNumberField(props, state, inputRef);

  return (
    <div>
      <label {...labelProps}>{props.label}</label>
      <div {...groupProps}>
        <Button {...decrementButtonProps}>-</Button>
        <input {...inputProps} ref={inputRef} />
        <Button {...incrementButtonProps}>+</Button>
      </div>
    </div>
  );
}

本題 #

APG はこちらです。

role #

inputtypenumberではなくてtextにしているので、spinbuttonrole ではなくてtextboxrole になっています。これは後述する色々なフォーマットに対応するためです。 その代わりに、要素の役割についてスクリーンリーダーの読み上げ用に補足説明を入れるaria-roledescriptionが利用されています。今回の場合、日本語では「数値フィールド」と読み上げられるようになっています。

また、useSpinButtonという hooks から返されるspinButtonPropsによってspinbuttonrole に上書きすることも可能なのですが、React Aria の実装ではさらにそれをrole: nullで上書きしてデフォルトのtextboxrole にしています。これは、Voice Over 利用時にspinbuttonrole にフォーカスできなくなってしまっていることが理由らしいです。 ちなみに、+/-ボタンはキーボードの矢印キーでインクリメント・デクリメントの操作が可能なことから Tab フォーカスされないようになっています。

さらに、spinbuttonrole ではないので、spinButtonPropsから返されるaria-valuemaxなどのaria-属性もnullに上書きしています。

様々な format #

ドキュメントにもあるように、小数点やパーセント表記、通貨、その他の単位のフォーマットがサポートされています。この変換を行ったり、+/-ボタンによるインクリメント・デクリメントをサポートしたりするために、useNumberFieldStateという hook が用意されています。

数値フィールド内の値はnumberValueinputValueという 2 つの state で管理されています。numberValueは内部で持つ用のnumber型の値、inputValueは表示用のstring型の値で、後者は単位がついたりしているものです。 どちらもuseNumberFieldState内でuseStateを用いて管理されています。numberValueuseSpinButtonに渡されてspinButtonPropsaria-valuenowに用いられ、inputValueinputPropsとしてuseNumberFieldから返されてinput要素に渡されます。

タッチパッド の onWheel #

マウスホイールを上下に動かすことで increment/decrement ができますが、タッチパッドのスクロール操作(一般的に指 2 本でやるやつ)でももちろんできます。 しかし、タッチパッドだと上下以外にも横方向へのスクロールや、斜め方向へのスクロールもできるので、誤って操作してしまうのを防ぐために、横方向のスクロール(e.deltaX)が縦方向のスクロール(e.deltaY)より大きい場合は increment/decrement されないようになっています。

inputMode #

昨日紹介した inputMode です。 今回は数値に特化しているので hook 内部で指定しています。 しかし、ブラウザや OS によって同じ inputMode でも表示されるキーボードが異なってしまうので、負の値を許容する NumberField のときは-ボタンが表示されるキーボードを表示する、とかをちゃんと条件分岐して設定しています。

具体的に説明します。 iPhone ではinputModenumericでもdecimalでも-ボタンが表示されないので、負の値を許容する場合はinputModetextにします。 Android ではinputModenumericの場合に-ボタンがあり、decimalのときにはないので、負の値を許容する場合はinputModenumericにします。

細かいところですがしっかり各端末の挙動が調査されて適切な設定がされていることが分かります。

公式のブログ記事にもまとめられていました(Mobileセクション以外は i18n の回で解説予定です)。

まとめ #

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