You are viewing version 2 of the library, the latest version is

Select

<div class="md-form">
    <div class="aria-select">
        <button aria-haspopup="listbox" aria-labelledby="example-select-label example-select-button" id="example-select-button">
            Selecteer een optie
        </button>
        <label id="example-select-label" for="example-select-element">Voorbeeld dropdown</label>
        <ul tabindex="-1" role="listbox" aria-labelledby="example-select-element" class="aria-select-list hidden">
            <li id="example-select-element_1" data-value="1" role="option">
                Optie 1
            </li>
            <li id="example-select-element_2" data-value="2" role="option">
                Optie 2
            </li>
            <li id="example-select-element_3" data-value="3" role="option">
                Optie 3
            </li>
        </ul>
        <input type="hidden" id="example-select-element" name="example-select-element">
    </div>
</div>

Select

Based on the version from MDB:
https://mdbootstrap.com/javascript/material-select/#basic

Using

To start using this component, some JavaScript is needed to initialize it.
Underneath a jQuery example on how to achieve this and should be placed in the Additional component(s) script section as documented in How to use.

<script>
/**
 * ARIA Collapsible Dropdown Listbox
 *
 * Based on the https://www.w3.org/TR/wai-aria-practices-1.1/examples/listbox/listbox-collapsible.html Example
 *
 * @function onload
 * @desc Initialize all aria-select elements once the page has loaded
 */
window.addEventListener('load', function () {
    $.each($('.aria-select'), function(index) {
        var selectInput = $('.aria-select').eq(index);
        var input = selectInput.find('input')[0];
        var button = selectInput.find('button')[0];
        var listBox = new aria.Listbox(selectInput.find('ul')[0]);
        new aria.ListBoxButton(input, button, listBox);
    });
});

var aria = {};

aria.ListBoxButton = function (input, button, listBox) {
    this.input = input;
    this.button = button;
    this.listBox = listBox;
    this.registerEvents();
};

aria.ListBoxButton.prototype.registerEvents = function () {
    this.button.addEventListener('click', this.showListBox.bind(this));
    this.button.addEventListener('keyup', this.checkShow.bind(this));
    this.listBox.listBoxNode.addEventListener('blur', this.hideListBox.bind(this));
    this.listBox.listBoxNode.addEventListener('keydown', this.checkHide.bind(this));
    this.listBox.setHandleFocusChange(this.onFocusChange.bind(this));
};

aria.ListBoxButton.prototype.checkShow = function (event) {
    var key = event.which || event.keyCode;

    switch (key) {
        case aria.KeyCode.UP:
        case aria.KeyCode.DOWN:
            if (event) event.preventDefault();
            this.showListBox();
            this.listBox.checkKeyPress(event);
            break;
    }
};

aria.ListBoxButton.prototype.checkHide = function (event) {
    var key = event.which || event.keyCode;

    switch (key) {
        case aria.KeyCode.RETURN:
        case aria.KeyCode.ESC:
            if (event) event.preventDefault();
            this.hideListBox();
            this.button.focus();
            break;
    }
};

aria.ListBoxButton.prototype.showListBox = function (event) {
    aria.Utils.removeClass(this.listBox.listBoxNode, 'hidden');
    this.button.setAttribute('aria-expanded', 'true');
    $(this.listBox.listBoxNode).animate({
        opacity: 1,
        'max-height': '40.625rem'
    }, 800);
    this.listBox.listBoxNode.focus();
    if (event) event.preventDefault();
};

aria.ListBoxButton.prototype.hideListBox = function (event) {
    aria.Utils.addClass(this.listBox.listBoxNode, 'hidden');
    $(this.listBox.listBoxNode).animate({
        opacity: 0,
        'max-height': '0rem'
    });
    this.button.removeAttribute('aria-expanded');
    if (event) event.preventDefault();
};

aria.ListBoxButton.prototype.onFocusChange = function (focusedItem) {
    this.input.value = focusedItem.getAttribute('data-value');
    this.button.innerText = focusedItem.innerText;
};

/**
 * @constructor
 *
 * @desc
 *  Listbox object representing the state and interactions for a listBox widget
 *
 * @param listBoxNode
 *  The DOM node pointing to the listBox
 */
