3

I have this a11y accessibility accordion library which works great but I need to find a way for the tabs to behave differently per device. Desktop data-accordion-opened="true" and mobile data-accordion-opened="false" and accordion not to be clickable on desktop. Best way around this using this library, open to all suggestions.

   /*
 * ES2015 accessible accordion system, using ARIA
 * Website: https://van11y.net/accessible-accordion/
 * License MIT: https://github.com/nico3333fr/van11y-accessible-accordion-aria/blob/master/LICENSE
 */
const loadConfig = () => {

    const CACHE = {};

    const set = (id, config) => {

        CACHE[id] = config;

    };
    const get = (id) => CACHE[id];
    const remove = (id) => CACHE[id];

    return {
        set,
        get,
        remove
    }
};

const DATA_HASH_ID = 'data-hashaccordion-id';

const pluginConfig = loadConfig();

/** Find an element based on an Id
 * @param  {String} id Id to find
 * @param  {String} hash hash id (not mandatory)
 * @return {Node} the element with the specified id
 */
const findById = (id, hash) => document.querySelector(`#${id}[${DATA_HASH_ID}="${hash}"]`);

/** add a class to a node
 * @param  {Node} el node to attach class
 * @param  {String} className the class to add
 */
const addClass = (el, className) => {
    if (el.classList) {
        el.classList.add(className); // IE 10+
    } else {
        el.className += ' ' + className; // IE 8+
    }
}

/** remove class from node
 * @param  {Node} el node to remove class
 * @param  {String} className the class to remove
 */
const removeClass = (el, className) => {
    if (el.classList) {
        el.classList.remove(className); // IE 10+
    } else {
        el.className = el.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' '); // IE 8+
    }
}

/** check if node has specified class
 * @param  {Node} el node to check
 * @param  {String} className the class
 */
const hasClass = (el, className) => {
    if (el.classList) {
        return el.classList.contains(className); // IE 10+
    } else {
        return new RegExp('(^| )' + className + '( |$)', 'gi').test(el.className); // IE 8+ ?
    }
}

const setAttributes = (node, attrs) => {
    Object
        .keys(attrs)
        .forEach((attribute) => {
            node.setAttribute(attribute, attrs[attribute]);
        });
};

/** search if element is or is contained in another element with attribute data-hashaccordion-id
 * @param  {Node} el element (node)
 * @param  {String} hashId the attribute data-hashtooltip-id
 * @return {String} the value of attribute data-hashtooltip-id
 */
const searchParentHashId = (el, hashId) => {
    let found = false;

    let parentElement = el;
    while (parentElement.nodeType === 1 && parentElement && found === false) {

        if (parentElement.hasAttribute(hashId) === true) {
            found = true;
        } else {
            parentElement = parentElement.parentNode;
        }
    }
    if (found === true) {
        return parentElement.getAttribute(hashId);
    } else {
        return '';
    }
}
const searchParent = (el, parentClass, hashId) => {
    let found = false;

    let parentElement = el;
    while (parentElement && found === false) {
        if (hasClass(parentElement, parentClass) === true && parentElement.getAttribute(DATA_HASH_ID) === hashId) {
            found = true;
        } else {
            parentElement = parentElement.parentNode;
        }
    }
    if (found === true) {
        return parentElement.getAttribute('id');
    } else {
        return '';
    }
}

const unSelectHeaders = (elts, attrSelected) => {
    elts
        .forEach((header_node) => {
            setAttributes(header_node, {
                [attrSelected]: 'false'
            });
        });
}

const selectHeader = (el, attrSelected) => {
    el.setAttribute(attrSelected, true);
}

const selectHeaderInList = (elts, param, attrSelected) => {
    let indice_trouve;
    elts
        .forEach((header_node, index) => {

            if (header_node.getAttribute(attrSelected) === 'true') {
                indice_trouve = index;
            }

        });

    if (param === 'next') {
        selectHeader(elts[indice_trouve + 1]);
        setTimeout(function() {
            elts[indice_trouve + 1].focus();
        }, 0);
    }
    if (param === 'prev') {
        selectHeader(elts[indice_trouve - 1]);
        setTimeout(function() {
            elts[indice_trouve - 1].focus();
        }, 0);
    }

}

