【React】数字フィールドでIME日本語入力を攻略する
こんにちは、フリーランスエンジニアの太田雅昭です。
数字フィールドでのIME
通常、数字入力には下記を使用します。
<input type="number"...
しかしIMEがオンになってると意図しない挙動になったりします。
onChangeのみでのカーソル問題
type=”text”にしてonChangeで強制的に変換するのもいいのですが、今度はカーソル問題が出てきます。入力するたびにカーソルが後ろに行ってしまいます。こういうことが何度かあり、その度に挑戦しては諦めて代替案を採用していました。
しかし今回、克服できたので共有させていただきます。
selectionStartとrequestAnimationFrameを使用する
selectionStartでカーソル位置を取得できます。ここから計算して、編集後にカーソルを移動させます。ただし普通に行った場合はReactの更新プロセスが後に来てしまい、意味がありません。
そこでrequestAnimationFrameを使用します。requestAnimationFrameは、次のフレーム更新直前に関数を実行してくれるため、Reactの後に割り込むことが可能となります。なおこうした使用方法はトリッキーです。ChatGPTが出した回答に入っていたのですが、通常の使用方法ではありません。裏技的な手法となります。
下記のコードでは、IMEオンの時でも常に半角数字が入力されます。react-number-formatライブラリよりも良いです。
import { useCallback } from 'react';
export interface NumberTextFieldProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'type'> {
onChange?: (value: string) => void
}
export const NumberTextField = (props: NumberTextFieldProps) => {
const { onChange, ...restProps } = props;
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const input = e.target
const originalValue = e.target.value
// 半角数字のみに変換
const numbersOnly = toNumbersOnly(originalValue)
// カーソル位置計算
const beforeCursorLength = (() => {
const cursorPosition = input.selectionStart || 0
// カーソル位置より前の文字列
const beforeCursor = originalValue.slice(0, cursorPosition)
// カーソル位置より前の部分を変換した場合の長さを計算
const convertedBeforeCursor = toNumbersOnly(beforeCursor);
return convertedBeforeCursor.length
})();
// 親コンポーネントへの通知
onChange?.(numbersOnly)
// カーソル位置を補正して復元。
// requestAnimationFrameで、Reactのレンダリング後にタイミングをずらす
requestAnimationFrame(() => {
const newPosition = Math.min(
beforeCursorLength,
numbersOnly.length
)
input.setSelectionRange(newPosition, newPosition)
})
}, [onChange])
return (
<input
{...restProps}
type="text"
onChange={handleChange}
/>
)
}
// 半角数字のみに変換
function toNumbersOnly(value: string | undefined) {
const converted = value?.replace(/[0-9]/g, (s) => {
return String.fromCharCode(s.charCodeAt(0) - 0xFEE0)
})
return converted?.replace(/[^0-9]/g, '') ?? ''
}
下記のように使用します。
import { Box } from '@mui/material'
import { StrictMode, useState } from 'react'
import { createRoot } from 'react-dom/client'
import { NumberTextField } from './components/NumberTextField'
const App = () => {
const [value, setValue] = useState('')
return (
<Box>
<NumberTextField
value={value}
onChange={(value) => setValue(value)}
/>
</Box>
)
}
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)