Open12
React Form Hook の iPhone対応
背景
- React Hook Formにてバリデーションエラーを検出すると、エラー元のinput要素にfocusが当たる。input要素が見えていないときは画面スクロールされる。よくあるやり方として、inputの近くにバリデーションメッセージを表示さると、ユーザーは再入力しやすくてUXがいい感じになる。
課題
-
iPhoneのRadioButton/CheckBoxはfocusが効かないようで、期待を裏切られた
参考:https://stackoverflow.com/questions/56137175/ios-safari-chrome-wont-scroll-up-to-show-validation-error-message-on-radio-inpu -
ユーザーから「何回押してもボタン効かないんだけど」と問い合わせが来た。フォーカスが当たらないのはいいけど、画面スクロールされないのはよくない。バリデーションメッセージが見えなかった。。。
対処
- inputのラッパーコンポーネントとして<Input>コンポーネントを作成し、対象要素にfocusイベントが発生したとき かつ iPhoneの場合は、scrollIntoViewを実行して画面スクロールさせるようにして対処した
対処について詳しく書きたい
こういうのを作った
const Input = React.forwardRef<HTMLInputElement, InputProps>((props, ref) => {
const internalRef = React.useRef<HTMLInputElement>();
useEffect(() => {
// iPhoneでradio/checkboxのinputにfocusが当たらない問題のワークアラウンド
if (props.type !== "radio" && props.type !== "checkbox") {
return;
}
if (!/iPad|iPhone/.test(navigator.userAgent)) {
return;
}
internalRef.current?.addEventListener("focus", (e) => {
(e.target as HTMLInputElement).scrollIntoView({
block: "center",
behavior: "smooth",
});
});
}, []);
return <input ref={useMergeRefs(internalRef, ref)} {...props} />;
});
こういう感じで使う
type FormValues = {
likeLanguage: string;
};
const Home: NextPage = () => {
const onSubmit = (data: FormValues) => console.log(data);
const {
register,
formState: { errors },
handleSubmit,
} = useForm();
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label>
<Input
type="radio"
value="C"
{...register("likeLanguage", { required: true })}
/>
C
</label>
<label>
<Input
type="radio"
value="TypeScript"
{...register("likeLanguage", { required: true })}
/>
TypeScript
</label>
<label>
<Input
type="radio"
value="PHP"
{...register("likeLanguage", { required: true })}
/>
PHP
</label>
{errors?.likeLanguage && <p>好きな言語が選択されていません</p>}
<Input type="submit" />
</form>
);
};
useMergeRefsはChakraUIから拝借した
【参考】
/* eslint-disable react-hooks/exhaustive-deps */
import * as React from "react";
type ReactRef<T> = React.Ref<T> | React.MutableRefObject<T>;
export function assignRef<T = any>(ref: ReactRef<T> | undefined, value: T) {
if (ref == null) return;
if (typeof ref === "function") {
ref(value);
return;
}
try {
// @ts-ignore
ref.current = value;
} catch (error) {
throw new Error(`Cannot assign value '${value}' to ref '${ref}'`);
}
}
/**
* React hook that merges react refs into a single memoized function
*
* @example
* import React from "react";
* import { useMergeRefs } from `@chakra-ui/hooks`;
*
* const Component = React.forwardRef((props, ref) => {
* const internalRef = React.useRef();
* return <div {...props} ref={useMergeRefs(internalRef, ref)} />;
* });
*/
export function useMergeRefs<T>(...refs: (ReactRef<T> | undefined)[]) {
return React.useMemo(() => {
if (refs.every((ref) => ref == null)) {
return null;
}
return (node: T) => {
refs.forEach((ref) => {
if (ref) assignRef(ref, node);
});
};
}, refs);
}
Eventらへんの型を綺麗にしたいかも
サンプルアプリ作っているときにこれやった
サンプルアプリのevent.targetを書き換えたい
ボタンクリックをわかりやすくするアニメーションをつけたい
記事にまとめたからクローズする
onFocusを使えばめっちゃ簡単に実装できそう
公式GitHubでも議論されている。MR送るのもありかもしれない