hbsnow.dev

文字数によって区切りのあるテキストフォームを作る

GitHub - nosir/cleave.js: Format input text content when you are typing...
Format input text content when you are typing... Contribute to nosir/cleave.js development by creating an account on GitHub.
GitHub - nosir/cleave.js: Format input text content when you are typing... favicon https://github.com/nosir/cleave.js/
GitHub - nosir/cleave.js: Format input text content when you are typing...

4つごとに空白のあるテキスト入力フォーム

空白のある箇所ごとに <input type="text"> をわけるような実装でもよさそうではありそうですが、デザイン上1つのフォームで実装する必要がでてくることもあります。

UXが向上しそうなのですが日本語圏ではかな入力時になにも入力されない状態が発生しまうので、入力可能な文字列次第では微妙になる場合もありそうな気はしています。

Cleave.js を使う

上記のライブラリを使えば特に問題なく実装できそうです。

yarn add -E cleave.js
yarn add -DE @types/cleave.js
<Cleave options={{ blocks: [4, 4, 4] }} />

CleaveOptions での設定が豊富なのでほとんどのケースはこれで対応できるはずです。

自分で頑張る

自分で頑張る場合は少し面倒です。

import React, { FC, useState, useCallback, useRef } from "react";

const InputText: FC = () => {
  const inputRef = useRef<HTMLInputElement>(null);
  const [value, setValue] = useState("");

  const handleChange = useCallback(() => {
    const target = inputRef.current;
    if (target && target.value.length >= 0) {
      const chunkedValue =
        target.value.match(/[\da-zA-Z]{1,4}/g)?.join(" ") ?? "";
      setValue(chunkedValue);
    }
  }, []);

  return (
    <input
      ref={inputRef}
      value={value}
      onChange={handleChange}
      maxLength={4 * 3 + 3}
    />
  );
};

export default InputText;

上記のコードは一見うまくいきそうに思えますが、これはカーソル位置を移動してbackspaceをしたときに正しく機能しません。

カーソル移動後のbackspaceで正しく動作しないテキスト入力フォームの動作

  1. 4つ区切りになっていない
  2. カーソルが空白の箇所で最後尾にとんでしまう

この2点が問題になります。

import React, { FC, useState, useCallback, useRef, useEffect } from "react";

const InputText: FC = () => {
  const inputRef = useRef<HTMLInputElement>(null);
  const [value, setValue] = useState("");
  const [prevPosition, setPrevPosition] =
    useState<HTMLInputElement["selectionEnd"]>(null);

  const handleChange = useCallback(() => {
    const target = inputRef.current;
    if (target === null) return;

    const chunkedValue = target.value
      .replace(/[^\da-zA-Z]/g, "")
      .replace(/(.{4})/g, "$1 ")
      .trim();

    setPrevPosition(target.selectionEnd);
    setValue(chunkedValue);
  }, []);

  useEffect(() => {
    const target = inputRef.current;
    if (target === null || prevPosition === null) return;

    const currentPosition = target.selectionEnd;
    if (currentPosition === null || prevPosition === null) return;

    // 半角空白が追加されたときのカーソルを補正する
    const nextPositionDiff =
      prevPosition + 1 === currentPosition &&
      value.charAt(prevPosition - 1) === " "
        ? 1
        : 0;

    target.selectionEnd = prevPosition + nextPositionDiff;
  }, [prevPosition, value]);

  return (
    <input
      ref={inputRef}
      value={value}
      onChange={handleChange}
      maxLength={4 * 3 + 3}
    />
  );
};

export default InputText;

この方法では chunkedValue の文字列が変化するため、削除時にカーソルが最後尾に飛ばされます。そのため useEffectselectionEnd の補正をしています。

とりあえず作ってはみたのですが、ちょっと自信がありません。素直にプラグインを使ったほうが良さそうです。