Проверка ввода после взаимодействия

Веб-функции: :user-valid и :user-invalid

Проблема

Показывать ошибки валидации в тот момент, когда пользователь фокусируется на поле и начинает вводить текст, преждевременно и отвлекает внимание. Например, когда пользователь вводит адрес электронной почты (например, "user@gm") или пароль со сложными требованиями, поле технически недействительно до завершения. При стандартном стиле :invalid сообщение об ошибке появляется сразу, что может раздражать пользователя.

Решение

Псевдокласс :user-invalid позволяет отложить состояние ошибки до тех пор, пока пользователь не "зафиксирует" значение (размыв поле) или не попытается отправить форму. Это гарантирует, что отзыв о проверке будет предоставлен только после того, как пользователь закончит взаимодействие с полем.

Стратегия реализации

  1. Ограничения HTML. Используйте стандартные атрибуты HTML5, такие как type="email", pattern и required, чтобы активировать встроенную логику проверки браузера.
  2. Визуальная обратная связь. Используйте :user-invalid, чтобы применить стиль ошибки только после взаимодействия.
  3. Положительное подкрепление. Вы можете использовать :user-valid, чтобы показать зеленый индикатор успеха, когда требования выполнены.
  4. Плавное восстановление. Как только пользователь исправляет введенные данные на допустимый формат, :user-invalid перестает сопоставлять их, немедленно удаляя состояние ошибки.

Руководство по внедрению

Пример использования 1. Проверка адресов электронной почты

ОБЯЗАТЕЛЬНО. Используйте стандартные атрибуты HTML5 для полей электронной почты. По умолчанию сообщение об ошибке скрыто и показывается, только когда браузер определяет, что пользователь оставил поле в недопустимом состоянии.

<form>
  <div class="field">
    <label for="email">Email Address</label>
    <!-- MANDATORY: Place format hints above the input so autocomplete popovers don't cover them during editing -->
    <span id="email-hint" class="hint">Format: you@example.com</span>
    <!-- DO: Use standard HTML validation attributes like type="email" and required -->
    <input
      type="email"
      id="email"
      name="email"
      required
      autocomplete="email"
      aria-describedby="email-hint"
      aria-errormessage="email-error"
    >
    <div id="email-error" class="error-msg">
      <span aria-hidden="true">❌</span> Please enter a valid email address.
    </div>
  </div>
</form>
.hint {
  display: block;
  color: #5f6368;
  font-size: 0.85rem;
  margin-bottom: 0.25rem;
}

.error-msg {
  display: none;
  color: #d93025;
  font-size: 0.875rem;
  margin-top: 0.25rem;
}

/*
  DO: Only show error styles after user interaction.
  Use multiple indicators (border/background shift + icon/text) to avoid color-only states.
*/
input:user-invalid {
  border-color: #d93025;
  background-color: #fce8e6;
}

/* DO: Reveal the error message using the adjacent sibling selector */
input:user-invalid + .error-msg {
  display: block;
}

/* DO: Provide a clear success indication on :user-valid */
input:user-valid {
  border-color: #188038;
}

Пример 2. Сложность пароля

ОБЯЗАТЕЛЬНО. Определите правило сложности, используя шаблон Regex Lookahead в атрибуте pattern. Список правил показывается над полем ввода, чтобы помочь пользователю, и выделяется, если есть ошибка.

<form>
  <div class="field">
    <label for="password">New Password</label>
    <!-- MANDATORY: Place hints and rules above the input so mobile keyboards do not obscure them -->
    <ul id="password-rules" class="rules-list">
      <li>At least 8 characters</li>
      <li>One uppercase letter</li>
      <li>One number</li>
      <li>One special character</li>
    </ul>
    <!-- DO: Use pattern and minlength for complex password validation
         DO: Match all constraints with lookaheads via pattern attribute
     -->
    <input
      type="password"
      id="password"
      autocomplete="new-password"
      required
      pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[\W_]).{8,}"
      minlength="8"
      aria-describedby="password-rules"
    >
  </div>
</form>
/* DO: State the default styling as neutral */
.rules-list { 
  color: #5f6368; 
  margin-bottom: 0.5rem;
}

/* DO: Show invalid state (After interaction): Error */
input:user-invalid {
  border-color: #d93025;
  background-color: #fce8e6;
}

/* DO: Highlight rules list when error is shown using the modern :has() selector */
.field:has(input:user-invalid) .rules-list {
  color: #d93025;
  font-weight: 600;
}

