From 87a4666c2507cfa8b6f7ca3d0ddedaa489e25449 Mon Sep 17 00:00:00 2001 From: nns Date: Tue, 13 Oct 2026 17:37:30 +0500 Subject: [PATCH] =?UTF-8?q?fix(=D1=80=D0=B5=D0=B3=D0=B8=D1=81=D1=82=D1=80?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8F/=D0=BC=D0=BE=D0=B1):=20=D1=81=D0=B2?= =?UTF-8?q?=D0=B0=D0=B9=D0=BF=20=D1=81=20=D0=BA=D0=BD=D0=BE=D0=BF=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=BF=D1=80=D0=BE=D0=BA=D1=80=D1=83=D1=87=D0=B8=D0=B2?= =?UTF-8?q?=D0=B0=D0=B5=D1=82,=20=D0=B0=20=D0=BD=D0=B5=20=D0=BE=D1=82?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D1=8F=D0=B5=D1=82=20=D1=84=D0=BE?= =?UTF-8?q?=D1=80=D0=BC=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Регрессия к 3322da0: submit запускался прямо на pointerdown, поэтому прокрутка, начатая пальцем на широкой кнопке «Начать бесплатно»/«Войти», отправляла форму. Теперь на pointerdown только preventDefault (держим фокус в поле — blur-ошибка не сдвигает вёрстку) и запоминаем точку касания. Отправляем на pointerup, только если палец не сдвинулся больше порога (10px); pointercancel (жест стал прокруткой) сбрасывает — submit не происходит. Тап отправляет с первого раза, свайп с кнопки просто прокручивает страницу. onSubmit формы сохранён для Enter, submittingRef защищает от двойного запроса. Применено к форме регистрации и форме входа кабинета. Проверено настоящими touch-жестами в Chromium (mobile 360×676, CDP dispatchTouchEvent): тап → submit; свайп вверх 250px с кнопки → страница прокручивается, submit НЕ срабатывает; долгое нажатие без движения → submit; валидная форма — ровно один запрос. Co-Authored-By: Claude Opus 4.8 --- .../src/components/SignupForm.tsx | 33 ++++++++++++++----- src/food-market.web/src/pages/LoginPage.tsx | 28 +++++++++++++--- 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/src/food-market.public/src/components/SignupForm.tsx b/src/food-market.public/src/components/SignupForm.tsx index e186bb1..7356ef1 100644 --- a/src/food-market.public/src/components/SignupForm.tsx +++ b/src/food-market.public/src/components/SignupForm.tsx @@ -162,17 +162,31 @@ export default function SignupForm({ defaultPlan = 'start' }: Props) { // onSubmit формы — срабатывает на Enter с клавиатуры. const submit = (ev: React.FormEvent) => { ev.preventDefault(); runSubmit() } - // Касание кнопки «Начать бесплатно»: отправляем на pointerdown, ДО того как - // смена фокуса (blur поля) успеет перерисовать форму и сдвинуть кнопку. - // preventDefault не даёт фокусу уйти с поля (значит blur-валидация не - // вставит ошибку и вёрстка не прыгнет) и подавляет последующий click, так что - // первое касание всегда отправляет форму — валидация всех полей + прокрутка и - // фокус к первой ошибке. Фикс потери первого касания на мобильном. - const onSubmitBtnPointerDown = (e: React.PointerEvent) => { + // Касание кнопки «Начать бесплатно». На pointerdown только preventDefault — + // фокус остаётся в поле, blur-ошибка не вставляется и вёрстка не прыгает + // (иначе кнопка уезжала и первое касание терялось). Отправляем на pointerup, + // только если палец НЕ сдвинулся больше порога и жест не стал прокруткой + // (pointercancel → tapRef сбрасывается). Так тап отправляет с первого раза, + // а свайп, начатый на кнопке, просто прокручивает страницу. + const TAP_SLOP = 10 + const tapRef = useRef<{ id: number; x: number; y: number; moved: boolean } | null>(null) + const onBtnPointerDown = (e: React.PointerEvent) => { if (e.button !== 0) return e.preventDefault() + tapRef.current = { id: e.pointerId, x: e.clientX, y: e.clientY, moved: false } + } + const onBtnPointerMove = (e: React.PointerEvent) => { + const t = tapRef.current + if (!t || e.pointerId !== t.id) return + if (Math.abs(e.clientX - t.x) > TAP_SLOP || Math.abs(e.clientY - t.y) > TAP_SLOP) t.moved = true + } + const onBtnPointerUp = (e: React.PointerEvent) => { + const t = tapRef.current + tapRef.current = null + if (!t || e.pointerId !== t.id || t.moved) return runSubmit() } + const onBtnPointerCancel = () => { tapRef.current = null } // text-base на мобильной ширине (16px) → iOS Safari не зумит страницу при // фокусе на поле; на sm и выше — привычные 14px. user-scalable=no НЕ ставим. @@ -314,7 +328,10 @@ export default function SignupForm({ defaultPlan = 'start' }: Props) { {fieldErrors.agree &&

{fieldErrors.agree}

} -

Уже есть аккаунт? Войти

diff --git a/src/food-market.web/src/pages/LoginPage.tsx b/src/food-market.web/src/pages/LoginPage.tsx index f3cb71c..8dce620 100644 --- a/src/food-market.web/src/pages/LoginPage.tsx +++ b/src/food-market.web/src/pages/LoginPage.tsx @@ -36,14 +36,29 @@ export function LoginPage() { // onSubmit формы (Enter с клавиатуры). function handleSubmit(e: FormEvent) { e.preventDefault(); runSubmit() } - // Касание кнопки «Войти»: отправляем на pointerdown, ДО того как blur поля - // перерисует форму и сдвинёт кнопку (иначе на мобильном первое касание - // терялось). preventDefault удерживает фокус в поле и подавляет click. - function onSubmitBtnPointerDown(e: React.PointerEvent) { + // Касание кнопки «Войти». На pointerdown только preventDefault — фокус + // остаётся в поле, blur-ошибка не сдвигает вёрстку (иначе первое касание + // терялось). Отправляем на pointerup, только если палец не сдвинулся и жест + // не стал прокруткой (pointercancel сбрасывает). Свайп с кнопки — прокрутка. + const TAP_SLOP = 10 + const tapRef = useRef<{ id: number; x: number; y: number; moved: boolean } | null>(null) + function onBtnPointerDown(e: React.PointerEvent) { if (e.button !== 0) return e.preventDefault() + tapRef.current = { id: e.pointerId, x: e.clientX, y: e.clientY, moved: false } + } + function onBtnPointerMove(e: React.PointerEvent) { + const t = tapRef.current + if (!t || e.pointerId !== t.id) return + if (Math.abs(e.clientX - t.x) > TAP_SLOP || Math.abs(e.clientY - t.y) > TAP_SLOP) t.moved = true + } + function onBtnPointerUp(e: React.PointerEvent) { + const t = tapRef.current + tapRef.current = null + if (!t || e.pointerId !== t.id || t.moved) return runSubmit() } + function onBtnPointerCancel() { tapRef.current = null } async function runSubmit() { const em = validateEmail(email) @@ -154,7 +169,10 @@ export function LoginPage() {