aria.Listbox = function (listBoxNode) {
    this.listBoxNode = listBoxNode;
    this.activeDescendant = this.listBoxNode.getAttribute('aria-activedescendant');
    this.multiselectable = this.listBoxNode.hasAttribute('aria-multiselectable');
    this.moveUpDownEnabled = false;
    this.siblingList = null;
    this.upButton = null;
    this.downButton = null;
    this.moveButton = null;
    this.keysSoFar = '';
    this.handleFocusChange = function () {};
    this.handleItemChange = function (event, items) {};
    this.registerEvents();
};

/**
 * @desc
 *  Register events for the listBox interactions
 */
aria.Listbox.prototype.registerEvents = function () {
    this.listBoxNode.addEventListener('focus', this.setupFocus.bind(this));
    this.listBoxNode.addEventListener('keydown', this.checkKeyPress.bind(this));
    this.listBoxNode.addEventListener('click', this.checkClickItem.bind(this));
};

/**
 * @desc
 *  If there is no activeDescendant, focus on the first option
 */
aria.Listbox.prototype.setupFocus = function () {
    if (this.activeDescendant) {
        return;
    }

    this.focusFirstItem();
};

/**
 * @desc
 *  Focus on the first option
 */
aria.Listbox.prototype.focusFirstItem = function () {
    var firstItem;

    firstItem = this.listBoxNode.querySelector('[role="option"]');

    if (firstItem) {
        this.focusItem(firstItem);
    }
};

/**
 * @desc
 *  Focus on the last option
 */
aria.Listbox.prototype.focusLastItem = function () {
    var itemList = this.listBoxNode.querySelectorAll('[role="option"]');

    if (itemList.length) {
        this.focusItem(itemList[itemList.length - 1]);
    }
};

/**
 * @desc
 *  Handle various keyboard controls; UP/DOWN will shift focus; SPACE selects
 *  an item.
 *
 * @param event
 *  The keydown event object
 */
aria.Listbox.prototype.checkKeyPress = function (event) {
    var key = event.which || event.keyCode;
    var nextItem = document.getElementById(this.activeDescendant);

    if (!nextItem) {
        return;
    }

    switch (key) {
        case aria.KeyCode.PAGE_UP:
        case aria.KeyCode.PAGE_DOWN:
            if (this.moveUpDownEnabled) {
                if (event) event.preventDefault();

                if (key === aria.KeyCode.PAGE_UP) {
                    this.moveUpItems();
                }
                else {
                    this.moveDownItems();
                }
            }

            break;
        case aria.KeyCode.UP:
        case aria.KeyCode.DOWN:
            if (event) event.preventDefault();

            if (this.moveUpDownEnabled && event.altKey) {
                if (key === aria.KeyCode.UP) {
                    this.moveUpItems();
                }
                else {
                    this.moveDownItems();
                }
                return;
            }

            if (key === aria.KeyCode.UP) {
                nextItem = nextItem.previousElementSibling;
            }
            else {
                nextItem = nextItem.nextElementSibling;
            }

            if (nextItem) {
                this.focusItem(nextItem);
            }

            break;
        case aria.KeyCode.HOME:
            if (event) event.preventDefault();
            this.focusFirstItem();
            break;
        case aria.KeyCode.END:
            if (event) event.preventDefault();
            this.focusLastItem();
            break;
        case aria.KeyCode.SPACE:
            if (event) event.preventDefault();
            this.toggleSelectItem(nextItem);
            break;
        case aria.KeyCode.BACKSPACE:
        case aria.KeyCode.DELETE:
        case aria.KeyCode.RETURN:
            if (!this.moveButton) {
                return;
            }

            var keyshortcuts = this.moveButton.getAttribute('aria-keyshortcuts');
            if (key === aria.KeyCode.RETURN && keyshortcuts.indexOf('Enter') === -1) {
                return;
            }
            if (
                    (key === aria.KeyCode.BACKSPACE || key === aria.KeyCode.DELETE) &&
                    keyshortcuts.indexOf('Delete') === -1
            ) {
                return;
            }

            if (event) event.preventDefault();

            var nextUnselected = nextItem.nextElementSibling;
            while (nextUnselected) {
                if (nextUnselected.getAttribute('aria-selected') != 'true') {
                    break;
                }
                nextUnselected = nextUnselected.nextElementSibling;
            }
            if (!nextUnselected) {
                nextUnselected = nextItem.previousElementSibling;
                while (nextUnselected) {
                    if (nextUnselected.getAttribute('aria-selected') != 'true') {
                        break;
                    }
                    nextUnselected = nextUnselected.previousElementSibling;
                }
            }

            this.moveItems();

            if (!this.activeDescendant && nextUnselected) {
                this.focusItem(nextUnselected);
            }
            break;
        default:
            var itemToFocus = this.findItemToFocus(key);
            if (itemToFocus) {
                this.focusItem(itemToFocus);
            }
            break;
    }
};

