Chọn tương tác với trình đơn

Vấn đề

Đối với trình đơn thả xuống bắt buộc (ví dụ: "Chọn quốc gia"), quy trình xác thực tiêu chuẩn sẽ gắn cờ trường là không hợp lệ ngay lập tức nếu lựa chọn mặc định có giá trị trống. Điều này có thể gây ra sự lộn xộn về mặt hình ảnh. Chúng ta chỉ muốn hiện lỗi nếu người dùng mở trình đơn rồi đóng mà không chọn một mục hoặc cố gắng gửi biểu mẫu.

Giải pháp

Lớp giả :user-invalid hoạt động liền mạch với các phần tử <select>. Trạng thái này tôn trọng quy trình tương tác của người dùng: chỉ cần tải trang hoặc lấy tiêu điểm/làm mờ mà không thực hiện thay đổi thì không được tính là một lượt tương tác, vì vậy, trường này vẫn ở trạng thái trung lập cho đến khi người dùng chủ động chọn.

Chiến lược triển khai

  1. Ràng buộc HTML: Sử dụng <select> với required. Lựa chọn đầu tiên phải có value="" và lý tưởng nhất là bị vô hiệu hoá/ẩn để buộc người dùng phải đưa ra lựa chọn hợp lệ.
  2. Phản hồi bằng hình ảnh: Sử dụng :user-invalid để tạo kiểu cho đường viền hộp chọn.
  3. Thời gian: Trình duyệt coi trường này là "đã tương tác" nếu người dùng thay đổi giá trị (ngay cả khi quay lại trạng thái không hợp lệ mặc định) trước khi họ làm mờ chế độ điều khiển hoặc khi gửi biểu mẫu.

Hướng dẫn triển khai

1. Cấu trúc HTML

Tuỳ chọn "phần giữ chỗ" là yếu tố then chốt ở đây.

<form>
  <div class="field">
    <label for="country">Country</label>
    <select
      id="country"
      name="country"
      required
      aria-errormessage="country-error"
    >
      <option value="" disabled selected>Select a country...</option>
      <option value="us">United States</option>
      <option value="ca">Canada</option>
      <option value="uk">United Kingdom</option>
    </select>
    <div id="country-error" class="error-msg">
      Please select a country.
    </div>
  </div>
</form>

2. CSS

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

/*
  Only show error after the user visits the select menu.
*/
select:user-invalid {
  border-color: #d93025;
  background-color: #fce8e6;
}

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

select:user-valid {
  border-color: #188038;
}

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. Hành vi trên thiết bị di động: Trên thiết bị di động, hiệu ứng "làm mờ" có thể xảy ra theo cách khác nhau tuỳ thuộc vào bộ chọn hệ điều hành. Bạn nên kiểm thử trên thiết bị thực.
  2. 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);
});