Урок 20. Решения

📁 Раздел: shadcn/ui ⏱️ Время изучения: ~55 мин 🎯 Сложность: Продвинутая

⚡ Кратко

Поле со всеми связями, схема обратной связи и чек-лист доступности.

Решение задания 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>
Последний пункт часто забывают: если форма длинная, а ошибка в первом поле, пользователь просто не увидит, что случилось. Перевод фокуса решает это за одну строку.