aria.Listbox.prototype.findItemToFocus = function (key) {
    var itemList = this.listBoxNode.querySelectorAll('[role="option"]');
    var character = String.fromCharCode(key);

    if (!this.keysSoFar) {
        for (var i = 0; i < itemList.length; i++) {
            if (itemList[i].getAttribute('id') == this.activeDescendant) {
                this.searchIndex = i;
            }
        }
    }
    this.keysSoFar += character;
    this.clearKeysSoFarAfterDelay();

    var nextMatch = this.findMatchInRange(
            itemList,
            this.searchIndex + 1,
            itemList.length
    );
    if (!nextMatch) {
        nextMatch = this.findMatchInRange(
                itemList,
                0,
                this.searchIndex
        );
    }
    return nextMatch;
};

aria.Listbox.prototype.clearKeysSoFarAfterDelay = function () {
    if (this.keyClear) {
        clearTimeout(this.keyClear);
        this.keyClear = null;
    }
    this.keyClear = setTimeout((function () {
        this.keysSoFar = '';
        this.keyClear = null;
    }).bind(this), 500);
};

aria.Listbox.prototype.findMatchInRange = function (list, startIndex, endIndex) {
    // Find the first item starting with the keysSoFar substring, searching in
    // the specified range of items
    for (var n = startIndex; n < endIndex; n++) {
        var label = list[n].innerText;
        if (label && label.toUpperCase().indexOf(this.keysSoFar) === 0) {
            return list[n];
        }
    }
    return null;
};

/**
 * @desc
 *  Check if an item is clicked on. If so, focus on it, select it and close the list box.
 *
 * @param event
 *  The click event object
 */
aria.Listbox.prototype.checkClickItem = function (event) {
    if (event.target.getAttribute('role') === 'option') {
        this.focusItem(event.target);
        this.toggleSelectItem(event.target);
        aria.Utils.addClass(this.listBoxNode, 'hidden');
    }
};

/**
 * @desc
 *  Toggle the aria-selected value
 *
 * @param element
 *  The element to select
 */
aria.Listbox.prototype.toggleSelectItem = function (element) {
    if (this.multiselectable) {
        element.setAttribute(
            'aria-selected',
            element.getAttribute('aria-selected') === 'true' ? 'false' : 'true'
        );

        if (this.moveButton) {
            if (this.listBoxNode.querySelector('[aria-selected="true"]')) {
                this.moveButton.setAttribute('aria-disabled', 'false');
            }
            else {
                this.moveButton.setAttribute('aria-disabled', 'true');
            }
        }
    }
};

/**
 * @desc
 *  Defocus the specified item
 *
 * @param element
 *  The element to defocus
 */
aria.Listbox.prototype.defocusItem = function (element) {
    if (!element) {
        return;
    }

    aria.Utils.removeClass(element, 'focused');
};

/**
 * @desc
 *  Focus on the specified item
 *
 * @param element
 *  The element to focus
 */
aria.Listbox.prototype.focusItem = function (element) {
    this.defocusItem(document.getElementById(this.activeDescendant));
    aria.Utils.addClass(element, 'focused');
    this.listBoxNode.setAttribute('aria-activedescendant', element.id);
    this.activeDescendant = element.id;

    if (this.listBoxNode.scrollHeight > this.listBoxNode.clientHeight) {
        var scrollBottom = this.listBoxNode.clientHeight + this.listBoxNode.scrollTop;
        var elementBottom = element.offsetTop + element.offsetHeight;
        if (elementBottom > scrollBottom) {
            this.listBoxNode.scrollTop = elementBottom - this.listBoxNode.clientHeight;
        }
        else if (element.offsetTop < this.listBoxNode.scrollTop) {
            this.listBoxNode.scrollTop = element.offsetTop;
        }
    }

    if (!this.multiselectable && this.moveButton) {
        this.moveButton.setAttribute('aria-disabled', false);
    }

    this.checkUpDownButtons();
    this.handleFocusChange(element);
};

