RemixとConformで検証エラー時のスクロールをsmoothにする
はじめに
Remixで使えるバリデーションライブラリにConformというものがあります。
ConformはZodのスキーマをもとにバックエンドでのバリデーションの結果をフロントエンドへいい感じに連携してくれるので、バックとフロントのバリデーションを分けて考える必要がなくとても便利です。
今回使ったバージョン
- @remix-run/cloudflare: 2.8.0
- @remix-run/react: 2.8.0
- @conform-to/react: 1.0.2
- @conform-to/zod: 1.0.2
検証エラー時の挙動
実装にもよりますが、Conformではフォームを送信しようとしたタイミングで検証が実行され、エラーがあった場合には送信を中断してその入力要素までスクロールしてくれます。
しかし、このスクロールが一瞬で移動してしまうため少し味気なかったりします。
なぜそうなってしまうのか
まずはドキュメントや誰かが書いているかもしれない記事をさがしましたがどこにも見つけられず、どうやらConformの設定でどうこうできるものではないようです。
そんな時はソースコードを読みます。
辿っていくと依存関係でインストールされる以下のファイルで Element.focus()
が呼び出されていることがわかります。
対処法
Element.focus()
で要素までスクロールしていることが分かったのでドキュメント要素に scroll-behavior: smooth;
を指定することでエラー要素までのスクロールをアニメーションさせることができます。
問題点
が、今回Remixと一緒に使っていることで別の問題が発生します。
Remixで画面遷移をする際、下の方から画面TOPまでスクロールしているようで、ここにアニメーションがついてしまうのです。
JavaScriptでスクロール位置を指定する scrollTo
というメソッドにも behavior
を指定するオプションがあり、値として明示的にアニメーションさせない instant
があります。
下のディスカッションではそのオプションを指定できるようにしてはという提案がされています。
対処法の問題点の対処法
とはいえ、現時点でそれを制御する方法は存在しないので自分で考えるしかありません。
幸い今回は検証エラー時のスクロールをアニメーションしたいだけなので、この画面に入ってきたら smooth
を設定し、出る直前に解除すればなんとかなりそうです。
画面から移動するタイミングの判定には useNavigation
を使います。
navigation.location
には画面遷移する際、次のルート情報がセットされます。
navigation.location
This tells you what the next location is going to be.
import { useNavigation } from "@remix-run/react";
import { useEffect } from "react";
...
const navigation = useNavigation();
useEffect(() => {
if (navigation.location) {
document.documentElement.style.scrollBehavior = "";
} else {
document.documentElement.style.scrollBehavior = "smooth";
}
}, [navigation.location]);
...
これで、画面にいるうちはsmoothスクロールになり、画面を出る直前に解除されるためRemixのスクロールはinstantになります。
まとめ
困ったら読む
Discussion