🐥
React Hook FormをしっかりSafari on iOS / Firefox対応する
React Hook FormsのバリデーションがSafari on iOS / Firefox上で意図通り動かなかったので対策しました。
やりたいこと:バリデーションエラー検出したら、エラー要因のinput要素を表示したい
バリデーションエラーの検出時に下記を行います。
- エラー要因のinput要素にfocusを当てる(画面外のときはスクロールして表示)
- 上記input要素近くにエラーメッセージ表示
React Hook Formsを使えば簡単に実現できる、ありがちな作りだと思います。
- うまく動いている例
ご興味ある方はこちらから操作してみてください。
問題:Chromeは OK だが、Safari on iOS や Firefox は NG
Safari on iOS かつ radio/checkboxでエラー発生 のときは、画面スクロールが動きませんでした。
Input要素が画面外のときにエラーメッセージが見えず、「送信ボタン押してるのに効かない!理由もわからない!」と困ってしまいます。
再現環境を作りましたのでご興味ある方はお試しください。
Firefoxはスクロールしますが不十分でした。input要素の下部がかすかに見える程度でした。
2021/11/24現在、最新の環境で検証しました
- react-hook-form: ^7.20.2
- Safari(iPhone): Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1
原因:HTMLInputElement.focus()の挙動がブラウザごとに異なる
react-hook-formのなかでエラー発生要因のInput要素のfocus()を呼んで実現しています。ただし、Safari(iPhone/iPad)のradio/checkboxはfocus()を呼んでも画面スクロールしないようです・・・。
(フォームが長すぎるから1画面に入るよう短くするのがいいかもね・・・)
対策: onFocusでscrollIntoView()を実行して画面スクロール
inputのラッパーコンポーネント <Input /> を作成し、対象要素にfocusイベントが発生したときにscrollIntoView()を実行して画面スクロールさせました。
inputのラッパーコンポーネント <Input /> を作成
Input.tsx
type InputProps = React.DetailedHTMLProps<
React.InputHTMLAttributes<HTMLInputElement>,
HTMLInputElement
>;
const Input = React.forwardRef<HTMLInputElement, InputProps>(
(props, ref) => {
const onFocus: FocusEventHandler<HTMLInputElement> = (event) => {
// バリデーションエラー発生時にradio/checkboxのinputにしっかりスクロールしない問題のワークアラウンド
// 【問題詳細】
// Safari on iOS: 全くスクロールしない, FireFox,IE:スクロールが不十分でエラーメッセージが見えない,
if (props.type !== "radio" && props.type !== "checkbox") {
return;
}
try {
const element = event.currentTarget;
// radio/checkboxをタップしたときはスクロールしない
if (isInviewAll(element)) return;
// バリデーションエラー発生時のみスクロールする
// 注意:本対応はIE対応できていない(scrollIntoViewはIE非対応)
element.scrollIntoView({
block: "center",
behavior: "auto",
});
} catch (e) {
console.error(e);
}
};
return <input ref={ref} onFocus={onFocus} {...props} />;
}
);
const isInviewAll = (element: HTMLInputElement) => {
const rect = element.getBoundingClientRect();
// 1 < rect.top は firefox対応
return 1 < rect.top && rect.bottom < window.innerHeight;
};
Input.displayName = "Input";
export default Input;
react-hook-form の公式マニュアルを参考にしてラッパーコンポーネントを作りました
利用例:作成した <Input /> は <input /> と同じように使える
sample.tsx
const {
register,
formState: { errors },
handleSubmit,
} = useForm();
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<p>Q1. 好きなプログラミング言語はなんですか?</p>
{errors?.language && (
<p className="text-red-500 py-2 px-2">
<span>選択してください</span>
</p>
)}
<Input
type="radio"
value="TypeScript"
{...register("language", { required: true })}
/>
<label>TypeScript</label>
<button
type="submit"
className="text-white bg-blue-600 py-2 px-4 rounded-md"
>
送信
</button>
</div>
</form>
);
おわりに
今回作成したソースコードです。
もっといい方法がありそうなので、コメントやMRを頂けたら嬉しいです!
Discussion