diff --git a/dist/index.esm.js b/dist/index.esm.js index 902cf93..bfe9149 100644 --- a/dist/index.esm.js +++ b/dist/index.esm.js @@ -1,3 +1,5 @@ +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 _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } @@ -179,13 +181,17 @@ var Topic = function () { return topics; } + var children = Array.from(list.children); + var _iteratorNormalCompletion2 = true; var _didIteratorError2 = false; var _iteratorError2 = undefined; try { - for (var _iterator2 = list.children[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { - var item = _step2.value; + for (var _iterator2 = children.entries()[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { + var _step2$value = _slicedToArray(_step2.value, 2), + index = _step2$value[0], + item = _step2$value[1]; var label = item.querySelector('label'); var checkbox = item.querySelector('input'); @@ -193,7 +199,14 @@ var Topic = function () { var childList = item.querySelector('ul'); childList = childList instanceof HTMLUListElement ? childList : null; - var topic = new Topic(label, checkbox, childList, parent); + var previous = index > 0 ? topics[index - 1] : null; + + var topic = new Topic(label, checkbox, childList, parent, previous); + + if (index > 0) { + topics[index - 1].next = topic; + } + topics.push(topic); } } @@ -221,13 +234,14 @@ var Topic = function () { }]); - function Topic(label, checkbox, childList, parent) { + function Topic(label, checkbox, childList, parent, previous) { _classCallCheck(this, Topic); this.label = label; this.checkbox = checkbox; this.parent = parent; this.children = Topic.fromList(childList, this); + this.previous = previous; if (this.checkbox.checked) { this.select(); @@ -611,6 +625,19 @@ var MillerColumnsElement = function (_CustomElement2) { } } + /** Focus the miller columns item associated with a topic */ + + }, { + key: 'focusTopic', + value: function focusTopic(topic) { + if (topic instanceof Topic && topic.checkbox) { + var item = topic.checkbox.closest('.' + this.classNames.item); + if (item instanceof HTMLElement) { + item.focus(); + } + } + } + /** Sets up the event handling for a list item and a topic */ }, { @@ -624,10 +651,43 @@ var MillerColumnsElement = function (_CustomElement2) { topic.checkbox.dispatchEvent(new Event('click')); }, false); trigger.addEventListener('keydown', function (event) { - if ([' ', 'Enter'].indexOf(event.key) !== -1) { - event.preventDefault(); - _this3.taxonomy.topicClicked(topic); - topic.checkbox.dispatchEvent(new Event('click')); + switch (event.key) { + case ' ': + case 'Enter': + event.preventDefault(); + _this3.taxonomy.topicClicked(topic); + topic.checkbox.dispatchEvent(new Event('click')); + break; + case 'ArrowUp': + event.preventDefault(); + if (topic.previous) { + _this3.showCurrentColumns(topic.previous); + _this3.focusTopic(topic.previous); + } + break; + case 'ArrowDown': + event.preventDefault(); + if (topic.next) { + _this3.showCurrentColumns(topic.next); + _this3.focusTopic(topic.next); + } + break; + case 'ArrowLeft': + event.preventDefault(); + if (topic.parent) { + _this3.showCurrentColumns(topic.parent); + _this3.focusTopic(topic.parent); + } + break; + case 'ArrowRight': + event.preventDefault(); + if (topic.children) { + _this3.showCurrentColumns(topic.children[0]); + _this3.focusTopic(topic.children[0]); + } + break; + default: + return; } }, false); } diff --git a/dist/index.umd.js b/dist/index.umd.js index 2de859d..e5a83fa 100644 --- a/dist/index.umd.js +++ b/dist/index.umd.js @@ -49,6 +49,44 @@ Object.setPrototypeOf(_CustomElement.prototype, HTMLElement.prototype); Object.setPrototypeOf(_CustomElement, HTMLElement); + 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"); + } + }; + }(); + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); @@ -223,13 +261,17 @@ return topics; } + var children = Array.from(list.children); + var _iteratorNormalCompletion2 = true; var _didIteratorError2 = false; var _iteratorError2 = undefined; try { - for (var _iterator2 = list.children[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { - var item = _step2.value; + for (var _iterator2 = children.entries()[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { + var _step2$value = _slicedToArray(_step2.value, 2), + index = _step2$value[0], + item = _step2$value[1]; var label = item.querySelector('label'); var checkbox = item.querySelector('input'); @@ -237,7 +279,14 @@ var childList = item.querySelector('ul'); childList = childList instanceof HTMLUListElement ? childList : null; - var topic = new Topic(label, checkbox, childList, parent); + var previous = index > 0 ? topics[index - 1] : null; + + var topic = new Topic(label, checkbox, childList, parent, previous); + + if (index > 0) { + topics[index - 1].next = topic; + } + topics.push(topic); } } @@ -260,13 +309,14 @@ } }]); - function Topic(label, checkbox, childList, parent) { + function Topic(label, checkbox, childList, parent, previous) { _classCallCheck(this, Topic); this.label = label; this.checkbox = checkbox; this.parent = parent; this.children = Topic.fromList(childList, this); + this.previous = previous; if (this.checkbox.checked) { this.select(); @@ -619,6 +669,16 @@ this.renderTaxonomyColumn(topic.children); } } + }, { + key: 'focusTopic', + value: function focusTopic(topic) { + if (topic instanceof Topic && topic.checkbox) { + var item = topic.checkbox.closest('.' + this.classNames.item); + if (item instanceof HTMLElement) { + item.focus(); + } + } + } }, { key: 'attachEvents', value: function attachEvents(trigger, topic) { @@ -630,10 +690,43 @@ topic.checkbox.dispatchEvent(new Event('click')); }, false); trigger.addEventListener('keydown', function (event) { - if ([' ', 'Enter'].indexOf(event.key) !== -1) { - event.preventDefault(); - _this3.taxonomy.topicClicked(topic); - topic.checkbox.dispatchEvent(new Event('click')); + switch (event.key) { + case ' ': + case 'Enter': + event.preventDefault(); + _this3.taxonomy.topicClicked(topic); + topic.checkbox.dispatchEvent(new Event('click')); + break; + case 'ArrowUp': + event.preventDefault(); + if (topic.previous) { + _this3.showCurrentColumns(topic.previous); + _this3.focusTopic(topic.previous); + } + break; + case 'ArrowDown': + event.preventDefault(); + if (topic.next) { + _this3.showCurrentColumns(topic.next); + _this3.focusTopic(topic.next); + } + break; + case 'ArrowLeft': + event.preventDefault(); + if (topic.parent) { + _this3.showCurrentColumns(topic.parent); + _this3.focusTopic(topic.parent); + } + break; + case 'ArrowRight': + event.preventDefault(); + if (topic.children) { + _this3.showCurrentColumns(topic.children[0]); + _this3.focusTopic(topic.children[0]); + } + break; + default: + return; } }, false); } diff --git a/index.js b/index.js index 2099be0..8123289 100644 --- a/index.js +++ b/index.js @@ -120,14 +120,23 @@ class Topic { return topics } - for (const item of list.children) { + const children = Array.from(list.children) + + for (const [index, item] of children.entries()) { const label = item.querySelector('label') const checkbox = item.querySelector('input') if (label instanceof HTMLLabelElement && checkbox instanceof HTMLInputElement) { let childList = item.querySelector('ul') childList = childList instanceof HTMLUListElement ? childList : null - const topic = new Topic(label, checkbox, childList, parent) + const previous = index > 0 ? topics[index - 1] : null + + const topic = new Topic(label, checkbox, childList, parent, previous) + + if (index > 0) { + topics[index - 1].next = topic + } + topics.push(topic) } } @@ -139,17 +148,26 @@ class Topic { checkbox: HTMLInputElement children: Array parent: ?Topic + next: ?Topic + previous: ?Topic // Whether this topic is selected, we only allow one item in a branch of the // taxonomy to be selected. // E.g. given education > school > 6th form only one of these can be selected // at a time and the parents are implicity selected from it selected: boolean - constructor(label: HTMLLabelElement, checkbox: HTMLInputElement, childList: ?HTMLUListElement, parent: ?Topic) { + constructor( + label: HTMLLabelElement, + checkbox: HTMLInputElement, + childList: ?HTMLUListElement, + parent: ?Topic, + previous: ?Topic + ) { this.label = label this.checkbox = checkbox this.parent = parent this.children = Topic.fromList(childList, this) + this.previous = previous if (this.checkbox.checked) { this.select() @@ -392,6 +410,16 @@ class MillerColumnsElement extends HTMLElement { } } + /** Focus the miller columns item associated with a topic */ + focusTopic(topic: ?Topic) { + if (topic instanceof Topic && topic.checkbox) { + const item = topic.checkbox.closest(`.${this.classNames.item}`) + if (item instanceof HTMLElement) { + item.focus() + } + } + } + /** Sets up the event handling for a list item and a topic */ attachEvents(trigger: HTMLElement, topic: Topic) { trigger.tabIndex = 0 @@ -406,10 +434,43 @@ class MillerColumnsElement extends HTMLElement { trigger.addEventListener( 'keydown', (event: KeyboardEvent) => { - if ([' ', 'Enter'].indexOf(event.key) !== -1) { - event.preventDefault() - this.taxonomy.topicClicked(topic) - topic.checkbox.dispatchEvent(new Event('click')) + switch (event.key) { + case ' ': + case 'Enter': + event.preventDefault() + this.taxonomy.topicClicked(topic) + topic.checkbox.dispatchEvent(new Event('click')) + break + case 'ArrowUp': + event.preventDefault() + if (topic.previous) { + this.showCurrentColumns(topic.previous) + this.focusTopic(topic.previous) + } + break + case 'ArrowDown': + event.preventDefault() + if (topic.next) { + this.showCurrentColumns(topic.next) + this.focusTopic(topic.next) + } + break + case 'ArrowLeft': + event.preventDefault() + if (topic.parent) { + this.showCurrentColumns(topic.parent) + this.focusTopic(topic.parent) + } + break + case 'ArrowRight': + event.preventDefault() + if (topic.children) { + this.showCurrentColumns(topic.children[0]) + this.focusTopic(topic.children[0]) + } + break + default: + return } }, false diff --git a/test/test.js b/test/test.js index 80bfec0..fdf2339 100644 --- a/test/test.js +++ b/test/test.js @@ -97,17 +97,17 @@ describe('miller-columns', function() { document.body.innerHTML = undefined }) - it('unnest lists', function() { + it('unnests lists', function() { const lists = document.querySelectorAll('ul') assert.equal(lists.length, 4) }) - it('mark items with children as parents', function() { + it('marks items with children as parents', function() { const firstItem = document.querySelector('ul li') assert.isTrue(firstItem.classList.contains('miller-columns__item--parent')) }) - it('mark selected item as active when clicked', function() { + it('marks selected item as active when clicked', function() { const firstItem = document.querySelector('ul li') const firstItemCheckbox = firstItem.querySelector('input') firstItemCheckbox.addEventListener('click', function(e) { @@ -120,7 +120,7 @@ describe('miller-columns', function() { assert.isTrue(firstItem.querySelector('input').checked) }) - it('mark selected item as active when pressing Enter', function() { + it('marks selected item as active when pressing Enter', function() { const firstItem = document.querySelector('ul li') const firstItemCheckbox = firstItem.querySelector('input') firstItemCheckbox.addEventListener('keydown', function(e) { @@ -134,7 +134,7 @@ describe('miller-columns', function() { assert.isTrue(firstItem.querySelector('input').checked) }) - it('mark selected item as active when pressing Space', function() { + it('marks selected item as active when pressing Space', function() { const firstItem = document.querySelector('ul li') const firstItemCheckbox = firstItem.querySelector('input') firstItemCheckbox.addEventListener('keydown', function(e) { @@ -148,7 +148,47 @@ describe('miller-columns', function() { assert.isTrue(firstItem.querySelector('input').checked) }) - it('show the child list for active list item', function() { + it('marks next item as focused when pressing ArrowDown', function() { + const firstItem = document.querySelector('.miller-columns__column li:nth-of-type(1)') + const secondItem = document.querySelector('.miller-columns__column li:nth-of-type(2)') + + firstItem.focus() + pressKey('ArrowDown', firstItem) + + assert.deepEqual(secondItem, document.activeElement) + }) + + it('marks previous item as focused when pressing ArrowUp', function() { + const firstItem = document.querySelector('.miller-columns__column li:nth-of-type(1)') + const secondItem = document.querySelector('.miller-columns__column li:nth-of-type(2)') + + secondItem.focus() + pressKey('ArrowUp', secondItem) + + assert.deepEqual(firstItem, document.activeElement) + }) + + it('marks first child item as focused when pressing ArrowRight', function() { + const firstItemL1 = document.querySelector('.miller-columns__column:nth-of-type(1) li') + const firstItemL2 = document.querySelector('.miller-columns__column:nth-of-type(2) li') + + firstItemL1.focus() + pressKey('ArrowRight', firstItemL1) + + assert.deepEqual(firstItemL2, document.activeElement) + }) + + it('marks parent item as focused when pressing ArrowLeft', function() { + const firstItemL1 = document.querySelector('.miller-columns__column:nth-of-type(1) li') + const firstItemL2 = document.querySelector('.miller-columns__column:nth-of-type(2) li') + + firstItemL2.focus() + pressKey('ArrowLeft', firstItemL2) + + assert.deepEqual(firstItemL1, document.activeElement) + }) + + it('shows the child list for active list item', function() { const firstItem = document.querySelector('ul li') const l2List = document.querySelectorAll('.miller-columns__column')[1] assert.isTrue(l2List.classList.contains('miller-columns__column--collapse')) @@ -156,7 +196,7 @@ describe('miller-columns', function() { assert.isFalse(l2List.classList.contains('miller-columns__column--collapse')) }) - it('unselect children when item is unselected', function() { + it('unselects children when item is unselected', function() { const firstItemL1 = document.querySelector('.miller-columns__column:nth-of-type(1) li') const firstItemL2 = document.querySelector('.miller-columns__column:nth-of-type(2) li') firstItemL1.click() @@ -360,14 +400,14 @@ describe('miller-columns', function() { document.body.innerHTML = undefined }) - it('mark items with checked inputs as selected', function() { + it('marks items with checked inputs as selected', function() { const selectedCheckbox = document.querySelector('ul li input:checked') const listItem = selectedCheckbox.closest('li') assert.isTrue(listItem.classList.contains('miller-columns__item--active')) assert.isTrue(listItem.querySelector('input').checked) }) - it('mark selected item’s parent as selected', function() { + it('marks selected item’s parent as selected', function() { const listItem = document.querySelector('ul li') assert.isTrue(listItem.classList.contains('miller-columns__item--active')) assert.isTrue(listItem.querySelector('input').checked)