const plugin = (config = {}) => {

    const CONFIG = {
        ACCORDION_JS: 'js-accordion',
        ACCORDION_JS_HEADER: 'js-accordion__header',
        ACCORDION_JS_PANEL: 'js-accordion__panel',

        ACCORDION_DATA_PREFIX_CLASS: 'data-accordion-prefix-classes',
        ACCORDION_DATA_OPENED: 'data-accordion-opened',
        ACCORDION_DATA_MULTISELECTABLE: 'data-accordion-multiselectable',
        ACCORDION_DATA_COOL_SELECTORS: 'data-accordion-cool-selectors',

        ACCORDION_PREFIX_IDS: 'accordion',
        ACCORDION_BUTTON_ID: '_tab',
        ACCORDION_PANEL_ID: '_panel',

        ACCORDION_STYLE: 'accordion',
        ACCORDION_TITLE_STYLE: 'accordion__title',
        ACCORDION_HEADER_STYLE: 'accordion__header',
        ACCORDION_PANEL_STYLE: 'accordion__panel',

        ACCORDION_ROLE_TABLIST: 'tablist',
        ACCORDION_ROLE_TAB: 'tab',
        ACCORDION_ROLE_TABPANEL: 'tabpanel',

        ATTR_ROLE: 'role',
        ATTR_MULTISELECTABLE: 'aria-multiselectable',
        ATTR_EXPANDED: 'aria-expanded',
        ATTR_LABELLEDBY: 'aria-labelledby',
        ATTR_HIDDEN: 'aria-hidden',
        ATTR_CONTROLS: 'aria-controls',
        ATTR_SELECTED: 'aria-selected',
        ...config
    };

    const HASH_ID = Math.random().toString(32).slice(2, 12);

    pluginConfig.set(HASH_ID, CONFIG);
    /**
     * Find all accordions inside a container
     * @param  {Node} node Default document
     * @return {Array}
     */
    const $listAccordions = (node = document) => [].slice.call(node.querySelectorAll('.' + CONFIG.ACCORDION_JS)); //[...node.querySelectorAll('.' + CONFIG.ACCORDION_JS)]; // that does not work on IE when transpiled :-(


    /**
     * Build accordions for a container
     * @param  {Node} node
     * @param  {addListeners} boolean
     */
    const attach = (node) => {


        $listAccordions(node)
            .forEach((accordion_node) => {

                let iLisible = 'z' + Math.random().toString(32).slice(2, 12); // avoid selector exception when starting by a number
                let prefixClassName = accordion_node.hasAttribute(CONFIG.ACCORDION_DATA_PREFIX_CLASS) === true ? accordion_node.getAttribute(CONFIG.ACCORDION_DATA_PREFIX_CLASS) + '-' : '';
                let coolSelectors = accordion_node.hasAttribute(CONFIG.ACCORDION_DATA_COOL_SELECTORS) === true ? true : false;

                // Init attributes accordion
                if (accordion_node.getAttribute(CONFIG.ACCORDION_DATA_MULTISELECTABLE) === 'none') {
                    accordion_node.setAttribute(CONFIG.ATTR_MULTISELECTABLE, 'false');
                } else {
                    accordion_node.setAttribute(CONFIG.ATTR_MULTISELECTABLE, 'true');
                }
                accordion_node.setAttribute(CONFIG.ATTR_ROLE, CONFIG.ACCORDION_ROLE_TABLIST);
                accordion_node.setAttribute('id', iLisible);
                accordion_node.setAttribute(DATA_HASH_ID, HASH_ID);

                addClass(accordion_node, prefixClassName + CONFIG.ACCORDION_STYLE);

                let $listAccordionsHeader = [].slice.call(accordion_node.querySelectorAll('.' + CONFIG.ACCORDION_JS_HEADER));
                $listAccordionsHeader
                    .forEach((header_node, index_header) => {

                        // if we do not have cool selectors enabled,
                        // it is not a direct child, we ignore it
                        if (header_node.parentNode !== accordion_node && coolSelectors === false) {
                            return;
                        }

                        let indexHeaderLisible = index_header + 1;
                        let accordionPanel = header_node.nextElementSibling;
                        let accordionHeaderText = header_node.innerHTML;
                        let accordionButton = document.createElement("BUTTON");
                        let accordionOpenedAttribute = header_node.hasAttribute(CONFIG.ACCORDION_DATA_OPENED) === true ? header_node.getAttribute(CONFIG.ACCORDION_DATA_OPENED) : '';

                        // set button with attributes
                        accordionButton.innerHTML = accordionHeaderText;
                        addClass(accordionButton, CONFIG.ACCORDION_JS_HEADER);
                        addClass(accordionButton, prefixClassName + CONFIG.ACCORDION_HEADER_STYLE);
                        setAttributes(accordionButton, {
                            [CONFIG.ATTR_ROLE]: CONFIG.ACCORDION_ROLE_TAB,
                            'id': CONFIG.ACCORDION_PREFIX_IDS + iLisible + CONFIG.ACCORDION_BUTTON_ID + indexHeaderLisible,
                            [CONFIG.ATTR_CONTROLS]: CONFIG.ACCORDION_PREFIX_IDS + iLisible + CONFIG.ACCORDION_PANEL_ID + indexHeaderLisible,
                            [CONFIG.ATTR_SELECTED]: 'false',
                            'type': 'button',
                            [DATA_HASH_ID]: HASH_ID
                        });

                        // place button
                        header_node.innerHTML = '';
                        header_node.appendChild(accordionButton);

                        // move title into panel
                        //accordionPanel.insertBefore(header_node, accordionPanel.firstChild);
                        // set title with attributes
                        addClass(header_node, prefixClassName + CONFIG.ACCORDION_TITLE_STYLE);
                        removeClass(header_node, CONFIG.ACCORDION_JS_HEADER);

                        // set attributes to panels
                        addClass(accordionPanel, prefixClassName + CONFIG.ACCORDION_PANEL_STYLE);
                        setAttributes(accordionPanel, {
                            [CONFIG.ATTR_ROLE]: CONFIG.ACCORDION_ROLE_TABPANEL,
                            [CONFIG.ATTR_LABELLEDBY]: CONFIG.ACCORDION_PREFIX_IDS + iLisible + CONFIG.ACCORDION_BUTTON_ID + indexHeaderLisible,
                            'id': CONFIG.ACCORDION_PREFIX_IDS + iLisible + CONFIG.ACCORDION_PANEL_ID + indexHeaderLisible,
                            [DATA_HASH_ID]: HASH_ID
                        });
                               

                    });


            });


    };

    return {
        attach
        /*,
                destroy*/
    }
};

