문제
필수 드롭다운 (예: '국가 선택')의 경우 기본 옵션에 빈 값이 있으면 표준 유효성 검사에서 해당 필드를 즉시 잘못된 것으로 표시합니다. 이로 인해 시각적 노이즈가 발생할 수 있습니다. 사용자가 메뉴를 열고 옵션을 선택하지 않고 닫거나 양식을 제출하려고 하는 경우에만 오류를 표시하려고 합니다.
해결 방법
:user-invalid 가상 클래스는 <select> 요소와 원활하게 작동합니다. 사용자의 상호작용 흐름을 존중합니다. 단순히 페이지를 로드하거나 변경 없이 포커스를 맞추거나 흐리게 처리하는 것은 상호작용으로 간주되지 않으므로 사용자가 적극적으로 선택을 시도할 때까지 필드는 중립 상태로 유지됩니다.
구현 전략
- HTML 제약 조건:
required와 함께<select>를 사용합니다. 첫 번째 옵션은value=""여야 하며, 유효한 선택을 강제하기 위해 사용 중지/숨김 처리하는 것이 좋습니다. - 시각적 피드백:
:user-invalid를 사용하여 선택 상자 테두리의 스타일을 지정합니다. - 타이밍: 사용자가 컨트롤을 흐리게 처리하기 전 또는 양식을 제출할 때 값을 변경하면 (기본 잘못된 상태로 다시 변경하는 경우에도) 브라우저는 필드를 '상호작용'으로 간주합니다.
구현 가이드
1. HTML 구조
여기서 핵심은 'placeholder' 옵션입니다.
<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;
}
대체 및 브라우저 지원
:user-valid 및 :user-invalid의 대체 및 브라우저 지원
대체 CSS
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);
기타 고려사항
- 모바일 동작: 휴대기기에서는 OS 선택 도구에 따라 '블러'가 다르게 발생할 수 있습니다. 실제 기기에서 테스트하는 것이 좋습니다.
- 접근성: 네이티브
: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);
});