/* DO: Add success indications for :user-valid state */
input:user-valid {
  border-color: #188038;
}

/* DO: Hide rules once satisfied */
.field:has(input:user-valid) .rules-list {
  display: none;
}

Резервные объявления и поддержка браузеров

Резервные варианты и поддержка браузеров для :user-valid и :user-invalid

Резервный ССЦ

input:user-invalid,
input.user-invalid-fallback {
  border-color: #d93025;
  background-color: #fce8e6;
}

input:user-invalid + .error-msg,
input.user-invalid-fallback + .error-msg {
  display: block;
}

Резервный код JavaScript

Используйте многоразовую утилиту, которая отслеживает состояние взаимодействия с помощью WeakMap. Это позволяет избежать загрязнения DOM "грязными" классами или атрибутами данных.

const UserInvalidFallback = (() => {
  const dirtyState = new WeakMap();

  const updateState = (input) => {
    const isValid = input.checkValidity();

    // Update both visual and ARIA state
    input.classList.toggle('user-invalid-fallback', !isValid);
    input.classList.toggle('user-valid-fallback', isValid);

    if (!isValid) {
      input.setAttribute('aria-invalid', 'true');
    } else {
      input.removeAttribute('aria-invalid');
    }
  };

  const handleEvent = (event) => {
    const input = event.target;

    if (event.type === 'reset') {
      const controls = input.elements || [];
      for (const control of controls) {
        dirtyState.delete(control);
        control.classList.remove('user-invalid-fallback');
        control.classList.remove('user-valid-fallback');
        control.removeAttribute('aria-invalid');
      }
      return;
    }

    if (!input.checkValidity) return;

    if (event.type === 'input' || event.type === 'change') {
      const state = dirtyState.get(input) || { hasInteracted: false, hasBlurred: false };
      state.hasInteracted = true;
      dirtyState.set(input, state);
      if (state.hasBlurred) {
        updateState(input);
      }
    } else if (event.type === 'blur') {
      const state = dirtyState.get(input) || { hasInteracted: false, hasBlurred: false };
      state.hasBlurred = true;
      dirtyState.set(input, state);
      if (state.hasInteracted) {
        updateState(input);
      }
    }
  };

  const init = (root = document) => {
    if (CSS.supports('selector(:user-invalid)')) return;

    root.addEventListener('blur', handleEvent, true); // Capture phase
    root.addEventListener('input', handleEvent);
    root.addEventListener('change', handleEvent);
    root.addEventListener('reset', handleEvent, true); // Capture resets
  };

  return { init };
})();

// Initialize for a specific form
const form = document.querySelector('#demo-form');
UserInvalidFallback.init(form);

О чем ещё нужно помнить

  1. Специальные возможности.
    • ОБЯЗАТЕЛЬНО. Используйте aria-describedby, чтобы связать список правил с полем ввода.
    • НЕЛЬЗЯ: скрывать список правил, пока не будет введен допустимый пароль. Пользователи должны знать, что от них требуется.
  2. Ограничения атрибута Pattern. ОБЯЗАТЕЛЬНО: атрибут pattern выполняет полное сопоставление (подразумевается ^...$). Убедитесь, что регулярное выражение пароля учитывает всю строку.
  3. Строгость проверки. Обратите внимание, что проверка type="email" по умолчанию в браузере не очень строгая (например, может пройти user@localserver). Если вам нужна более строгая проверка, используйте более надежную библиотеку или собственную функцию валидации вместе с type="email".
  4. Управление фокусом. ОБЯЗАТЕЛЬНО. Если пользователь отправляет форму с недействительным полем, браузер автоматически фокусируется на первом недействительном поле. Стили :user-invalid будут применены сразу, поскольку попытка отправки считается взаимодействием.
  5. Согласованность с ARIA. Нативный элемент :user-invalid не синхронизируется с атрибутами ARIA автоматически. Добавьте следующий код JavaScript, чтобы синхронизировать aria-invalid с визуальным состоянием:
// Sync aria-invalid with the CSS :user-invalid state
const syncAria = (el) => {
  el.setAttribute?.('aria-invalid', el.matches(':user-invalid') ? 'true' : 'false');
};

// Update on blur (to show error) and input (to clear it)
document.addEventListener('blur', (e) => syncAria(e.target), true);
document.addEventListener('input', (e) => {
  if (e.target.hasAttribute('aria-invalid')) syncAria(e.target);
});