数値入力で input[type="number"]を使ったら、ユーザから問い合わせが増えてしまった話

この記事は [SmartHR Advent Calendar 2020](https://qiita.com/advent-calendar/2020/smarthr) 23 日目の記事です。

こんばんは！ [@diescake](https://twitter.com/diescake) です。

今年は、React で「そこそこの規模で、そこそこ機能のフォームアプリケーション」を設計・実装する機会がありました。
技術選定で `formik` + `yup` を選んだので、その選定理由と結果を共有しよう！

……そんなふうに考えていた時期が私にもありました。

驚くほど筆が進まず、いつの間にやら当日どころか既に夜になってしまったので tips 的な話に逃げる運びとなりました。ご了承ください。

というわけで、数値入力フォームに何気なく `input[type="number"]` を利用したら、ユーザから問い合わせが増えてしまった話です！

## input[type="number"] とは？

今更説明するまでもないくらい基本的な要素ですが HTML において `input` はユーザの入力を受け付ける要素で、`type` 属性に `"number"` を指定すると、数値入力用のフォームを実現できます。

- MDN - \<input type="number" /\>
  - https://developer.mozilla.org/ja/docs/Web/HTML/Element/input/number

```html
<input type="number" />
```

PC Chrome 87 の場合だと、こんな感じの UI でレンダリングされます。

![スクリーンショット 2020-12-23 21.17.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38837/74bba5c3-93fc-a7f4-a1ee-d6d188ba57f6.png)

大体のブラウザでは、フォーカスするとクリッカブルな `▲` `▼` アイコンが表示されて、クリックすることでインクリメント・デクリメントできます。

加えて、通常のテキスト入力フォームである `input[type="text"]` とは異なり `input[type="number"]` の場合は、数値として valid な値しか入力を受け付けません。

そのため、数値の入力を期待しているフォームでは `input[type="number"]` を使うと安心です。
なんといっても、入力された value をそのまま `parseInt()` なり `Number()` に食わせれば数値として取り出せるのですから…。

そう考えて `input[type="number"]` を利用しました。

## どんな問い合わせが発生したのか？

さて、ウェブアプリケーションをリリースしてユーザが触れるようになると、**数値の入力ができない** という問い合わせが発生しました。
キーボードを叩いても、数値入力フォームは無反応。テキスト入力フォームは入力できるのに、と……。

問い合わせを受け、プログラムを見直してもフォームに `disabled` な状態は定義していない……。
何より、テキスト入力は可能なのにどうして数値入力だけできないんや…… 🤔

うーん、何かしらのユーザ環境依存だったり、ブラウザ依存？ そうか！ `IE` ！ やはり `IE` お前なのか！(ぐるぐる目)
などなど、色々と調査をしたものの……。

## 原因はなんだったのか？

わかってしまえばとても単純で、ユーザの IME で `全角入力` のまま数字キーを叩いてたことが原因でした。
全角数字は valid な数値として扱われないため、キーを叩いてもフォームは反応しません。

さらに悪いことに、この数値入力フォームでは **空入力を許容していませんでした。**
つまり初期値の `0` を削除できないということですね。

なんでこんな仕様にしていたか言い訳をすると、フォームに対応する数値の型を `number` に絞っていたことが原因です。

```ts
type = {
  なんらかの数値A: number
  なんらかの数値B: number
  ...
}
```

こうすると、`なんらかの数値` は non nullable として扱えるので、何かと楽なのはお分かりいただけます……よね？

……というわけで、開発都合で、`BackSpace` キー押しても無反応だし、(全角入力の状態で)数字キーを押しても無反応という、ユーザにとってとても分かりづらい状況が生まれていました。

## どう対処したのか？

結局の所、数値入力フォームは `input[type="text"]` として自由に入力して貰って、`onBlur` のタイミングでサニタイズ、フォーマットすることにしました。
全角数値や `ー` といった入力も、半角文字列に置き換え valid な数値に変換しています。

合わせて、フォームに対応する型定義も、`number` から `number | null` や、素直に `string` に変更する必要がありますが、
ここは、入力中の文字列は、ローカルスコープの `useState` で保持するようにして、`onBlur` のタイミングでフォームに対応する値を更新することで `number` のまま凌ぎました。

いっそここまでくると、非制御コンポーネントにするという手段もありかもしれませんねー。

また、方針は変えずに半角数値のみを許容するにしても、
全角入力のままキーを叩いたら、tooltip で警告を出すような仕組みでも良いかもしれません。

例えば、OS のログイン画面だと、CapsLock のまま password を入力しようとすると tooltip で警告がでますよね。アレみたいなイメージです。

## ちなみに、HTML Living Standard では……

[HTML Living Standard の 4.10.5.1.12 Number state](https://html.spec.whatwg.org/multipage/input.html#number-state-(type=number)) を見るとこう書かれています。

>A simple way of determining whether to use type=number is to consider whether it would make sense for the input control to have a spinbox interface (e.g. with "up" and "down" arrows). Getting a credit card number wrong by 1 in the last digit isn't a minor mistake, it's as wrong as getting every digit incorrect. So it would not make sense for the user to select a credit card number using "up" and "down" buttons.

数値だからといって盲目的に `input[type="number"]` を採用するのではなくて、
入力フォームにスピンボックス( `▲` `▼` アイコン)が表示されることに意味があるかどうか、を基準に考えると良いと書かれていました。

私のユースケースだと、金額と面積 だったので、例を見ても不適切な使い方でもないと思いました。
今回の問題は、どちらかというと IME 的な話なので、各ブラウザが `inputmode` をサポートできていれば、より適切に解決したかもしれないですね。

## おわりに

話をスムーズにするため、そこはかとなく事実を湾曲しております！🙇

実際には `input[type="number"]` ではなくて、同等の動作をする自作コンポーネントに起因した話だったり、私は直接ユーザからの問い合わせを受ける立場になく、サポートチーム経由の話だったりします。

私は常々 SPA を書くエンジニアとして、使いやすさを大事にしていると思い込んでいたんですが、
いざ蓋をあけてユーザに触ってもらうと、多々反省もありました。

子供の頃読んだ `地獄先生ぬ〜べ〜` の `不幸の手紙` の話をよく覚えているのですが、1 人 1 人のヘイトが小さくとも数十万人のヘイトが集まると巨大な怨念になりますよね。
いつしかこの怨念が生霊となって復習を果たしに来ることがないよう、フィードバックを受け止めてカイゼンを重ねていこうと思います！

では、明日は SmartHR の誇る催眠術師 @ouji-miyahara です！

## 参考文献

- HTML Living Standard NumberState(type=number
  - https://html.spec.whatwg.org/multipage/input.html#number-state-(type=number)
- 地獄先生ぬーべー 4 巻 (ジャンプコミックス)
  - https://www.amazon.co.jp/dp/4088714598