مشکل
علامتگذاری فیلدهای الزامی با حالت خطا بلافاصله پساز بار شدن صفحه میتواند گیجکننده باشد. در حالت ایدهآل، یک فیلد الزامی فقط زمانی باید «نامعتبر» بهنظر برسد که کاربر تلاش کرده باشد آن را پر کند و موفق نشده باشد.
راهحل
کلاس کاذب :user-invalid این مشکل را بهطور کامل حل میکند. برای فیلد الزامی، هنگام بار شدن صفحه مطابقت نخواهد داشت. فقط در شرایط زیر مطابقت خواهد داشت:
- کاربر با فیلد تعامل میکند (مثلاً نویسهای را تایپ میکند و آن را حذف میکند) و سپس آن را ترک میکند (محو کردن) و آن را خالی میگذارد.
- کاربر تلاش میکند فرم را درحالیکه فیلد خالی است ارسال کند.
راهبرد پیادهسازی
- محدودیت HTML: مشخصه
requiredرا به ورودیهایتان اضافه کنید. - بازخورد دیداری: از
:user-invalidبرای قرمز کردن حاشیه و نمایش نوشتار کمکی «الزامی» استفاده کنید. - زمانبندی: برای بازخورد دیداری، به زمانبندی بومی مرورگر تکیه کنید. دیگر برای افزودن کلاس
touchedبه گردانندههایonBlurنیاز ندارید، هرچند برای همگامسازی مشخصههای ARIA هنوز به مقداری جاوا اسکریپت نیاز است (به زیر مراجعه کنید).
راهنمای پیادهسازی
۱. ساختار 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>
۲. 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;
}
۳. همگامسازی وضعیت جاوا اسکریپت
اجباری: چون :user-invalid یک حالت دیداری است، باید یک پل جاوا اسکریپت برای همگامسازی پویا aria-invalid="true" برای فناوریهای کمکی ارائه دهید، زمانی که کاربر یک فیلد نامعتبر را محو میکند یا تلاش میکند ارسال کند.
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');
}
});
پشتیبانی از مرورگر و بازگشت به حالت قبلی
پشتیبانی مرورگر و بازگشت برای :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;
}
بازگشت جاوا اسکریپت
از ابزار کمکی قابلاستفاده مجددی استفاده کنید که وضعیت تعامل را بااستفاده از 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);
ملاحظات دیگر
- ستارهها: هنوز هم بهترین روش این است که فیلدهای الزامی را بهصورت بصری (مثلاً با ستاره
*) در برچسب مشخص کنید تا کاربران قبلاز تعامل بدانند چه انتظاری داشته باشند. - دکمههای ارسال: برخلاف دکمههای
disabled، دکمه ارسال را فعال نگه دارید. اگر کاربر روی آن کلیک کند، مرورگر بهطور خودکار:user-invalidرا در همه فیلدهای الزامی خالی راهاندازی میکند و روی اولین فیلد تمرکز میکند. این برای دسترسپذیری و تجربه کاربری عالی است. - دسترسپذیری:
:user-invalidبومی بهطور خودکار با شاخصههای ARIA همگامسازی نمیشود. برای همگام نگه داشتن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);
});