المشكلة
توفّر عملية التحقّق من صحة HTML5 العادية ملاحظات مرئية (من خلال :invalid أو :user-invalid)، ولكنّها لا تتم مزامنتها تلقائيًا مع سمات تسهيل الاستخدام، مثل aria-invalid.
في حال استخدام نمط :invalid العادي، قد تعلن برامج قراءة الشاشة عن "إدخال غير صالح" في اللحظة التي ينتقل فيها المستخدم إلى حقل مطلوب فارغ حاليًا. يؤدي ذلك إلى تجربة مزعجة للمستخدمين الذين يستخدمون التكنولوجيا المساعدة، حيث يتم الإعلان عن الخطأ قبل حدوث التفاعل.
الحل
نريد تطبيق الحالة البرمجية (aria-invalid="true") في اللحظة نفسها التي يتم فيها تطبيق الحالة المرئية (:user-invalid). بما أنّ :user-invalid يعتمد على علامة "تفاعل المستخدم" الداخلية في المتصفّح، يمكننا استخدام JavaScript للتحقّق من تطابق هذا المحدّد أثناء أحداث التفاعل العادية.
لمزيد من التفاصيل، يُرجى الاطّلاع على MDN aria-invalid.
استراتيجية التنفيذ
- الطبقة المرئية: استخدِم CSS
:user-invalidلعرض الحدود أو الرموز. - طبقة تسهيل الاستخدام: استخدِم
aria-invalidوaria-errormessageلإبلاغ التكنولوجيا المساعِدة (AT) بالحالة. - طبقة تسهيل الاستخدام والعرض المرئي في Bridge: أنشِئ أداة JavaScript خفيفة تستمع إلى أحداث
blurوinput، وتتحقّق مما إذا كان العنصر يتطابق مع:user-invalid، وتعدّل سمات ARIA وفقًا لذلك.
دليل الاستخدام
1. بنية HTML
اربط حقل الإدخال برسالة الخطأ الخاصة به باستخدام aria-errormessage (أو aria-describedby لتوفير دعم أوسع).
<form>
<div class="field">
<label for="email">Email</label>
<input
type="email"
id="email"
required
aria-errormessage="email-error"
>
<span id="email-error" class="error-msg">
Please enter a valid email address.
</span>
</div>
</form>
2. CSS
يمكنك التحكّم في ظهور رسالة الخطأ باستخدام الفئة الزائفة الأصلية :user-invalid.
.error-msg {
display: none;
color: #d93025;
}
/* Show error message when input is user-invalid */
input:user-invalid ~ .error-msg {
display: block;
}
/* Optional: Visual cues on the input itself */
input:user-invalid {
border-color: #d93025;
}
3- JavaScript
بما أنّه لا يوجد حدث "UserInvalidChanged"، يمكنك ربط أحداث النماذج العادية للتحقّق من الحالة.
const updateAriaState = (event) => {
const input = event.target;
if (!input.matches?.('input, textarea, select')) return;
// Check if the browser currently considers this input "user-invalid"
const isUserInvalid = input.matches(':user-invalid');
if (isUserInvalid) {
input.setAttribute('aria-invalid', 'true');
} else {
input.removeAttribute('aria-invalid');
}
};
// Listen on the document to handle dynamically added fields.
// 'blur' and 'focus' do not bubble, so we must use the capture phase (true).
document.addEventListener('blur', updateAriaState, true);
document.addEventListener('focus', updateAriaState, true);
// Also update on input if we've already shown the error,
// so the error clears immediately when fixed.
document.addEventListener('input', (event) => {
const input = event.target;
if (!input.matches?.('input, textarea, select')) return;
const hasAriaInvalid = input.hasAttribute('aria-invalid');
const ariaInvalid = input.getAttribute('aria-invalid');
if (hasAriaInvalid && ariaInvalid === 'true') {
updateAriaState(event);
}
});
العودة إلى الإعدادات التلقائية والتوافق مع المتصفّح
رصد الميزات
يمكنك التحقّق من توافق CSS وJavaScript.
التحقّق من JavaScript:
if (!CSS.supports('selector(:user-invalid)')) {
// Fallback logic here
}
CSS للإجراء الاحتياطي
لضمان عدم تمييز منطق الاحتياط بصريًا عن السلوك الأصلي، يجب تطبيق أنماط الأخطاء على كل من الفئة الزائفة وفئة الاحتياط.
/* Apply error styles to both native selector and fallback class */
input:user-invalid,
input.user-invalid-fallback {
border-color: #d93025;
background-color: #fce8e6;
}
/* Show error message for both cases */
input:user-invalid ~ .error-msg,
input.user-invalid-fallback ~ .error-msg {
display: block;
}
المنطق الاحتياطي
إذا لم يكن :user-invalid متوفّرًا، يمكنك تتبُّع حالة التفاعل يدويًا باستخدام WeakMap.
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' && input.matches?.('form')) {
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.matches?.('input, textarea, select')) 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 = () => {
if (CSS.supports('selector(:user-invalid)')) return;
document.addEventListener('blur', handleEvent, true); // Capture phase required
document.addEventListener('input', handleEvent, true);
document.addEventListener('change', handleEvent, true);
document.addEventListener('reset', handleEvent, true); // Capture resets
};
return { init };
})();
// Initialize globally
UserInvalidFallback.init();
اعتبارات أخرى
aria-liveمقابلaria-errormessage:- يربط السمة
aria-errormessageالإدخال بالنص، ولكن قد لا تقرأه برامج قراءة الشاشة بصوت عالٍ فور ظهوره (فقط عند التركيز على الإدخال). - إذا كنت بحاجة إلى إعلان فوري عند ظهور الخطأ (على سبيل المثال، عند التمويه)، ننصحك بإضافة
role="alert"أوaria-live="polite"إلى حاوية رسالة الخطأ، ولكن عليك إجراء اختبار شامل لتجنُّب "الإعلان المزدوج" عندما يركّز المستخدم على الحقل لإصلاحه.
- يربط السمة
التوسع الدولي:
- تأكَّد من ترجمة المحتوى النصي لرسالة الخطأ (
#email-error). يبقى المنطق كما هو.
- تأكَّد من ترجمة المحتوى النصي لرسالة الخطأ (