const main = () => {

    /* listeners for all configs */
    ['click', 'keydown', 'focus']
    .forEach(eventName => {

        document.body
            .addEventListener(eventName, e => {

                let hashId = searchParentHashId(e.target, DATA_HASH_ID); //e.target.dataset.hashId;
                // search if click on button or on element in a button contains data-hash-id (it is needed to load config and know which class to search)

                if (hashId !== '') {

                    // loading config from element
                    let CONFIG = pluginConfig.get(hashId);

                    // focus on button
                    if (hasClass(e.target, CONFIG.ACCORDION_JS_HEADER) === true && eventName === 'focus') {
                        let buttonTag = e.target;
                        let accordionContainer = findById(searchParent(buttonTag, CONFIG.ACCORDION_JS, hashId), hashId);
                        let coolSelectors = accordionContainer.hasAttribute(CONFIG.ACCORDION_DATA_COOL_SELECTORS) === true ? true : false;
                        let $accordionAllHeaders = [].slice.call(accordionContainer.querySelectorAll('.' + CONFIG.ACCORDION_JS_HEADER));

                        if (coolSelectors === false) {
                            $accordionAllHeaders = $accordionAllHeaders.filter(element => element.parentNode.parentNode === accordionContainer);
                        }

                        unSelectHeaders($accordionAllHeaders, CONFIG.ATTR_SELECTED);

                        selectHeader(buttonTag, CONFIG.ATTR_SELECTED);

                    }

                    // click on button
                    if (hasClass(e.target, CONFIG.ACCORDION_JS_HEADER) === true && eventName === 'click') {
                        let buttonTag = e.target;
                        let accordionContainer = findById(searchParent(buttonTag, CONFIG.ACCORDION_JS, hashId), hashId);
                        let coolSelectors = accordionContainer.hasAttribute(CONFIG.ACCORDION_DATA_COOL_SELECTORS) === true ? true : false;
                        let $accordionAllHeaders = [].slice.call(accordionContainer.querySelectorAll('.' + CONFIG.ACCORDION_JS_HEADER));
                        let accordionMultiSelectable = accordionContainer.getAttribute(CONFIG.ATTR_MULTISELECTABLE);
                        let destination = findById(buttonTag.getAttribute(CONFIG.ATTR_CONTROLS), hashId);
                        let stateButton = buttonTag.getAttribute(CONFIG.ATTR_EXPANDED);

                        if (coolSelectors === false) {
                            $accordionAllHeaders = $accordionAllHeaders.filter(element => element.parentNode.parentNode === accordionContainer);
                        }

                        // if closed
                        if (stateButton === 'false') {
                            buttonTag.setAttribute(CONFIG.ATTR_EXPANDED, true);
                            destination.removeAttribute(CONFIG.ATTR_HIDDEN);
                        } else {
                            buttonTag.setAttribute(CONFIG.ATTR_EXPANDED, false);
                            destination.setAttribute(CONFIG.ATTR_HIDDEN, true);
                        }

                        if (accordionMultiSelectable === 'false') {
                            $accordionAllHeaders
                                .forEach((header_node) => {

                                    let destinationPanel = findById(header_node.getAttribute(CONFIG.ATTR_CONTROLS), hashId);

                                    if (header_node !== buttonTag) {
                                        header_node.setAttribute(CONFIG.ATTR_SELECTED, false);
                                        header_node.setAttribute(CONFIG.ATTR_EXPANDED, false);
                                        destinationPanel.setAttribute(CONFIG.ATTR_HIDDEN, true);
                                    } else {
                                        header_node.setAttribute(CONFIG.ATTR_SELECTED, true);
                                    }
                                });

                        }

                        setTimeout(function() {
                            buttonTag.focus();
                        }, 0);
                        e.preventDefault();

                    }

                    // keyboard management for headers
                    if (hasClass(e.target, CONFIG.ACCORDION_JS_HEADER) === true && eventName === 'keydown') {
                        let buttonTag = e.target;
                        let idAccordionContainer = searchParent(buttonTag, CONFIG.ACCORDION_JS, hashId);
                        let accordionContainer = findById(idAccordionContainer, hashId);

                        let coolSelectors = accordionContainer.hasAttribute(CONFIG.ACCORDION_DATA_COOL_SELECTORS) === true ? true : false;
                        let $accordionAllHeaders = [].slice.call(accordionContainer.querySelectorAll('.' + CONFIG.ACCORDION_JS_HEADER));

                        if (coolSelectors === false) {
                            $accordionAllHeaders = $accordionAllHeaders.filter(element => element.parentNode.parentNode === accordionContainer);
                        }

                        // strike home on a tab => 1st tab
                        if (e.keyCode === 36) {
                            unSelectHeaders($accordionAllHeaders, CONFIG.ATTR_SELECTED);
                            selectHeader($accordionAllHeaders[0], CONFIG.ATTR_SELECTED);
                            setTimeout(function() {
                                $accordionAllHeaders[0].focus();
                            }, 0);
                            e.preventDefault();
                        }
                        // strike end on the tab => last tab
                        else if (e.keyCode === 35) {
                            unSelectHeaders($accordionAllHeaders, CONFIG.ATTR_SELECTED);
                            selectHeader($accordionAllHeaders[$accordionAllHeaders.length - 1], CONFIG.ATTR_SELECTED);
                            setTimeout(function() {
                                $accordionAllHeaders[$accordionAllHeaders.length - 1].focus();
                            }, 0);
                            e.preventDefault();
                        }
                        // strike up or left on the tab => previous tab
                        else if ((e.keyCode === 37 || e.keyCode === 38) && !e.ctrlKey) {

                            // if first selected = select last
                            if ($accordionAllHeaders[0].getAttribute(CONFIG.ATTR_SELECTED) === 'true') {
                                unSelectHeaders($accordionAllHeaders, CONFIG.ATTR_SELECTED);
                                selectHeader($accordionAllHeaders[$accordionAllHeaders.length - 1], CONFIG.ATTR_SELECTED);
                                setTimeout(function() {
                                    $accordionAllHeaders[$accordionAllHeaders.length - 1].focus();
                                }, 0);
                                e.preventDefault();
                            } else {
                                selectHeaderInList($accordionAllHeaders, 'prev', CONFIG.ATTR_SELECTED);
                                e.preventDefault();
                            }

                        }
                        // strike down or right in the tab => next tab
                        else if ((e.keyCode === 40 || e.keyCode === 39) && !e.ctrlKey) {

                            // if last selected = select first
                            if ($accordionAllHeaders[$accordionAllHeaders.length - 1].getAttribute(CONFIG.ATTR_SELECTED) === 'true') {
                                unSelectHeaders($accordionAllHeaders, CONFIG.ATTR_SELECTED);
                                selectHeader($accordionAllHeaders[0], CONFIG.ATTR_SELECTED);
                                setTimeout(function() {
                                    $accordionAllHeaders[0].focus();
                                }, 0);
                                e.preventDefault();
                            } else {
                                selectHeaderInList($accordionAllHeaders, 'next', CONFIG.ATTR_SELECTED);
                                e.preventDefault();
                            }

                        }
                    }



                }


            }, true);


    });

    return plugin;

};