/**
 * @desc
 *  Enable/disable the up/down arrows based on the activeDescendant.
 */
aria.Listbox.prototype.checkUpDownButtons = function () {
    var activeElement = document.getElementById(this.activeDescendant);

    if (!this.moveUpDownEnabled) {
        return false;
    }

    if (!activeElement) {
        this.upButton.setAttribute('aria-disabled', 'true');
        this.downButton.setAttribute('aria-disabled', 'true');
        return;
    }

    if (this.upButton) {
        if (activeElement.previousElementSibling) {
            this.upButton.setAttribute('aria-disabled', false);
        }
        else {
            this.upButton.setAttribute('aria-disabled', 'true');
        }
    }

    if (this.downButton) {
        if (activeElement.nextElementSibling) {
            this.downButton.setAttribute('aria-disabled', false);
        }
        else {
            this.downButton.setAttribute('aria-disabled', 'true');
        }
    }
};

/**
 * @desc
 *  Add the specified items to the listBox. Assumes items are valid options.
 *
 * @param items
 *  An array of items to add to the listBox
 */
aria.Listbox.prototype.addItems = function (items) {
    if (!items || !items.length) {
        return false;
    }

    items.forEach((function (item) {
        this.defocusItem(item);
        this.toggleSelectItem(item);
        this.listBoxNode.append(item);
    }).bind(this));

    if (!this.activeDescendant) {
        this.focusItem(items[0]);
    }

    this.handleItemChange('added', items);
};

/**
 * @desc
 *  Remove all of the selected items from the listBox; Removes the focused items
 *  in a single select listBox and the items with aria-selected in a multi
 *  select listBox.
 *
 * @returns items
 *  An array of items that were removed from the listBox
 */
aria.Listbox.prototype.deleteItems = function () {
    var itemsToDelete;

    if (this.multiselectable) {
        itemsToDelete = this.listBoxNode.querySelectorAll('[aria-selected="true"]');
    }
    else if (this.activeDescendant) {
        itemsToDelete = [ document.getElementById(this.activeDescendant) ];
    }

    if (!itemsToDelete || !itemsToDelete.length) {
        return [];
    }

    itemsToDelete.forEach((function (item) {
        item.remove();

        if (item.id === this.activeDescendant) {
            this.clearActiveDescendant();
        }
    }).bind(this));

    this.handleItemChange('removed', itemsToDelete);

    return itemsToDelete;
};

aria.Listbox.prototype.clearActiveDescendant = function () {
    this.activeDescendant = null;
    this.listBoxNode.setAttribute('aria-activedescendant', null);

    if (this.moveButton) {
        this.moveButton.setAttribute('aria-disabled', 'true');
    }

    this.checkUpDownButtons();
};

/**
 * @desc
 *  Shifts the currently focused item up on the list. No shifting occurs if the
 *  item is already at the top of the list.
 */
aria.Listbox.prototype.moveUpItems = function () {
    var previousItem;

    if (!this.activeDescendant) {
        return;
    }

    currentItem = document.getElementById(this.activeDescendant);
    previousItem = currentItem.previousElementSibling;

    if (previousItem) {
        this.listBoxNode.insertBefore(currentItem, previousItem);
        this.handleItemChange('moved_up', [ currentItem ]);
    }

    this.checkUpDownButtons();
};

/**
 * @desc
 *  Shifts the currently focused item down on the list. No shifting occurs if
 *  the item is already at the end of the list.
 */
aria.Listbox.prototype.moveDownItems = function () {
    var nextItem;

    if (!this.activeDescendant) {
        return;
    }

    currentItem = document.getElementById(this.activeDescendant);
    nextItem = currentItem.nextElementSibling;

    if (nextItem) {
        this.listBoxNode.insertBefore(nextItem, currentItem);
        this.handleItemChange('moved_down', [ currentItem ]);
    }

    this.checkUpDownButtons();
};

/**
 * @desc
 *  Delete the currently selected items and add them to the sibling list.
 */
aria.Listbox.prototype.moveItems = function () {
    if (!this.siblingList) {
        return;
    }

    var itemsToMove = this.deleteItems();
    this.siblingList.addItems(itemsToMove);
};

/**
 * @desc
 *  Enable Up/Down controls to shift items up and down.
 *
 * @param upButton
 *   Up button to trigger up shift
 *
 * @param downButton
 *   Down button to trigger down shift
 */
