Автоматическое приведение ввода к телефонному формату

Храните только десять национальных цифр, форматируйте их для показа и восстанавливайте курсор по числу цифр перед ним. Удаление разделителя должно удалять соседнюю цифру.

Короткий ответ

Что ответить на собеседовании

Храните только десять национальных цифр, форматируйте их для показа и восстанавливайте курсор по числу цифр перед ним. Удаление разделителя должно удалять соседнюю цифру.

Подробный разбор

Ответ с пояснениями

Условие

Создайте React‑компонент PhoneNumberInput, который будет динамически преобразовывать вводимые символы в поле ввода в привычный вид телефонного номера, например +7 (___) ___--. При наборе пользователь видит уже отформатированный номер, без необходимости вручную вставлять скобки, пробелы или дефисы.

import "./styles.css";
import { PhoneNumberInput } from "./Input";

export default function App() {
  return (
    <div className="App">
      <div>Input</div>
      <PhoneNumberInput />
      <br />
      <br />
      <div>Your Input</div>
    </div>
  );
}

Задача подразумевает обработку событий ввода, работу с курсором и поддержание корректного формата в реальном времени.

Ответ

Храните только десять национальных цифр, форматируйте их для показа и восстанавливайте курсор по числу цифр перед ним. Удаление разделителя должно удалять соседнюю цифру.

Для маски +7 (XXX) XXX-XX-XX минимальная реализация ниже поддерживает вставку, выделение и Backspace/Delete:

function PhoneNumberInput() {
  const [digits, setDigits] = React.useState('');
  const input = React.useRef(null);
  const caret = React.useRef(null);
  const format = value => '+7 (' + value.slice(0,3) + (value.length >= 3 ? ') ' : '') + value.slice(3,6) + (value.length > 6 ? '-' + value.slice(6,8) : '') + (value.length > 8 ? '-' + value.slice(8,10) : '');
  const [revision, refresh] = React.useState(0);
  function commit(value, index) {
    setDigits(value); caret.current = index; refresh(n => n + 1);
  }
  React.useLayoutEffect(() => {
    if (caret.current === null) return;
    const text = format(digits);
    let pos = 4, seen = 0;
    while (pos < text.length && seen < caret.current) {
      if (/[0-9]/.test(text[pos])) seen++;
      pos++;
    }
    input.current.setSelectionRange(pos, pos);
    caret.current = null;
  }, [digits, revision]);
  function ordinal(text, position) { return Math.max(0, (text.slice(0, position).match(/[0-9]/g) || []).length - 1); }
  function change(event) {
    const node = event.target;
    const raw = node.value.replace(/[^0-9]/g, '');
    const prefix = /^[78]/.test(raw) && (node.value.startsWith('+') || raw.length > 10) ? 1 : 0;
    const count = (node.value.slice(0, node.selectionStart).match(/[0-9]/g) || []).length;
    commit(raw.slice(prefix, prefix + 10), Math.max(0, count - prefix));
  }
  function keyDown(event) {
    if (event.key !== 'Backspace' && event.key !== 'Delete') return;
    event.preventDefault();
    const node = event.currentTarget;
    let from = ordinal(node.value, node.selectionStart);
    let to = ordinal(node.value, node.selectionEnd);
    if (from === to) {
      if (event.key === 'Backspace') from = Math.max(0, from - 1);
      else to = Math.min(digits.length, to + 1);
    }
    commit(digits.slice(0, from) + digits.slice(to), from);
  }
  return <label>Телефон <input ref={input} type='tel' inputMode='tel' value={format(digits)} onChange={change} onKeyDown={keyDown} /></label>;
}

Это учебная маска фиксированного кода страны, не международная валидация телефона. Для production отдельно проверяют мобильное редактирование, autofill, IME и доступность; сервер проверяет итоговые данные независимо.

ИИ-помощник для собеседований

Хочешь уверенно проходить собеседования?

Попробуй ИИ-помощник для собеседований: слышит вас и собеседника, анализирует экран, подсказывает ответы в реальном времени, работает без VPN и не попадает в захват экрана.

Подробнее