a11y-accordion-component
Version:
Accessible Accordion Component in Vanilla JS
313 lines (260 loc) • 12.4 kB
JavaScript
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Accordions = f()}})(function(){var define,module,exports;return (function(){function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s}return e})()({1:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
/* accordions
========================================================================== */
var Accordions = function () {
var DATA_COMPONENT = '[data-component="accordion"]';
var KEY_CODES = {
enter: 13,
space: 32,
pageUp: 33,
pageDown: 34,
end: 35,
home: 36,
up: 38,
down: 40
};
var Accordion = function () {
function Accordion(options) {
_classCallCheck(this, Accordion);
this.accordion = options.accordion;
this.triggers = this.queryFilter(this.accordion.querySelectorAll('[data-controls]'));
var _triggers = _slicedToArray(this.triggers, 1);
this.firstTrigger = _triggers[0];
this.lastTrigger = this.triggers[this.triggers.length - 1];
this.state = [];
this.currentFocusedIndex = null;
this.isMultiSelectable = options.isMultiSelectable;
this.isCollapsible = options.isCollapsible;
this.onFocus = this.onFocus.bind(this);
this.onClick = this.onClick.bind(this);
this.onKeydown = this.onKeydown.bind(this);
}
_createClass(Accordion, [{
key: 'queryFilter',
value: function queryFilter(selectors) {
var _this = this;
var elements = [];
var parent = void 0;
selectors.forEach(function (selector) {
parent = selector.parentNode;
while (parent !== _this.accordion) {
if (parent.dataset.component === _this.accordion.dataset.component) return;
parent = parent.parentNode;
}
elements.push(selector);
});
return elements;
}
}, {
key: 'setState',
value: function setState(event) {
var _this2 = this;
this.state.forEach(function (section) {
if (event.currentTarget === section.trigger) {
if (_this2.isCollapsible) section.isExpanded = !section.isExpanded;else {
section.isDisabled = true;
section.isExpanded = true;
}
} else {
if (!_this2.isMultiSelectable) section.isExpanded = false;
if (!_this2.isCollapsible) section.isDisabled = false;
}
});
// update attributes
this.updateAttributes(event);
}
}, {
key: 'onFocus',
value: function onFocus(event) {
var _this3 = this;
this.state.forEach(function (section, index) {
if (event.target === section.trigger) {
_this3.currentFocusedIndex = index;
}
});
}
}, {
key: 'setFocus',
value: function setFocus(event) {
if (event.target.hasAttribute('data-controls')) {
event.preventDefault();
event.stopPropagation();
if (event.which === KEY_CODES.up || event.which === KEY_CODES.pageUp) {
this.state[this.currentFocusedIndex].prevTrigger.focus();
}
if (event.which === KEY_CODES.down || event.which === KEY_CODES.pageDown) {
this.state[this.currentFocusedIndex].nextTrigger.focus();
}
if (event.which === KEY_CODES.home) this.firstTrigger.focus();
if (event.which === KEY_CODES.end) this.lastTrigger.focus();
} else {
if (event.which === KEY_CODES.pageUp || event.which === KEY_CODES.pageDown) {
event.preventDefault();
event.stopPropagation();
}
if (event.which === KEY_CODES.pageUp) this.state[this.currentFocusedIndex].trigger.focus();
if (event.which === KEY_CODES.pageDown) this.state[this.currentFocusedIndex].nextTrigger.focus();
}
}
}, {
key: 'addAttributes',
value: function addAttributes() {
this.accordion.setAttribute('role', 'presentation');
this.state.forEach(function (section) {
section.trigger.setAttribute('role', 'button');
section.trigger.setAttribute('tabindex', 0);
section.trigger.setAttribute('aria-controls', section.trigger.dataset.controls);
section.panel.setAttribute('role', 'region');
section.panel.setAttribute('tabindex', -1);
section.panel.setAttribute('aria-labelledby', section.trigger.id);
});
}
}, {
key: 'updateAttributes',
value: function updateAttributes(event) {
if (event) event.preventDefault();
this.state.forEach(function (section) {
section.trigger.setAttribute('aria-expanded', section.isExpanded);
section.trigger.setAttribute('aria-disabled', section.isDisabled);
section.panel.setAttribute('aria-hidden', !section.isExpanded);
});
}
}, {
key: 'removeAttributes',
value: function removeAttributes() {
delete this.accordion.dataset.component;
this.accordion.removeAttribute('role');
this.state.forEach(function (section) {
section.trigger.removeAttribute('role');
section.trigger.removeAttribute('tabindex');
section.trigger.removeAttribute('aria-controls');
section.trigger.removeAttribute('aria-expanded');
section.trigger.removeAttribute('aria-disabled');
section.panel.removeAttribute('role');
section.panel.removeAttribute('tabindex');
section.panel.removeAttribute('aria-hidden');
section.panel.removeAttribute('aria-labelledby');
});
}
}, {
key: 'onClick',
value: function onClick(event) {
this.setState(event);
}
}, {
key: 'onKeydown',
value: function onKeydown(event) {
if (event.which === KEY_CODES.enter && event.target.hasAttribute('data-controls')) this.setState(event);
if (event.which === KEY_CODES.space && event.target.hasAttribute('data-controls')) this.setState(event);
if (event.which === KEY_CODES.up) this.setFocus(event);
if (event.which === KEY_CODES.down) this.setFocus(event);
if (event.which === KEY_CODES.home) this.setFocus(event);
if (event.which === KEY_CODES.end) this.setFocus(event);
if (event.which === KEY_CODES.pageUp) this.setFocus(event);
if (event.which === KEY_CODES.pageDown) this.setFocus(event);
}
}, {
key: 'addEventListeners',
value: function addEventListeners(trigger, panel) {
trigger.addEventListener('focus', this.onFocus);
trigger.addEventListener('click', this.onClick);
trigger.addEventListener('keydown', this.onKeydown);
panel.addEventListener('keydown', this.onKeydown);
}
}, {
key: 'removeEventListeners',
value: function removeEventListeners(trigger, panel) {
trigger.removeEventListener('focus', this.onFocus);
trigger.removeEventListener('click', this.onClick);
trigger.removeEventListener('keydown', this.onKeydown);
panel.removeEventListener('keydown', this.onKeydown);
}
}, {
key: 'destroy',
value: function destroy() {
var _this4 = this;
this.state.forEach(function (section) {
// remove event listeners
_this4.removeEventListeners(section.trigger, section.panel);
});
// remove attributes
this.removeAttributes();
}
}, {
key: 'render',
value: function render() {
var _this5 = this;
var panel = void 0;
var isExpanded = void 0;
this.triggers.forEach(function (trigger, index) {
panel = document.getElementById(trigger.dataset.controls);
isExpanded = trigger.dataset.open === 'true';
if (isExpanded) _this5.currentFocusedIndex = index;
_this5.state.push({
trigger: trigger,
prevTrigger: _this5.triggers[index - 1] || _this5.lastTrigger,
nextTrigger: _this5.triggers[index + 1] || _this5.firstTrigger,
panel: panel,
isExpanded: isExpanded,
isDisabled: !_this5.isCollapsible ? isExpanded : false
});
// add event listeners
_this5.addEventListeners(trigger, panel);
});
// add attributes
this.addAttributes();
// update attributes
this.updateAttributes();
}
}]);
return Accordion;
}();
// save all active accordions
var activeAccordions = [];
var render = function render(accordionId) {
var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
_ref$isMultiSelectabl = _ref.isMultiSelectable,
isMultiSelectable = _ref$isMultiSelectabl === undefined ? true : _ref$isMultiSelectabl,
_ref$isCollapsible = _ref.isCollapsible,
isCollapsible = _ref$isCollapsible === undefined ? true : _ref$isCollapsible;
var accordion = document.getElementById(accordionId);
var options = { accordion: accordion, isMultiSelectable: isMultiSelectable, isCollapsible: isCollapsible };
// add data component
accordion.dataset.component = 'accordion';
var activeAccordion = new Accordion(options);
activeAccordion.render();
// add active accordion to array
activeAccordions.push(activeAccordion);
};
var destroy = function destroy(accordionId) {
activeAccordions.forEach(function (activeAccordion, index) {
if (accordionId === activeAccordion.accordion.id) {
activeAccordion.destroy();
// remove accordion from array
activeAccordions.splice(index, 1);
}
});
};
var init = function init() {
var components = document.querySelectorAll(DATA_COMPONENT);
var options = {};
components.forEach(function (component) {
options.accordion = component;
options.isMultiSelectable = component.dataset.multiselectable !== 'false';
options.isCollapsible = component.dataset.collapsible !== 'false';
var accordion = new Accordion(options);
accordion.render();
});
};
return { render: render, destroy: destroy, init: init };
}();
exports.default = Accordions;
},{}]},{},[1])(1)
});