aria.Listbox.prototype.enableMoveUpDown = function (upButton, downButton) {
    this.moveUpDownEnabled = true;
    this.upButton = upButton;
    this.downButton = downButton;
    upButton.addEventListener('click', this.moveUpItems.bind(this));
    downButton.addEventListener('click', this.moveDownItems.bind(this));
};

/**
 * @desc
 *  Enable Move controls. Moving removes selected items from the current
 *  list and adds them to the sibling list.
 *
 * @param button
 *   Move button to trigger delete
 *
 * @param siblingList
 *   Listbox to move items to
 */
aria.Listbox.prototype.setupMove = function (button, siblingList) {
    this.siblingList = siblingList;
    this.moveButton = button;
    button.addEventListener('click', this.moveItems.bind(this));
};

aria.Listbox.prototype.setHandleItemChange = function (handlerFn) {
    this.handleItemChange = handlerFn;
};

aria.Listbox.prototype.setHandleFocusChange = function (focusChangeHandler) {
    this.handleFocusChange = focusChangeHandler;
};

/**
 * @desc
 *  Key code constants
 */
aria.KeyCode = {
    BACKSPACE: 8,
    TAB: 9,
    RETURN: 13,
    ESC: 27,
    SPACE: 32,
    PAGE_UP: 33,
    PAGE_DOWN: 34,
    END: 35,
    HOME: 36,
    LEFT: 37,
    UP: 38,
    RIGHT: 39,
    DOWN: 40,
    DELETE: 46
};

aria.Utils = aria.Utils || {};

// Polyfill src https://developer.mozilla.org/en-US/docs/Web/API/Element/matches
aria.Utils.matches = function (element, selector) {
    if (!Element.prototype.matches) {
        Element.prototype.matches =
                Element.prototype.matchesSelector ||
                Element.prototype.mozMatchesSelector ||
                Element.prototype.msMatchesSelector ||
                Element.prototype.oMatchesSelector ||
                Element.prototype.webkitMatchesSelector ||
                function (s) {
                    var matches = element.parentNode.querySelectorAll(s);
                    var i = matches.length;
                    while (--i >= 0 && matches.item(i) !== this) {}
                    return i > -1;
                };
    }

    return element.matches(selector);
};

aria.Utils.remove = function (item) {
    if (item.remove && typeof item.remove === 'function') {
        return item.remove();
    }
    if (item.parentNode &&
            item.parentNode.removeChild &&
            typeof item.parentNode.removeChild === 'function') {
        return item.parentNode.removeChild(item);
    }
    return false;
};

aria.Utils.isFocusable = function (element) {
    if (element.tabIndex > 0 || (element.tabIndex === 0 && element.getAttribute('tabIndex') !== null)) {
        return true;
    }

    if (element.disabled) {
        return false;
    }

    switch (element.nodeName) {
        case 'A':
            return !!element.href && element.rel != 'ignore';
        case 'INPUT':
            return element.type != 'hidden' && element.type != 'file';
        case 'BUTTON':
        case 'SELECT':
        case 'TEXTAREA':
            return true;
        default:
            return false;
    }
};

aria.Utils.getAncestorBySelector = function (element, selector) {
    if (!aria.Utils.matches(element, selector + ' ' + element.tagName)) {
        // Element is not inside an element that matches selector
        return null;
    }

    // Move up the DOM tree until a parent matching the selector is found
    var currentNode = element;
    var ancestor = null;
    while (ancestor === null) {
        if (aria.Utils.matches(currentNode.parentNode, selector)) {
            ancestor = currentNode.parentNode;
        }
        else {
            currentNode = currentNode.parentNode;
        }
    }

    return ancestor;
};

aria.Utils.hasClass = function (element, className) {
    return (new RegExp('(\\s|^)' + className + '(\\s|$)')).test(element.className);
};

aria.Utils.addClass = function (element, className) {
    if (!aria.Utils.hasClass(element, className)) {
        element.className += ' ' + className;
    }
};

aria.Utils.removeClass = function (element, className) {
    var classRegex = new RegExp('(\\s|^)' + className + '(\\s|$)');
    element.className = element.className.replace(classRegex, ' ').trim();
};

</script>

Notes

  • MDB framework defines that the input element comes prior to the label element