Урок 20. Решения
⚡ Кратко
Поле со всеми связями, схема обратной связи и чек-лист доступности.
Решение задания 1
<Field data-invalid={!!error}>
<FieldLabel htmlFor="phone">Телефон</FieldLabel>
<Input
id="phone"
type="tel"
inputMode="tel"
autoComplete="tel"
placeholder="+7 900 000-00-00"
aria-invalid={!!error}
aria-describedby="phone-hint phone-error"
/>
<FieldDescription id="phone-hint">Формат: +7 и десять цифр</FieldDescription>
<FieldError id="phone-error">{error}</FieldError>
</Field>
Формат: +7 и десять цифр
Номер введён не полностью
<div class="w-full max-w-sm space-y-1.5">
<label class="text-sm font-medium" for="sol-phone">Телефон</label>
<input id="sol-phone" type="tel" inputmode="tel" value="+7 900 12" aria-invalid="true" aria-describedby="sol-phone-hint sol-phone-error"
class="h-8 w-full rounded-lg border border-input bg-transparent px-2.5 text-sm outline-none aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20">
<p id="sol-phone-hint" class="text-sm text-muted-foreground">Формат: +7 и десять цифр</p>
<div id="sol-phone-error" role="alert" class="text-sm text-destructive">Номер введён не полностью</div>
</div>
Что здесь важно: inputMode="tel" открывает
на мобильном цифровую клавиатуру, autoComplete="tel" включает
подстановку, а aria-describedby перечисляет оба
связанных текста — подсказку и ошибку.
Решение задания 2
📄 src/lib/schemas.ts
// src/lib/schemas.ts
import { z } from "zod"
export const feedbackSchema = z.object({
name: z.string().min(2, "Имя не короче двух символов"),
email: z.email("Введите корректный адрес"),
topic: z.enum(["bug", "idea", "billing"], { message: "Выберите тему" }),
message: z.string().min(20, "Опишите подробнее — минимум 20 символов"),
consent: z.literal(true, { message: "Нужно согласие на обработку данных" }),
})
export type FeedbackValues = z.infer<typeof feedbackSchema>
// { name: string; email: string; topic: "bug" | "idea" | "billing"; message: string; consent: true }
Почему сообщения в схеме: правило и его формулировка всегда рядом. Если завтра минимум станет 50 символов, вы поменяете число и текст в одном месте — и на клиенте, и на сервере.
Решение задания 3: чек-лист
| Проверка | Как должно быть |
|---|---|
| Порядок фокуса | сверху вниз, совпадает с визуальным |
| Видимость фокуса | кольцо или рамка на каждом поле и кнопке |
| Подписи | клик по подписи фокусирует поле |
| Ошибка | объявляется скринридером, связана с полем |
| Отправка | Enter в текстовом поле отправляет форму |
| Повторная отправка | кнопка заблокирована во время запроса |
| После ошибки сервера | фокус переводится на первое поле с ошибкой |
// перевод фокуса на первое поле с ошибкой
const onInvalid = (errors: FieldErrors<FeedbackValues>) => {
const first = Object.keys(errors)[0]
document.getElementById(first)?.focus()
}
<form onSubmit={handleSubmit(onValid, onInvalid)}>…</form>
Последний пункт часто забывают: если форма длинная,
а ошибка в первом поле, пользователь просто не увидит, что случилось.
Перевод фокуса решает это за одну строку.