1/** 2 * HOB FormChat Pro â RGAA Accessibility Runtime 3 * 4 * Handles dynamic RGAA compliance: 5 * - aria-invalid toggle on validation errors 6 * - Error message injection with role="alert" 7 * - Focus management on validation failure 8 * - aria-live announcements for dynamic zones (calculators, AI) 9 * - Focus-visible outline enforcement 10 * 11 * @package HOB_FormChat_Pro 12 * @since 1.21 13 */ 14(function () { 15 'use strict'; 16 17 document.addEventListener('DOMContentLoaded', function () { 18 var wrappers = document.querySelectorAll('.hob-fc-rgaa'); 19 if (!wrappers.length) return; 20 21 wrappers.forEach(function (wrapper) { 22 initRgaa(wrapper); 23 }); 24 }); 25 26 function initRgaa(wrapper) { 27 var form = wrapper.querySelector('.hob-fc-form'); 28 if (!form) return; 29 30 var isHighContrast = wrapper.dataset.rgaaContrast === 'high'; 31 32 // 1. Focus-visible: add class for CSS outline. 33 form.addEventListener('focusin', function (e) { 34 var el = e.target; 35 if (el.matches('input, select, textarea, button, [tabindex]')) { 36 el.classList.add('hob-fc-focus-visible'); 37 } 38 }); 39 form.addEventListener('focusout', function (e) { 40 e.target.classList.remove('hob-fc-focus-visible'); 41 }); 42 43 // 2. Validation: set aria-invalid on blur for required fields. 44 form.addEventListener('focusout', function (e) { 45 var el = e.target; 46 if (!el.matches('input, select, textarea')) return; 47 if (!el.hasAttribute('aria-required') && !el.required) return; 48 49 var isEmpty = !el.value || (el.value.trim() === ''); 50 var fieldId = el.id ? el.id.replace('hob-field-', '') : ''; 51 var errorEl = fieldId ? document.getElementById('hob-error-' + fieldId) : null; 52 53 if (isEmpty) { 54 el.setAttribute('aria-invalid', 'true'); 55 if (errorEl) { 56 var label = ''; 57 var labelEl = form.querySelector('label[for="' + el.id + '"]'); 58 if (labelEl) label = labelEl.textContent.replace(/\s*\*\s*$/, '').trim(); 59 errorEl.textContent = label 60 ? 'Le champ ' + label + ' est obligatoire.' 61 : 'Ce champ est obligatoire.'; 62 errorEl.style.display = ''; 63 } 64 } else { 65 el.removeAttribute('aria-invalid'); 66 if (errorEl) { 67 errorEl.textContent = ''; 68 errorEl.style.display = 'none'; 69 } 70 } 71 }); 72 73 // 3. On submit: validate and focus first invalid field. 74 form.addEventListener('submit', function (e) { 75 var firstInvalid = form.querySelector('[aria-invalid="true"], :invalid'); 76 if (firstInvalid) { 77 e.preventDefault(); 78 firstInvalid.focus(); 79 80 // Announce to screen readers. 81 var liveZone = wrapper.querySelector('.hob-fc-rgaa-announce'); 82 if (!liveZone) { 83 liveZone = document.createElement('div'); 84 liveZone.className = 'hob-fc-rgaa-announce hob-fc-sr-only'; 85 liveZone.setAttribute('aria-live', 'assertive'); 86 liveZone.setAttribute('role', 'status'); 87 wrapper.appendChild(liveZone); 88 } 89 var invalidCount = form.querySelectorAll('[aria-invalid="true"]').length 90 || form.querySelectorAll(':invalid').length; 91 liveZone.textContent = invalidCount + ' erreur' + (invalidCount > 1 ? 's' : '') 92 + ' dans le formulaire. Veuillez corriger ' + (invalidCount > 1 ? 'les' : 'le') 93 + ' champ' + (invalidCount > 1 ? 's' : '') + ' en erreur.'; 94 } 95 }); 96 97 // 4. Dynamic zones: add aria-live to calculator results. 98 var calcOutputs = form.querySelectorAll('.hob-fc-calculator-result, [data-calc-formula]'); 99 calcOutputs.forEach(function (el) { 100 if (!el.getAttribute('aria-live')) { 101 el.setAttribute('aria-live', 'polite'); 102 el.setAttribute('aria-atomic', 'true'); 103 } 104 }); 105 106 // 5. High contrast mode: add data attribute for CSS. 107 if (isHighContrast) { 108 wrapper.classList.add('hob-fc-high-contrast'); 109 } 110 } 111})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.