はじめまして!2023年7月からミラティブでフロントエンドインターンをしております、かずえもんと申します😺
今回は、インターンでの作業中にハマってしまった Safari のバグについて調査していたら、なんと10年モノの issue だったことが判明し、ライブラリを導入する意義について考える機会となった話を書いてみたいと思います。
- Safari の日付入力欄に無効な日付を入れると起こるバグ
- onChange が正しく呼ばれないのは Safari が原因?
- Safari 17 以降で再現しないので原因は Safari で間違いなさそう
- Safari 17 以前での解決方法として DatePicker を使ってみる
- まとめ: ネイティブを補う存在としてのライブラリ
- We're Hiring!!
Safari の日付入力欄に無効な日付を入れると起こるバグ
2023年8月、私はミラティブの管理システムの入力フォームを「useRHF」というカスタムフックを用いた実装に置き換える作業を進めていました。
useRHF とは、React において React Hook Form + Zod をいい感じに使えるようにしてくれる内製のカスタムフックで、以下の記事にて紹介されているものです。
順調に作業が進み、Chrome と Safari で動作確認を行っていたところ、誕生日の入力フォームで Safari がおかしな挙動を示すことに気がつきました。
後述しますが、Safari 17 (macOS Sonoma) 以降ではこの問題は再現しません。
実際の画面はお見せできないため、誕生日の入力フォームのデモ版を作成しました。日付の入力欄は実際のフォームと同じ input type="date"
を使って実装しています*1。別のライブラリを入れなくてもカレンダーによる日付選択ができて非常に便利ですね。
このフォームに日付を入力して「Submit」ボタンを押すと、フォームの送信時に発火する関数 onSubmit
( RHF の handleSubmit
経由 ) が受け取った値がアラートに表示されます。
バグを再現するには、このフォームの日付入力欄に、カレンダーから選択できない無効な日付「2023/02/29」を手入力します。そして入力後に Submit すると、Chrome 120 では以下のように空文字(""
)が onSubmit
の受け取った値であると表示されます。受け取った値(""
)と入力欄の見た目の値(2023/02/29
)は一致していませんが、受け取った値が空文字であることから無効な値が入力されていると判別できるため、想定通りの挙動です。
一方 Safari 16.6 では、受け取った値は 2023-02-02
となっています。見た目の値は 2023/02/29
なので、ユーザーが入力したと思っている日付と、RHF 経由で取得した日付が一致していないことになります。表示された日付から推測すると、無効な日付を入力した時に onSubmit
が受け取る値が「無効な日付となる一個前に入力していた日付」になってしまっていると思われます。
この場合、受け取った値は空文字ではなく有効な日付であるため、Zod などのバリデーションライブラリを用いても無効な値が入力されていると判別できません。そうなると、最終的にサーバーにユーザーの意図と異なる日付が送信されてしまう可能性があります。
例えば、誤った誕生日が登録された場合、サービスの挙動がユーザーにとって意図しないものになる可能性がありますし、他にも設定した日時を過ぎたら表示を切り替えるような機能の設定画面でこの挙動が発生した場合、予期せぬタイミングで情報が公開されてしまうなどの可能性もあります。そのため、早速原因の調査に取り掛かりました。
onChange が正しく呼ばれないのは Safari が原因?
どの段階で不具合が発生しているかを確かめるため、まずは onChange
がどのタイミングで呼び出されるかを確かめました。
動作確認は、以下4パターンに対して、非制御コンポーネント(Uncontrolled)の場合と制御コンポーネント(Controlled)の場合の計8パターンのサンプルを CodeSandbox で作成し、1990/02/29
と手入力した場合に onChange
が "1990-02-02"
→ ""
の順で呼ばれていれば ✅ とし、そうでない場合は 🚨 と記載する形式で行いました。
- MUIなし、RHFなし (ブラウザ標準)
- MUIなし、RHFあり
- MUIあり、RHFなし
- MUIあり、RHFあり
実際に作成したデモがこちらです。
動作確認した結果、以下のような挙動となりました。
確認箇所 | Chrome 120 | Safari 16.6 |
---|---|---|
Uncontrolled | ✅ | ✅ |
Controlled | ✅ | ✅ |
RHFUncontrolled | ✅ | ✅ |
RHFControlled | ✅ | 🚨 "1990-02-02" しかトリガーされない |
MUIUncontrolled | ✅ | ✅ |
MUIControlled | ✅ | ✅ |
MUIRHFUncontrolled | ✅ | ✅ |
MUIRHFControlled | ✅ | 🚨 "1990-02-02" しかトリガーされない |
検証結果から、Safari 16.6 で制御コンポーネントに対して RHF を使用した場合、onChange
が正しくトリガーされなくなることがわかりました。Chrome 120 では正常に動作したことから、Safari の内部実装に問題がある可能性が浮上しましたが、まだ確証は得られていません。
Safari 17 以降で再現しないので原因は Safari で間違いなさそう
ところで、2023年12月時点での最新バージョンである Safari 17 以降ではこの問題は再現しません。
Safari 17 や Safari Technology Preview のリリースノートから推測してみたところ、おそらく2023年6月14日にリリースされた Safari Technology Preview 172(Release Notes)の「Fixed the change event to fire when the user reverts the value of a color, date, time, or datetime input after JavaScript changed the value」によってこの問題は修正されたものと思われ、この修正差分は Safari 17 から含まれています。
タイトルを見る限り「JavaScript で値を差し戻した後( = 今回の場合は空文字 ""
にリセットした後? ) に onchange
が呼ばれなくなる」という不具合のようです。制御コンポーネントを使用している場合は入力の反映に JavaScript を介していますので、まさに今回の不具合の原因と推測できます。したがって、今回の不具合は MUI や RHF の問題というよりも Safari 17 以前の input type="date" の内部実装の問題である可能性が濃厚であると言えそうです。
- Bugzilla: 121590 – Change event isn't firing when the user reverts the value of a color/date/time input after JS changed the value
- Pull Request: Change event isn't firing when the user reverts the value of color/date/time/datetime input after JS changed the value by Ahmad-S792 · Pull Request #14212 · WebKit/WebKit · GitHub
Bugzilla を探ってみたところ、なんと2013年9月18日に起票され、2023年5月25日に解決された、10年モノの issue でした。さらに遡ってみると、当該 Pull Request に含まれるソースコードは2013年9月2日に Chromium で同様の問題を修正したときのソースコードの一部のようで、これらを発見した時は正直びっくりしましたね。
なお、起票された2013年時点の Safari ではまだ date
も color
も実装されていないはず*2なので、起票自体は同じバグがあったらマージしてね、という予備的なものだと思われます。
Safari 17 以前での解決方法として DatePicker を使ってみる
調査を通して、今回のバグは Safari 17 以前の input type="date" の内部実装が原因である可能性が高いことがわかりましたが、Safari 17 以前のユーザーでこのバグを解消するためにはどうしたらよいでしょうか。
そこで今回は、DatePicker のライブラリを使うとどうか試してみました。
例として MUI 純正である @mui/x-date-pickers
を使ったデモを作りました。入力値は Day.js のオブジェクトで返ってくる*3ため、アラートでは Day.js の format
関数を使って YYYY-MM-DD
形式にパースして出力しています。
実際に Chrome 120 と Safari 16.6 で試したところ、無効な日付を入力した場合はどちらも Day.js のオブジェクトの日付データが NaN
、format
関数の出力は Invalid Date
となり、先ほどの1つ前の値が入力値となる不具合は発生しませんでした。これでユーザーが意図しない日付になってしまうことは防げそうです。
ところで、デモには Zod などのバリデーションをかけていませんが、入力欄のふちが赤くなっていて、無効な日付であることが示されています。Day.js には日付のバリデーション機能がない*4ため、この挙動は DatePicker 内蔵のバリデーション機能によるものであると考えられます。自動でバリデーションされるのは便利ですね。
また @mui/x-date-pickers
の内部実装では、input の type には text
が指定されており、date
は利用されていません。おそらくこういったブラウザ固有のバグを回避したり、ライブラリとの連携を容易にしたりするために使用していないものと思われます。
まとめ: ネイティブを補う存在としてのライブラリ
Google Chrome や Safari、Edge、Firefox などの主要ブラウザでは、ほとんどの機能が Web 標準に従って互換性を保っていますが、今回のようなブラウザのネイティブ実装のバグによって想定外の挙動が発生する場合があることが分かりました。
そこで、ネイティブを補うものとして、ブラウザ固有の動作の差異やバグを回避しながら、ユーザーに同じUXを提供する手段として様々なライブラリが開発されており、エンジニアもより良い開発体験を享受できることを実感しました。
こう考えると、ライブラリって偉大すぎますね。
とはいえ、何も考えずにどんどんライブラリを入れていくのではなく、まずネイティブの機能で実現できないか、バンドルされた後のサイズに大きく影響しないか、などの点を精査することも必要ですし、もっとも「ライブラリを使うことがユーザーにとって良い選択肢であるか」という点も重要です。今回のケースのように、ネイティブにバグがある場合には、ライブラリを使うことはユーザーにとっても良い選択肢であると言えると思います。
ユーザーに良い価値を提供できるように、ネイティブとライブラリをうまく組み合わせて開発を進めていくことが大事だな、と改めて気づくきっかけとなりました。
We're Hiring!!
最後まで読んでくださり、ありがとうございました!
ミラティブでは引き続きインターンを募集しています。ぜひチェックしてみてください!
👇インターン情報はこちら
👇採用情報はこちら
*1:Safari では2021年4月リリースの Safari 14.1 から date に対応しており、現在はすべてのモダンブラウザで利用可能になっています。
*2:https://caniuse.com/?search=input%20type を参照
*3:date-fns など、他の日付ライブラリにも変更可能です