fix(регистрация): телефон — нестираемый префикс «+7 »
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker Public / Build + push Public (test + latest) (push) Waiting to run
Docker Public / Deploy :test → test.food-market.kz (push) Blocked by required conditions
Docker Web / Build + push Web (push) Waiting to run
Docker Web / Deploy Web on stage (push) Blocked by required conditions

Стирание больше не превращает «7» кода страны в цифру номера. Причина: при
Backspace в префикс raw становился «+7», и парсер считал это номером «7»
(«+7 7», далее набор давал мусор «+7 010 000 00 07»). Теперь:
- извлечение тела номера идёт через stripPrefix (проход по совпадающим
  символам префикса) — повреждённый префикс даёт пустое тело, а не номер;
- onKeyDown глушит Backspace на границе префикса и Delete перед префиксом;
  выделение+стирание (Ctrl+A/Ctrl+X) очищает поле до «+7 ».
Полностью стёртое поле = «+7 » и пустой номер. Обе копии (сайт + кабинет).

Проверено настоящим вводом в Chromium (Playwright): Ctrl+A+Backspace,
Backspace до упора, Delete в начале, выделение+набор, Ctrl+A+Ctrl+X —
после каждого набор «7010000000» ⇒ «+7 701 000 00 00»; регрессия набора и
вставки тоже зелёная.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
nns 2026-10-12 22:29:51 +05:00
parent 6510ccafb5
commit 043d5c0417
2 changed files with 58 additions and 10 deletions

View file

@ -25,6 +25,16 @@ function valueToNational(value: string): string {
return toNational(value)
}
// Снимаем наш визуальный префикс «+7 », проходя по совпадающим символам. Если
// пользователь стёр ЧАСТЬ префикса (raw="+7" или "+"), совпавший кусок всё равно
// отбрасывается, а тело оказывается пустым — цифра «7» кода страны НИКОГДА не
// утекает в номер при стирании. Остаток (тело) нормализуется отдельно.
function stripPrefix(raw: string): string {
let i = 0
while (i < PREFIX.length && i < raw.length && raw[i] === PREFIX[i]) i++
return raw.slice(i)
}
function nationalToDisplay(d: string): string {
if (d.length === 0) return PREFIX
let s = PREFIX + d.slice(0, 3)
@ -88,14 +98,27 @@ export function PhoneInput({ value, onChange, className, disabled, ...rest }: Ph
const raw = e.target.value
const cursorPos = e.target.selectionStart ?? raw.length
pendingRef.current = natDigitsBeforeCursor(raw, cursorPos)
// При посимвольном вводе значение начинается с «+7 » — берём часть после
// префикса, чтобы его «7» не попала в номер. При полной вставке (replace)
// значения без префикса нормализуем целиком (снимем код страны/«8»).
const src = raw.startsWith(PREFIX) ? raw.slice(PREFIX_LEN) : raw
const nat = toNational(src)
// Тело = всё после нашего префикса «+7 » (с защитой от частично стёртого
// префикса, см. stripPrefix). Нормализуем тело: вставка полного номера
// (с 8/кодом страны) приводится к 10 цифрам; при наборе первая «7» — это
// начало оператора 7XX и не снимается.
const nat = toNational(stripPrefix(raw))
onChange(nat.length > 0 ? '+7' + nat : '')
}
// Префикс «+7 » нестираемый: Backspace на границе префикса (в номере стирать
// нечего) и Delete внутри/перед префиксом — глушим, чтобы «7» кода страны не
// превратилась в цифру номера. Выделение (Ctrl+A) + стирание не трогаем —
// браузер очистит всё, а value вернётся к «+7 ».
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
const el = e.currentTarget
const start = el.selectionStart ?? 0
const end = el.selectionEnd ?? 0
if (start !== end) return // есть выделение — пусть удаляет, нормализуется в handleChange
if (e.key === 'Backspace' && start <= PREFIX_LEN) { e.preventDefault(); return }
if (e.key === 'Delete' && start < PREFIX_LEN) { e.preventDefault() }
}
const guardCursor = (el: HTMLInputElement) => {
const start = el.selectionStart ?? 0
const end = el.selectionEnd ?? 0
@ -118,6 +141,7 @@ export function PhoneInput({ value, onChange, className, disabled, ...rest }: Ph
value={display}
onBeforeInput={handleBeforeInput}
onChange={handleChange}
onKeyDown={handleKeyDown}
onSelect={handleSelect}
onFocus={handleFocus}
disabled={disabled}

View file

@ -29,6 +29,16 @@ function valueToNational(value: string): string {
return toNational(value)
}
// Снимаем наш визуальный префикс «+7 », проходя по совпадающим символам. Если
// пользователь стёр ЧАСТЬ префикса (raw="+7" или "+"), совпавший кусок всё равно
// отбрасывается, тело пустеет — цифра «7» кода страны НИКОГДА не утекает в номер
// при стирании. Остаток (тело) нормализуется отдельно.
function stripPrefix(raw: string): string {
let i = 0
while (i < PREFIX.length && i < raw.length && raw[i] === PREFIX[i]) i++
return raw.slice(i)
}
// Display из нац. номера. Формат: «+7 7XX XXX XX XX» — после «+7 » идут
// 3+3+2+2 = 10 цифр через пробелы.
function nationalToDisplay(d: string): string {
@ -118,14 +128,27 @@ export function PhoneInput({ value, onChange, className, disabled, ...rest }: Ph
const raw = e.target.value
const cursorPos = e.target.selectionStart ?? raw.length
pendingDigitCountRef.current = natDigitsBeforeCursor(raw, cursorPos)
// При посимвольном вводе значение начинается с «+7 » — берём часть после
// префикса, чтобы его «7» не попала в номер. При полной вставке значения
// без префикса нормализуем целиком (снимем код страны/«8»).
const src = raw.startsWith(PREFIX) ? raw.slice(PREFIX_LEN) : raw
const nat = toNational(src)
// Тело = всё после нашего префикса «+7 » (с защитой от частично стёртого
// префикса, см. stripPrefix). Нормализуем тело: вставка полного номера
// (с 8/кодом страны) приводится к 10 цифрам; при наборе первая «7» — это
// начало оператора 7XX и не снимается.
const nat = toNational(stripPrefix(raw))
onChange(nat.length > 0 ? '+7' + nat : '')
}
// Префикс «+7 » нестираемый: Backspace на границе префикса (в номере стирать
// нечего) и Delete внутри/перед префиксом — глушим, чтобы «7» кода страны не
// превратилась в цифру номера. Выделение + стирание не трогаем — браузер
// очистит всё, value вернётся к «+7 ».
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
const el = e.currentTarget
const start = el.selectionStart ?? 0
const end = el.selectionEnd ?? 0
if (start !== end) return
if (e.key === 'Backspace' && start <= PREFIX_LEN) { e.preventDefault(); return }
if (e.key === 'Delete' && start < PREFIX_LEN) { e.preventDefault() }
}
// Не пускаем курсор/selection в префикс. onSelect ловит любые попытки
// (клик, стрелки, Cmd+A) и мягко двигает на digit-зону.
const guardCursor = (el: HTMLInputElement) => {
@ -152,6 +175,7 @@ export function PhoneInput({ value, onChange, className, disabled, ...rest }: Ph
value={display}
onBeforeInput={handleBeforeInput}
onChange={handleChange}
onKeyDown={handleKeyDown}
onSelect={handleSelect}
onFocus={handleFocus}
disabled={disabled}