window.van11yAccessibleAccordionAria = main();

const onLoad = () => {
    const expand_default = window.van11yAccessibleAccordionAria();
    expand_default.attach();

    document.removeEventListener('DOMContentLoaded', onLoad);
}

document.addEventListener('DOMContentLoaded', onLoad);
.footer-navigation_colunm {
    margin-bottom: 30px;
    width: 40%;
    float: left;
}
.minimalist-accordion__title {
        margin-bottom: 0 !important;
    }
    .js-accordion__header {
        color: #000;
        font: normal 400 11px OB-Medium,"Helvetica Neue",Helvetica,Arial,sans-serif;
        line-height: 20px;
        letter-spacing: 2px;
        margin: 15px 0 0;
        text-transform: uppercase;
        background: transparent;
        border: none;   
    }
    .footer-navigation_link {
        margin: 5px auto !important;
    }
  /* accordion */


  /* just for example as nested accordion */
  [data-accordion-prefix-classes="minimalist-css"] {
    margin-left: 3em;
  }

/*
  .minimalist-accordion__header::before,
  .minimalist-noanim-accordion__header::before,
  .minimalist-css-accordion__header::before {
    content: '';
    display: inline-block;
    width: 1.2em;
    height:1.2em;
    background-image: url(https://van11y.net/layout/images/icon-arrow-green_20180126.svg);
    background-repeat: no-repeat;
    background-position: 0 100%;
    margin-right: .25em;
    transform: rotate(0deg);
  }
*/
  .minimalist-css-accordion__header::before {
    background-size: 90%;
    width: .7em;
    height: .7em;
    -webkit-transition: transform .25s ease;
    transition: transform .25s ease;
  }

  [aria-expanded="true"].minimalist-accordion__header::before,
  [aria-expanded="true"].minimalist-noanim-accordion__header::before,
  [aria-expanded="true"].minimalist-css-accordion__header::before {
    transform: rotate(90deg);
    transform-origin: 50% 50%;
  }
