• bradenanderson

    (@bradenanderson)


    I’m trying to get my website in compliance with WCAG 2.2 standards, which has been fairly easy UNTIL I scan pages with RTCL elements. Things like Search Filter fields and listing “thumbs” are not ADA/WCAG compliant, and I can’t seem to “force” compliance via things like manually injecting ARIA labels via HTML/JS, likely because of how the container is wrapped via AJAX… Really wish someone from the RTCL dev would look into this. ADA/ECAG compliance is a legal requirement in the US and UE, so this is a pretty HUGE oversight on part of RT.

    The page I need help with: [log in to see the link]

Viewing 4 replies - 1 through 4 (of 4 total)
  • Plugin Support Muhammad Ali Akbar

    (@alireyad)

    Hi,
    I see there exist aria-label for input field which field don’t have label element and also available alt attribute for image elements. Could you inform how did you scan it to check WCAG 2.2 standards compliant?

    Thank you

    Thread Starter bradenanderson

    (@bradenanderson)

    Yes, I had to insert a TON of custom PHP/JS/HTML to get this to work. The solution is not native to your plugin. So, you will be seeing MY FIX right now, which got things to work and will show ARIA labels correctly. But the problem is still there for other users and is still underlying in your plugin. My site is fixed and accessible because of all the custom code I wrote, I would be happy to share that code here.

    You can use any WCAG checker you would like (it’s pretty obvious you don’t build any ADA/WCAG accessibility into your plugin if you are asking what tools to use to check). But I use GetWCAG, Accessibe, EqualWeb, and WAVE, to name a few of the best tools).

    Thread Starter bradenanderson

    (@bradenanderson)

    Here is the JS I wrote to handle the Search Filter/Slider (Radius) Aria Label Add:

    /* Accessibility fixer for noUiSlider handles (labels + aria-valuetext).
    Drop into Code Snippets as JS (Run everywhere). */
    (function () {
    const DEFAULT_LABEL = ‘Radius in miles’;

    // Try to find a meaningful visible label near the slider element.
    function getMeaningfulLabelForSlider(sliderEl) {
        // 1) Check for an explicit aria-labelledby on the slider or ancestors
        let el = sliderEl;
        for (let i = 0; i < 6 && el; i++, el = el.parentElement) {
            const labelledBy = el.getAttribute && el.getAttribute('aria-labelledby');
            if (labelledBy) {
                const referenced = document.getElementById(labelledBy);
                if (referenced && referenced.textContent.trim()) return referenced.textContent.trim();
            }
        }
    
        // 2) Look for preceding label/heading text (immediate siblings / ancestors)
        el = sliderEl;
        for (let depth = 0; depth < 6 && el; depth++, el = el.parentElement) {
            const prev = el.previousElementSibling;
            if (prev && prev.textContent && prev.textContent.trim()) {
                const txt = prev.textContent.trim();
                // If it looks like a label (contains letters), return it
                if (/[A-Za-z]/.test(txt)) return txt;
            }
            // also check for a label element inside parent
            const label = el.querySelector && el.querySelector('label, .label, .filter-label, .rtcl-label');
            if (label && label.textContent && label.textContent.trim()) return label.textContent.trim();
        }
    
        // fallback
        return DEFAULT_LABEL;
    }
    
    // Set aria attributes on a handle and hook updates
    function setHandleAccessibility(handleEl, sliderEl, labelText, handleIndex) {
        if (!handleEl) return;
    
        // If there is already an aria-labelledby, skip setting aria-label
        if (!handleEl.hasAttribute('aria-label') && !handleEl.hasAttribute('aria-labelledby')) {
            handleEl.setAttribute('aria-label', labelText);
        }
    
        // Initialize aria-valuetext from aria-valuenow if present
        const now = handleEl.getAttribute('aria-valuenow');
        if (now !== null) handleEl.setAttribute('aria-valuetext', now + ' miles');
    
        // Attach to noUiSlider update if available on the parent slider element
        try {
            if (sliderEl && sliderEl.noUiSlider && typeof sliderEl.noUiSlider.on === 'function' && !sliderEl.__ariaUpdateHooked) {
                sliderEl.noUiSlider.on('update', function (values, idx) {
                    const handles = sliderEl.querySelectorAll('.noUi-handle');
                    const h = handles[idx];
                    if (h) h.setAttribute('aria-valuetext', values[idx] + ' miles');
                });
                sliderEl.__ariaUpdateHooked = true;
            } else {
                // Fallback: observe aria-valuenow changes on this handle
                if (!handleEl.__ariaObserver) {
                    const obs = new MutationObserver((recs) => {
                        recs.forEach(r => {
                            if (r.type === 'attributes' && r.attributeName === 'aria-valuenow') {
                                const v = handleEl.getAttribute('aria-valuenow');
                                if (v !== null) handleEl.setAttribute('aria-valuetext', v + ' miles');
                            }
                        });
                    });
                    obs.observe(handleEl, { attributes: true, attributeFilter: ['aria-valuenow'] });
                    handleEl.__ariaObserver = obs;
                }
            }
        } catch (e) {
            // be silent if anything goes wrong
            console.error && console.error('aria-fix slider hook error', e);
        }
    }
    
    // Fix a given .noUi-target element (the slider container)
    function fixSliderElement(sliderEl) {
        if (!sliderEl || sliderEl.dataset.__ariaFixed === '1') return;
        const label = getMeaningfulLabelForSlider(sliderEl);
    
        const handles = sliderEl.querySelectorAll('.noUi-handle');
        if (!handles || handles.length === 0) {
            // nothing yet – don't mark as fixed so we can retry later
            return;
        }
    
        handles.forEach((h, idx) => {
            // For multi-handle sliders, give meaningful min/max labels
            let labelText = label;
            if (handles.length > 1) {
                labelText = (idx === 0) ? (label + ' (minimum)') : (label + ' (maximum)');
            }
            setHandleAccessibility(h, sliderEl, labelText, idx);
        });
    
        // mark fixed
        sliderEl.dataset.__ariaFixed = '1';
    }
    
    // 1) Monkey-patch noUiSlider.create if present so we can fix immediately after creation
    if (window.noUiSlider && typeof window.noUiSlider.create === 'function') {
        const origCreate = window.noUiSlider.create;
        window.noUiSlider.create = function (target, options) {
            const result = origCreate.apply(this, arguments);
            // Timeout 0 to allow plugin's creation to finish then apply the accessibility fixes
            setTimeout(function () {
                try { fixSliderElement(target); } catch (e) { /* noop */ }
            }, 0);
            return result;
        };
    }
    
    // 2) MutationObserver to catch sliders added or replaced dynamically
    const bodyObserver = new MutationObserver((mutations) => {
        mutations.forEach(m => {
            if (m.type === 'childList') {
                m.addedNodes.forEach(node => {
                    if (node.nodeType !== 1) return;
                    // If the node itself is a slider
                    if (node.matches && node.matches('.noUi-target')) {
                        fixSliderElement(node);
                    }
                    // Or contains slider descendants
                    if (node.querySelectorAll) {
                        node.querySelectorAll('.noUi-target').forEach(fixSliderElement);
                    }
                });
            }
        });
    });
    bodyObserver.observe(document.documentElement || document.body, { childList: true, subtree: true });
    
    // 3) Initial pass for any already-present sliders
    document.querySelectorAll('.noUi-target').forEach(fixSliderElement);
    
    // 4) Poll briefly as a last resort for super-late renders (stop after ~10s)
    let tries = 0;
    const poll = setInterval(() => {
        document.querySelectorAll('.noUi-target').forEach(fixSliderElement);
        if (++tries > 100) clearInterval(poll); // 100 * 100ms = 10s
    }, 100);
    
    // Optional: expose a debugger hook
    window.__noUiA11yFix = { fixSliderElement };

    })();

    Plugin Support Muhammad Ali Akbar

    (@alireyad)

    Hi,
    Yes, we noticed it and checked with this tool https://wave.webaim.org/

    We will sort out soon.

    Thank you

Viewing 4 replies - 1 through 4 (of 4 total)

You must be logged in to reply to this topic.