Required: The complete JavaScript file (for all patterns in the library): deque-patterns.min.js
Key parts of the JavaScript file
Note: The code below is functional only in the context of the complete JavaScript file.
In the @checkbox section:
/* TO DO: - Throw an error if the label is missing */ function toggle(element) { if (isToggledOn(element)) { toggleOff(element); } else { toggleOn(element); } } function isToggledOn(element) { return getCheckboxData(element) === 'true'; } function replaceSpace(str) { return str.replace(/ /g, '_').toLowerCase(); } function buildCheckboxTristate() { var _customCheckboxTristateWidgets = document.querySelectorAll('.custom-checkbox-widget'); var _instanceChkTristateCount = 0; if (_customCheckboxTristateWidgets.length > 0) { [].slice.call(_customCheckboxTristateWidgets).forEach(function (_eachCustomWidget) { var _dataConfig = { groupTitle: _eachCustomWidget.getAttribute('data-group-title'), groupOptionTitle: _eachCustomWidget.getAttribute('data-group-option-title'), options: _eachCustomWidget.getAttribute('data-options'), delimiter: _eachCustomWidget.getAttribute('data-delimiter') || ',' }; if (_dataConfig.options) { _dataConfig.options = _dataConfig.options.split(_dataConfig.delimiter); } var _id = 'instance_' + _instanceChkTristateCount + '_' + replaceSpace(_dataConfig.groupTitle); var _elementControl = '<div class="custom-checkbox" id="' + _id + '" role="group" aria-labelledby="group-header' + _id + '">'; _elementControl += '<div class="checkbox-group-heading" id="group-header' + _id + '">' + _dataConfig.groupTitle + '</div>'; _elementControl += '<div role="checkbox" name="parent-checkbox[]" data-childs="' + _id + '_childs" class="parent-checkbox checkbox-holder" \ aria-labelledby="' + _id + replaceSpace(_dataConfig.groupOptionTitle) + '" tabindex="0"> \ <span class="checkbox-indicator"></span> \ <span class="checkbox-label" id="' + _id + replaceSpace(_dataConfig.groupOptionTitle) + '">' + _dataConfig.groupOptionTitle + '</span> \ </div> \ <div class="child-checkbox-list" data-group="' + _id + '">'; for (var _elementOptionIndex in _dataConfig.options) { _elementControl += '<div role="checkbox" name="child-checkbox[]" class="child-checkbox checkbox-holder ' + _id + '_childs" aria-labelledby="' + _id + replaceSpace(_dataConfig.options[_elementOptionIndex]) + '" tabindex="0"> \ <span class="checkbox-indicator"></span> \ <span class="checkbox-label" id="' + _id + replaceSpace(_dataConfig.options[_elementOptionIndex]) + '">' + _dataConfig.options[_elementOptionIndex] + '</span> \ </div>'; } _elementControl += '</div></div>'; _eachCustomWidget.innerHTML = _elementControl; _instanceChkTristateCount++; }); var parentCheckboxElements = document.querySelectorAll('.parent-checkbox'); [].slice.call(parentCheckboxElements).forEach(function (eachParent) { eachParent.addEventListener('click', checkboxEvent); eachParent.addEventListener('keyup', checkboxEvent); }); var childElements = document.querySelectorAll('.child-checkbox'); [].slice.call(childElements).forEach(function (eachChildElement) { eachChildElement.addEventListener('click', checkboxEvent); eachChildElement.addEventListener('keyup', checkboxEvent); }); } } buildCheckboxTristate(); function checkboxEvent(event) { if (event.keyCode == 13 || event.keyCode == 32 || !event.keyCode) { var element = event.currentTarget; if (element.getAttribute('aria-checked') == 'true') { element.setAttribute('aria-checked', 'false'); element.classList.remove('active'); } else { element.setAttribute('aria-checked', 'true'); element.classList.add('active'); } if (element.getAttribute('data-childs')) { var childElements = document.querySelectorAll('.' + element.getAttribute('data-childs')); [].slice.call(childElements).forEach(function (eachChildElement) { if (element.classList.contains('parent-checkbox')) { eachChildElement.setAttribute('aria-checked', element.getAttribute('aria-checked')); } }); } var parentElement = document.querySelector('#' + element.parentElement.getAttribute('data-group')); if (parentElement) { var innerParent = parentElement.querySelector('.parent-checkbox'); if (innerParent) { var checkboxCounter = parentElement.querySelectorAll('.child-checkbox[aria-checked="true"]').length; var _childElements = parentElement.querySelectorAll('.child-checkbox'); if (checkboxCounter == 0) { innerParent.setAttribute('aria-checked', 'false'); } else if (checkboxCounter < _childElements.length) { innerParent.setAttribute('aria-checked', 'mixed'); } else if (checkboxCounter == _childElements.length) { innerParent.setAttribute('aria-checked', 'true'); } } } } } function setCheckboxData(element, value) { element.setAttribute('aria-checked', value); var dataElement = document.getElementById('checkboxTristateData[' + element.getAttribute('aria-labelledby') + ']'); if (dataElement) dataElement.value = value; } function getCheckboxData(element) { var dataElement = document.getElementById('checkboxTristateData[' + element.getAttribute('aria-labelledby') + ']'); return dataElement ? dataElement.value : null; } function toggleOn(element) { setCheckboxData(element, 'true'); } function toggleOff(element) { setCheckboxData(element, 'false'); } function toggleMixed(element) { setCheckboxData(element, 'mixed'); } function createSingleCheckbox(checkbox, isChecked) { var onChange = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : function () { }; checkbox.setAttribute('tabindex', '0'); checkbox.setAttribute('role', 'checkbox'); var indicator = document.createElement('span'); indicator.classList.add('deque-checkbox-indicator'); checkbox.appendChild(indicator); var labelText = checkbox.getAttribute('aria-labelledby'); var label = document.getElementById(labelText); //label.setAttribute('aria-hidden', 'true'); // prevents double readout label.classList.add('deque-checkbox-label'); var hiddenCheckbox = document.createElement('input'); hiddenCheckbox.type = 'hidden'; hiddenCheckbox.name = 'checkboxTristateData[' + labelText + ']'; hiddenCheckbox.id = 'checkboxTristateData[' + labelText + ']'; hiddenCheckbox.classList.add('deque-checkbox-data'); checkbox.appendChild(hiddenCheckbox); /*checkbox.addEventListener('focus', function () { var allCheckboxElements = document.querySelectorAll('.deque-checkbox-tristate-parent'); [].slice.call(allCheckboxElements).forEach(element => { element.setAttribute('aria-hidden', 'true'); }); });*/ if (isChecked) { toggleOn(checkbox); } else { toggleOff(checkbox); } function changeHandler(e) { e.stopPropagation(); e.preventDefault(); toggle(checkbox); broadcastChange(); } function broadcastChange() { onChange({ element: checkbox, isToggledOn: isToggledOn(label) }); } checkbox.parentNode.addEventListener('click', changeHandler); (0, _keyboardUtils.onElementSpace)(checkbox, changeHandler); (0, _keyboardUtils.onElementEnter)(checkbox, changeHandler); checkbox.parentNode.addEventListener('focus', function () { checkbox.classList.add('deque-checkbox-focused'); }); checkbox.parentNode.addEventListener('blur', function () { checkbox.classList.remove('deque-checkbox-focused'); }); return checkbox; } function createSingleCheckboxForRadio(checkbox, checkboxLabel, isChecked) { var onChange = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : function () { }; checkbox.setAttribute('tabindex', '0'); checkbox.setAttribute('role', 'checkbox'); var indicator = document.createElement('span'); indicator.classList.add('deque-checkbox-indicator'); checkbox.appendChild(indicator); var labelText = checkbox.getAttribute('aria-labelledby'); var label = document.getElementById(labelText); //label.setAttribute('aria-hidden', 'true'); // prevents double readout label.classList.add('deque-checkbox-label'); var hiddenRadio = document.createElement('input'); hiddenRadio.type = 'hidden'; hiddenRadio.name = 'checkboxTristateData[' + labelText + ']'; hiddenRadio.id = 'checkboxTristateData[' + labelText + ']'; hiddenRadio.classList.add('deque-checkbox-radio-data'); checkbox.appendChild(hiddenRadio); checkbox.appendChild(label); if (isChecked) { toggleOn(checkbox); } else { toggleOff(checkbox); } function changeHandler(e) { e.stopPropagation(); e.preventDefault(); toggle(checkbox); broadcastChange(); } function broadcastChange() { onChange({ element: checkbox, isToggledOn: isToggledOn(label) }); } checkbox.addEventListener('click', changeHandler); checkbox.addEventListener('keydown', changeHandler); checkboxLabel.addEventListener('click', changeHandler); (0, _keyboardUtils.onElementSpace)(checkbox, changeHandler); checkbox.addEventListener('focus', function () { checkbox.classList.add('deque-radio-focused'); }); checkbox.addEventListener('blur', function () { checkbox.classList.remove('deque-radio-focused'); }); return checkbox; } function createCheckboxGroup(parent, children) { var onChange = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : function () { }; parent = createSingleCheckbox(parent, false, function (e) { onChange(e); rootClicked(getCorrectRootState()); }); children = Array.prototype.slice.call(children); children = children.map(function (child) { return createSingleCheckbox(child, false, function () { if (onChange) { onChange(child); } setCorrectRootState(); }); }); var rootClickHandlers = { 'true': function _true() { children.forEach(toggleOff); toggleOff(parent); }, 'false': function _false() { children.forEach(toggleOn); toggleOn(parent); }, 'mixed': function mixed() { children.forEach(toggleOn); toggleOn(parent); } }; function rootClicked(rootState) { rootClickHandlers[rootState](); } function getCorrectRootState() { if (children.every(isToggledOn)) { return 'true'; } else if (children.every(function (child) { return !isToggledOn(child); })) { return 'false'; } else { return 'mixed'; } } var leafClickHandlers = { 'true': function _true() { return toggleOn(parent); }, 'false': function _false() { return toggleOff(parent); }, 'mixed': function mixed() { return toggleMixed(parent); } }; function setCorrectRootState() { leafClickHandlers[getCorrectRootState()](); } } function activateAllCheckboxes() { var checkboxes = document.querySelectorAll('.deque-checkbox-aria'); for (var i = 0; i < checkboxes.length; i++) { var childNode = checkboxes[i].querySelector('.deque-checkbox-data'); if (!checkboxes[i].contains(childNode)) { createSingleCheckbox(checkboxes[i], false); } } var tristates = document.querySelectorAll('.deque-checkbox-tristate-group'); for (var j = 0; j < tristates.length; j++) { var parentGroup = tristates[j].querySelector('.deque-checkbox-tristate-parent'); var parent = parentGroup.querySelector('.deque-checkbox-tristate'); var childrenGroup = tristates[j].querySelector('.deque-checkbox-tristate-children'); var children = childrenGroup.querySelectorAll('.deque-checkbox-tristate'); childNode = childrenGroup.querySelector('.deque-checkbox-data'); if (!childrenGroup.contains(childNode)) { createCheckboxGroup(parent, children); } } } activateAllCheckboxes();
In the @keyboardUtils section:
var KEYS = exports.KEYS = { BACKSPACE: 8, TAB: 9, ENTER: 13, SHIFT: 16, CTRL: 17, ALT: 18, ESCAPE: 27, SPACE: 32, LEFT: 37, RIGHT: 39, UP: 38, DOWN: 40, F10: 121, HOME: 36, END: 35, PAGE_UP: 33, PAGE_DOWN: 34 }; function bindElementToEventValue(element, eventName, testValue, handler) { function localHandler(e) { if (e.which === testValue) { handler(e); } } element.addEventListener(eventName, localHandler); return function () { element.removeEventListener(eventName, localHandler); }; } function bindElementToKeypressValue(element, testValue, handler) { return bindElementToEventValue(element, 'keypress', testValue, handler); } function bindElementToKeydownValue(element, testValue, handler) { return bindElementToEventValue(element, 'keydown', testValue, handler); } function onElementEnter(element, handler) { return bindElementToKeydownValue(element, KEYS.ENTER, handler); } function onElementEscape(element, handler) { return bindElementToKeydownValue(element, KEYS.ESCAPE, handler); } function onElementSpace(element, handler) { return bindElementToKeypressValue(element, KEYS.SPACE, handler); } function onElementLeft(element, handler) { return bindElementToKeydownValue(element, KEYS.LEFT, handler); } function onElementRight(element, handler) { return bindElementToKeydownValue(element, KEYS.RIGHT, handler); } function onElementUp(element, handler) { return bindElementToKeydownValue(element, KEYS.UP, handler); } function onElementDown(element, handler) { return bindElementToKeydownValue(element, KEYS.DOWN, handler); } function onElementHome(element, handler) { return bindElementToKeydownValue(element, KEYS.HOME, handler); } function onElementEnd(element, handler) { return bindElementToKeydownValue(element, KEYS.END, handler); } function onElementPageUp(element, handler) { return bindElementToKeydownValue(element, KEYS.PAGE_UP, handler); } function onElementPageDown(element, handler) { return bindElementToKeydownValue(element, KEYS.PAGE_DOWN, handler); } function onElementF10(element, handler) { return bindElementToKeydownValue(element, KEYS.F10, handler); } function isAlphaNumeric(charCode) { return charCode >= 48 && charCode <= 90 /* numbers, uppercase letters */ || charCode >= 97 && charCode <= 122 /* lowercase letters */; } function onElementCharacter(element, handler) { function localHandler(e) { var charCode = e.which; if (isAlphaNumeric(charCode)) { handler(e); } } element.addEventListener('keypress', localHandler); return function () { element.removeEventListener('keypress', localHandler); }; } function trapEnter(element) { onElementEnter(element, function (e) { e.stopPropagation(); e.preventDefault(); }); }
In the @guidUtils section:
/* note - not a true guid. I prepend 'g' because the ID of an element cannot start with a numeral */ function generateGuid() { var S4 = function S4() { return ((1 + Math.random()) * 0x10000 | 0).toString(16).substring(1); }; return 'g' + (S4() + S4() + '-' + S4() + '-' + S4() + '-' + S4() + '-' + S4() + S4() + S4()); }
In the @containerUtils section:
function elementIsChildOfElement(child, potentialParent) { while (child) { if (child === potentialParent) { return true; } child = child.parentNode; } return false; } function createFieldset(label) { var fieldset = document.createElement('fieldset'); var legend = document.createElement('legend'); legend.classList.add('legend'); // for easy lookup regardless of mode legend.id = (0, _guidUtils.generateGuid)(); legend.innerText = label; fieldset.appendChild(legend); return fieldset; } function createLiveRegion() { var level = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'polite'; var classes = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; var output = document.createElement('span'); classes.forEach(function (c) { return output.classList.add(c); }); output.id = (0, _guidUtils.generateGuid)(); output.setAttribute('aria-live', level); output.classList.add('deque-visuallyhidden'); output.innerText = ''; output.notify = function (text) { // TODO: Clean this up...no need to extend the element prototype while (output.firstChild) { output.removeChild(output.firstChild); } var msg = document.createElement('div'); msg.innerHTML = text; output.appendChild(msg); }; return output; }
Note: No additional JavaScript initialization code is necessary for this pattern. All elements with class="deque-checkbox-tristate-group"
will be initialized automatically by the external JavaScript file.