/*
  .minimalist-accordion__header[aria-selected="true"]::after,
  .minimalist-noanim-accordion__header[aria-selected="true"]::after,
  .minimalist-css-accordion__header[aria-selected="true"]::after {
    content: "";
    position: relative;
    border-bottom: .4em solid transparent;
    border-top: .4em solid transparent;
    margin-left: .5em;
    top: .1em;
    border-left: .7em solid;
    display: inline-block;
    speak: none;
  }
*/
  /*.minimalist-accordion__title,
  .minimalist-noanim-accordion__title,
  .minimalist-css-accordion__title {
    border: 0;
    clip: rect(0 0 0 0);
    height: 1px;
    margin: -1px;
    overflow: hidden;
    padding: 0;
    position: absolute;
    width: 1px;
  }*/

  .minimalist-accordion__panel {
    display: block;
    overflow: hidden;
    opacity: 1;
    -webkit-transition: visibility 0s ease, max-height 1s ease, opacity 1s ease;
    transition: visibility 0s ease, max-height 1s ease, opacity 1s ease;
    max-height: 100em;
    /* magic number for max-height = enough height */
    visibility: visible;
    -webkit-transition-delay: 0s;
    transition-delay: 0s;
    margin: 0;
    padding: 0;
  }

  /* This is the hidden state */

  [aria-hidden=true].minimalist-accordion__panel {
    display: block;
    max-height: 0;
    opacity: 0;
    visibility: hidden;
    -webkit-transition-delay: 1s, 0s, 0s;
    transition-delay: 1s, 0s, 0s;
    margin: 0;
    padding: 0;
  }

  .minimalist-css-accordion__panel {
    display: block;
    overflow: hidden;
    opacity: 1;
    -webkit-transition: visibility 0s ease, max-height 1s ease, transform 1s ease, opacity 1s ease;
    transition: visibility 0s ease, max-height 1s ease, transform 1s ease, opacity 1s ease;
    transform: scaleY(1);
    max-height: 40em;
    /* magic number for max-height = enough height */
    visibility: visible;
    -webkit-transition-delay: 0s;
    transition-delay: 0s;
    margin: 0;
    padding: 0;
  }


  /* This is the hidden state */

  [aria-hidden=true].minimalist-css-accordion__panel {
    display: block;
    opacity: 0;
    transform: scaleY(0);
    max-height: 0;
    visibility: hidden;
    -webkit-transition-delay: 1s, 0s, 0s, 0s;
    transition-delay: 1s, 0s, 0s, 0s;
    margin: 0;
    padding: 0;
  }

  .minimalist-noanim-accordion__panel {
    display: block;
  }

  [aria-hidden=true].minimalist-noanim-accordion__panel {
    display: none;
  }
