Ý kiến phản hồi về trường bắt buộc

Vấn đề

Việc đánh dấu các trường bắt buộc bằng trạng thái lỗi ngay khi tải trang có thể gây nhầm lẫn. Tốt nhất là một trường bắt buộc chỉ nên có trạng thái "không hợp lệ" nếu người dùng đã cố gắng điền vào trường đó nhưng không thành công.

Giải pháp

Lớp giả :user-invalid giải quyết vấn đề này một cách hoàn hảo. Đối với trường bắt buộc, trường này sẽ không khớp khi tải trang. Điều kiện này chỉ khớp nếu:

  1. Người dùng tương tác với trường (ví dụ: nhập một ký tự rồi xoá ký tự đó) rồi rời khỏi trường (làm mờ), để trống trường.
  2. Người dùng cố gắng gửi biểu mẫu khi trường này trống.

Chiến lược triển khai

  1. Ràng buộc HTML: Thêm thuộc tính required vào dữ liệu đầu vào.
  2. Phản hồi bằng hình ảnh: Sử dụng :user-invalid để tạo kiểu cho đường viền màu đỏ và hiển thị văn bản trợ giúp "Bắt buộc".
  3. Thời gian: Dựa vào thời gian gốc của trình duyệt để nhận phản hồi bằng hình ảnh. Bạn không cần trình xử lý onBlur để thêm lớp touched nữa, mặc dù vẫn cần một số JavaScript để đồng bộ hoá các thuộc tính ARIA (xem bên dưới).

Hướng dẫn triển khai

1. Cấu trúc HTML

<form id="feedback-form">
  <div class="field">
    <label for="full-name">Full Name</label>
    <input
      type="text"
      id="full-name"
      name="full-name"
      required
      aria-errormessage="name-error"
    >
    <!-- MANDATORY: Include an icon or distinct non-color indicator alongside error text -->
    <div id="name-error" class="error-msg">
      <span aria-hidden="true">❌</span> This field is required.
    </div>
  </div>
</form>

2. CSS

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

/*
  Only highlight empty required fields AFTER the user visits them.
  MANDATORY: Provide multiple indicators (border shift + helper text/icon) to avoid color-only state communication.
*/
input:user-invalid {
  border-color: #d93025;
  background-color: #fce8e6;
}

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

/* Optional: Subtle indicator for required fields that are valid */
input:required:user-valid {
  border-color: #188038;
  border-width: 2px;
}

3. Đồng bộ hoá trạng thái JavaScript

BẮT BUỘC: Vì :user-invalid là trạng thái hình ảnh, nên bạn PHẢI cung cấp cầu nối JavaScript để đồng bộ hoá aria-invalid="true" một cách linh động cho các công nghệ hỗ trợ khi người dùng làm mờ một trường không hợp lệ hoặc cố gắng gửi.

const form = document.getElementById('feedback-form');

const syncAriaInvalid = (input) => {
  if (!input.checkValidity()) {
    input.setAttribute('aria-invalid', 'true');
  } else {
    input.removeAttribute('aria-invalid');
  }
};

// Sync on blur when a user finishes interacting
form.addEventListener('blur', (e) => {
  if (e.target.matches('input[required]')) {
    syncAriaInvalid(e.target);
  }
}, true);

// Sync all required fields when submission is attempted
form.addEventListener('submit', () => {
  form.querySelectorAll('input[required]').forEach(syncAriaInvalid);
});

// Remove error state immediately upon correction
form.addEventListener('input', (e) => {
  if (e.target.matches('input[required]') && e.target.checkValidity()) {
    e.target.removeAttribute('aria-invalid');
  }
});

Dự phòng và hỗ trợ trình duyệt

Dự phòng và hỗ trợ trình duyệt cho :user-valid và :user-invalid

CSS cho quảng cáo dự phòng

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;
}

Dự phòng JavaScript

Sử dụng một tiện ích có thể tái sử dụng để theo dõi trạng thái tương tác bằng WeakMap. Điều này giúp tránh làm ô nhiễm DOM bằng các lớp hoặc thuộc tính dữ liệu "bẩn".

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);

Những điều cần xem xét khác

  1. Dấu hoa thị: Bạn vẫn nên chỉ định các trường bắt buộc một cách trực quan (ví dụ: bằng dấu hoa thị *) trong nhãn để người dùng biết những gì cần làm trước khi tương tác.
  2. Nút gửi: Không giống như nút disabled, hãy luôn bật nút gửi. Nếu người dùng nhấp vào nút đó, trình duyệt sẽ tự động kích hoạt :user-invalid trên tất cả các trường bắt buộc còn trống và tập trung vào trường đầu tiên. Điều này rất hữu ích cho khả năng hỗ trợ tiếp cận và trải nghiệm người dùng.
  3. Hỗ trợ tiếp cận: :user-invalid gốc không tự động đồng bộ hoá với các thuộc tính ARIA. Thêm JavaScript sau để đồng bộ hoá aria-invalid với trạng thái hình ảnh:
// 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);
});