<div id="lp">


  <div class="js-accordion" data-accordion-prefix-classes="minimalist" data-accordion-cool-selectors="1">

    <div class="footer-navigation_colunm">
      <h2 class="js-accordion__header" data-accordion-opened="true">Accordion 1</h2>
       <div class="js-accordion__panel">

        <p class="footer-navigation_link">Yes, it rocks.</p>
        <p class="footer-navigation_link">Yes, it rocks.</p>
        <p class="footer-navigation_link">Yes, it rocks.</p>

       </div>
    </div>
    <div class="footer-navigation_colunm">
      <h2 class="js-accordion__header" data-accordion-opened="true">Accordion 2</h2>
       <div class="js-accordion__panel">

        <p class="footer-navigation_link">Yes, it rocks.</p>
        <p class="footer-navigation_link">Yes, it rocks.</p>
        <p class="footer-navigation_link">Yes, it rocks.</p>

       </div>
    </div>
    <div class="footer-navigation_colunm">
      <h2 class="js-accordion__header" data-accordion-opened="true">Accordion 3</h2>
       <div class="js-accordion__panel">

        <p class="footer-navigation_link">Yes, it rocks.</p>
        <p class="footer-navigation_link">Yes, it rocks.</p>
        <p class="footer-navigation_link">Yes, it rocks.</p>

       </div>
    </div>
    <div class="footer-navigation_colunm">
      <h2 class="js-accordion__header" data-accordion-opened="true">Accordion 4</h2>
       <div class="js-accordion__panel">

        <p class="footer-navigation_link">Yes, it rocks.</p>
        <p class="footer-navigation_link">Yes, it rocks.</p>
        <p class="footer-navigation_link">Yes, it rocks.</p>

       </div>
    </div>
   </div>
</div>
user3767554
  • 147
  • 1
  • 12
  • first, detect if you're on a mobile device [with something like this](https://stackoverflow.com/questions/11381673/detecting-a-mobile-browser). Then, conditionally apply some settings, functions, Jquery changes based on if the answer to that question is true or false. – TKoL Nov 15 '19 at 12:16

0 Answers0