New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

makeup-navigation-emitter

Package Overview
Dependencies
Maintainers
7
Versions
34
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

makeup-navigation-emitter - npm Package Compare versions

Comparing version 0.5.1 to 0.6.0

package-lock.json

474

dist/cjs/index.js

@@ -7,108 +7,152 @@ "use strict";

exports.createLinear = createLinear;
var KeyEmitter = _interopRequireWildcard(require("makeup-key-emitter"));
var ExitEmitter = _interopRequireWildcard(require("makeup-exit-emitter"));
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); Object.defineProperty(subClass, "prototype", { writable: false }); if (superClass) _setPrototypeOf(subClass, superClass); }
function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }
function _possibleConstructorReturn(self, call) { if (call && (typeof call === "object" || typeof call === "function")) { return call; } else if (call !== void 0) { throw new TypeError("Derived constructors may only return object or undefined"); } return _assertThisInitialized(self); }
function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }
function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
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); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var dataSetKey = 'data-makeup-index'; // todo: autoInit: -1, autoReset: -1 are used for activeDescendant behaviour. These values can be abstracted away with
// a new "type" option (roving or active)
var defaultOptions = {
axis: 'both',
autoInit: 0,
autoReset: null,
ignoreButtons: false,
autoInit: 'interactive',
autoReset: 'current',
ignoreByDelegateSelector: null,
wrap: false
};
var itemFilter = function itemFilter(el) {
return !el.hidden;
};
function clearData(els) {
els.forEach(function (el) {
return el.removeAttribute(dataSetKey);
});
function isItemNavigable(el) {
return !el.hidden && el.getAttribute('aria-disabled') !== 'true';
}
function setData(els) {
els.forEach(function (el, index) {
return el.setAttribute(dataSetKey, index);
});
function isIndexNavigable(items, index) {
return index >= 0 && index < items.length ? isItemNavigable(items[index]) : false;
}
function isButton(el) {
return el.tagName.toLowerCase() === 'button' || el.type === 'button';
function findNavigableItems(items) {
return items.filter(isItemNavigable);
}
function onKeyPrev(e) {
if (isButton(e.detail.target) === false || this.options.ignoreButtons === false) {
if (!this.atStart()) {
this.index--;
} else if (this.options.wrap) {
this.index = this.filteredItems.length - 1;
function findFirstNavigableIndex(items) {
return items.findIndex(item => isItemNavigable(item));
}
function findLastNavigableIndex(items) {
// todo: at(-1) is more performant than reverse(), but Babel is not transpiling it
return items.indexOf(findNavigableItems(items).reverse()[0]);
}
function findIndexByAttribute(items, attribute, value) {
return items.findIndex(item => isItemNavigable(item) && item.getAttribute(attribute) === value);
}
function findFirstNavigableAriaCheckedIndex(items) {
return findIndexByAttribute(items, 'aria-checked', 'true');
}
function findFirstNavigableAriaSelectedIndex(items) {
return findIndexByAttribute(items, 'aria-selected', 'true');
}
function findIgnoredByDelegateItems(el, options) {
return options.ignoreByDelegateSelector !== null ? [...el.querySelectorAll(options.ignoreByDelegateSelector)] : [];
}
function findPreviousNavigableIndex(items, index, wrap) {
var previousNavigableIndex = -1;
if (index === null) {
// no-op
} else if (atStart(items, index)) {
if (wrap === true) {
previousNavigableIndex = findLastNavigableIndex(items);
}
} else {
var i = index;
while (--i >= 0) {
if (isItemNavigable(items[i])) {
previousNavigableIndex = i;
break;
}
}
}
return previousNavigableIndex;
}
function onKeyNext(e) {
if (isButton(e.detail.target) === false || this.options.ignoreButtons === false) {
if (!this.atEnd()) {
this.index++;
} else if (this.options.wrap) {
this.index = 0;
function findNextNavigableIndex(items, index, wrap) {
var nextNavigableIndex = -1;
if (index === null) {
nextNavigableIndex = findFirstNavigableIndex(items);
} else if (atEnd(items, index)) {
if (wrap === true) {
nextNavigableIndex = findFirstNavigableIndex(items);
}
} else {
var i = index;
while (++i < items.length) {
if (isItemNavigable(items[i])) {
nextNavigableIndex = i;
break;
}
}
}
return nextNavigableIndex;
}
function onClick(e) {
var element = e.target;
var indexData = element.dataset.makeupIndex; // traverse widget ancestors until interactive element is found
// returning -1 means not found
function findIndexPositionByType(typeOrNum, items, currentIndex) {
var index = -1;
switch (typeOrNum) {
case 'none':
index = null;
break;
case 'current':
index = currentIndex;
break;
case 'interactive':
index = findFirstNavigableIndex(items);
break;
case 'ariaChecked':
index = findFirstNavigableAriaCheckedIndex(items);
break;
case 'ariaSelected':
index = findFirstNavigableAriaSelectedIndex(items);
break;
case 'ariaSelectedOrInteractive':
index = findFirstNavigableAriaSelectedIndex(items);
index = index === -1 ? findFirstNavigableIndex(items) : index;
break;
default:
index = typeof typeOrNum === 'number' || typeOrNum === null ? typeOrNum : -1;
}
return index;
}
function atStart(items, index) {
return index === findFirstNavigableIndex(items);
}
function atEnd(items, index) {
return index === findLastNavigableIndex(items);
}
function onKeyPrev(e) {
var ignoredByDelegateItems = findIgnoredByDelegateItems(this._el, this.options);
while (element !== this._el && !indexData) {
element = element.parentNode;
indexData = element.dataset.makeupIndex;
// todo: update KeyEmitter to deal with ignored items?
if (ignoredByDelegateItems.length === 0 || !ignoredByDelegateItems.includes(e.detail.target)) {
this.index = findPreviousNavigableIndex(this.items, this.index, this.options.wrap);
}
}
function onKeyNext(e) {
var ignoredByDelegateItems = findIgnoredByDelegateItems(this._el, this.options);
if (indexData !== undefined) {
this.index = indexData;
// todo: update KeyEmitter to deal with ignored items?
if (ignoredByDelegateItems.length === 0 || !ignoredByDelegateItems.includes(e.detail.target)) {
this.index = findNextNavigableIndex(this.items, this.index, this.options.wrap);
}
}
function onClick(e) {
var itemIndex = this.indexOf(e.target.closest(this._itemSelector));
if (isIndexNavigable(this.items, itemIndex)) {
this.index = itemIndex;
}
}
function onKeyHome(e) {
var ignoredByDelegateItems = findIgnoredByDelegateItems(this._el, this.options);
function onKeyHome(e) {
if (isButton(e.detail.target) === false || this.options.ignoreButtons === false) {
this.index = 0;
// todo: update KeyEmitter to deal with ignored items?
if (ignoredByDelegateItems.length === 0 || !ignoredByDelegateItems.includes(e.detail.target)) {
this.index = findFirstNavigableIndex(this.items);
}
}
function onKeyEnd(e) {
var ignoredByDelegateItems = findIgnoredByDelegateItems(this._el, this.options);
function onKeyEnd(e) {
if (isButton(e.detail.target) === false || this.options.ignoreButtons === false) {
this.index = this.filteredItems.length;
// todo: update KeyEmitter to deal with ignored items?
if (ignoredByDelegateItems.length === 0 || !ignoredByDelegateItems.includes(e.detail.target)) {
this.index = findLastNavigableIndex(this.items);
}
}
function onFocusExit() {

@@ -119,109 +163,143 @@ if (this.options.autoReset !== null) {

}
function onMutation(e) {
var fromIndex = this.index;
var toIndex = this.index;
// https://developer.mozilla.org/en-US/docs/Web/API/MutationRecord
var {
addedNodes,
attributeName,
removedNodes,
target,
type
} = e[0];
if (type === 'attributes') {
if (target === this.currentItem) {
if (attributeName === 'aria-disabled') {
// current item was disabled - keep it as current index (until a keyboard navigation happens)
toIndex = this.index;
} else if (attributeName === 'hidden') {
// current item was hidden and focus is lost - reset index to first interactive element
toIndex = findFirstNavigableIndex(this.items);
}
} else {
toIndex = this.index;
}
} else if (type === 'childList') {
if (removedNodes.length > 0 && [...removedNodes].includes(this._cachedElement)) {
// current item was removed and focus is lost - reset index to first interactive element
toIndex = findFirstNavigableIndex(this.items);
} else if (removedNodes.length > 0 || addedNodes.length > 0) {
// nodes were added and/or removed - keep current item and resync its index
toIndex = this.indexOf(this._cachedElement);
}
}
this._index = toIndex;
this._el.dispatchEvent(new CustomEvent('navigationModelMutation', {
bubbles: false,
detail: {
fromIndex,
toIndex
}
}));
}
class NavigationModel {
/**
* @param {HTMLElement} el
* @param {string} itemSelector
* @param {typeof defaultOptions} selectedOptions
*/
constructor(el, itemSelector, selectedOptions) {
/** @member {typeof defaultOptions} */
this.options = Object.assign({}, defaultOptions, selectedOptions);
function onMutation() {
// clear data-makeup-index on ALL items
clearData(this.items); // set data-makeup-index only on filtered items (e.g. non-hidden ones)
/** @member {HTMLElement} */
this._el = el;
setData(this.filteredItems);
if (this.index >= this.items.length) {
// do not use index setter, it will trigger change event
this._index = this.options.autoReset || this.options.autoInit;
/** @member {string} */
this._itemSelector = itemSelector;
}
this._el.dispatchEvent(new CustomEvent('navigationModelMutation'));
}
class LinearNavigationModel extends NavigationModel {
/**
* @param {HTMLElement} el
* @param {string} itemSelector
* @param {typeof defaultOptions} selectedOptions
*/
constructor(el, itemSelector, selectedOptions) {
super(el, itemSelector, selectedOptions);
var fromIndex = this._index;
var toIndex = findIndexPositionByType(this.options.autoInit, this.items, this.index);
var NavigationModel = /*#__PURE__*/_createClass(function NavigationModel(el, itemSelector, selectedOptions) {
_classCallCheck(this, NavigationModel);
// do not use setter as it will trigger a change event
this._index = toIndex;
this.options = Object.assign({}, defaultOptions, selectedOptions);
this._el = el;
this._itemSelector = itemSelector;
});
// always keep an element reference to the last item (for use in mutation observer)
// todo: convert index to Tuple to store last/current values instead?
this._cachedElement = this.items[toIndex];
this._el.dispatchEvent(new CustomEvent('navigationModelInit', {
bubbles: false,
detail: {
firstInteractiveIndex: this.firstNavigableIndex,
fromIndex,
items: this.items,
toIndex
}
}));
}
get currentItem() {
return this.items[this.index];
}
var LinearNavigationModel = /*#__PURE__*/function (_NavigationModel) {
_inherits(LinearNavigationModel, _NavigationModel);
// todo: code smell as getter abstracts that the query selector re-runs every time getter is accessed
get items() {
return [...this._el.querySelectorAll("".concat(this._itemSelector))];
}
get index() {
return this._index;
}
var _super = _createSuper(LinearNavigationModel);
function LinearNavigationModel(el, itemSelector, selectedOptions) {
var _this;
_classCallCheck(this, LinearNavigationModel);
_this = _super.call(this, el, itemSelector, selectedOptions);
if (_this.options.autoInit !== null) {
_this._index = _this.options.autoInit;
_this._el.dispatchEvent(new CustomEvent('navigationModelInit', {
/**
* @param {number} toIndex - update index position in this.items (non-interactive indexes fail silently)
*/
set index(toIndex) {
if (toIndex === this.index) {
return;
} else if (!isIndexNavigable(this.items, toIndex)) {
// no-op. throw exception?
} else {
var fromIndex = this.index;
// update cached element reference (for use in mutation observer if DOM node gets removed)
this._cachedElement = this.items[toIndex];
this._index = toIndex;
this._el.dispatchEvent(new CustomEvent('navigationModelChange', {
bubbles: false,
detail: {
items: _this.filteredItems,
toIndex: _this.options.autoInit
},
bubbles: false
fromIndex,
toIndex
}
}));
}
return _this;
}
_createClass(LinearNavigationModel, [{
key: "items",
get: function get() {
return this._el.querySelectorAll(this._itemSelector);
indexOf(element) {
return this.items.indexOf(element);
}
reset() {
var fromIndex = this.index;
var toIndex = findIndexPositionByType(this.options.autoReset, this.items, this.index);
if (toIndex !== fromIndex) {
// do not use setter as it will trigger a navigationModelChange event
this._index = toIndex;
this._el.dispatchEvent(new CustomEvent('navigationModelReset', {
bubbles: false,
detail: {
fromIndex,
toIndex
}
}));
}
}, {
key: "filteredItems",
get: function get() {
return Array.prototype.slice.call(this.items).filter(itemFilter);
}
}, {
key: "index",
get: function get() {
return this._index;
},
set: function set(newIndex) {
if (newIndex > -1 && newIndex < this.filteredItems.length && newIndex !== this.index) {
this._el.dispatchEvent(new CustomEvent('navigationModelChange', {
detail: {
fromIndex: this.index,
toIndex: newIndex
},
bubbles: false
}));
}
}
this._index = newIndex;
}
}
}, {
key: "reset",
value: function reset() {
if (this.options.autoReset !== null) {
this._index = this.options.autoReset; // do not use index setter, it will trigger change event
// 2D Grid Model will go here
this._el.dispatchEvent(new CustomEvent('navigationModelReset', {
detail: {
toIndex: this.options.autoReset
},
bubbles: false
}));
}
}
}, {
key: "atEnd",
value: function atEnd() {
return this.index === this.filteredItems.length - 1;
}
}, {
key: "atStart",
value: function atStart() {
return this.index <= 0;
}
}]);
return LinearNavigationModel;
}(NavigationModel); // 2D Grid Model will go here
/*

@@ -236,7 +314,8 @@ class GridModel extends NavigationModel {

var NavigationEmitter = /*#__PURE__*/function () {
function NavigationEmitter(el, model) {
_classCallCheck(this, NavigationEmitter);
class NavigationEmitter {
/**
* @param {HTMLElement} el
* @param {LinearNavigationModel} model
*/
constructor(el, model) {
this.model = model;

@@ -251,7 +330,5 @@ this.el = el;

this._observer = new MutationObserver(onMutation.bind(model));
setData(model.filteredItems);
KeyEmitter.addKeyDown(this.el);
ExitEmitter.addFocusExit(this.el);
var axis = model.options.axis;
if (axis === 'both' || axis === 'x') {

@@ -261,3 +338,2 @@ this.el.addEventListener('arrowLeftKeyDown', this._keyPrevListener);

}
if (axis === 'both' || axis === 'y') {

@@ -267,3 +343,2 @@ this.el.addEventListener('arrowUpKeyDown', this._keyPrevListener);

}
this.el.addEventListener('homeKeyDown', this._keyHomeListener);

@@ -273,32 +348,24 @@ this.el.addEventListener('endKeyDown', this._keyEndListener);

this.el.addEventListener('focusExit', this._focusExitListener);
this._observer.observe(this.el, {
childList: true,
subtree: true,
attributeFilter: ['hidden'],
attributes: true
attributeFilter: ['aria-disabled', 'hidden'],
attributes: true,
attributeOldValue: true
});
}
_createClass(NavigationEmitter, [{
key: "destroy",
value: function destroy() {
KeyEmitter.removeKeyDown(this.el);
ExitEmitter.removeFocusExit(this.el);
this.el.removeEventListener('arrowLeftKeyDown', this._keyPrevListener);
this.el.removeEventListener('arrowRightKeyDown', this._keyNextListener);
this.el.removeEventListener('arrowUpKeyDown', this._keyPrevListener);
this.el.removeEventListener('arrowDownKeyDown', this._keyNextListener);
this.el.removeEventListener('homeKeyDown', this._keyHomeListener);
this.el.removeEventListener('endKeyDown', this._keyEndListener);
this.el.removeEventListener('click', this._clickListener);
this.el.removeEventListener('focusExit', this._focusExitListener);
this._observer.disconnect();
}
}]);
return NavigationEmitter;
}();
destroy() {
KeyEmitter.removeKeyDown(this.el);
ExitEmitter.removeFocusExit(this.el);
this.el.removeEventListener('arrowLeftKeyDown', this._keyPrevListener);
this.el.removeEventListener('arrowRightKeyDown', this._keyNextListener);
this.el.removeEventListener('arrowUpKeyDown', this._keyPrevListener);
this.el.removeEventListener('arrowDownKeyDown', this._keyNextListener);
this.el.removeEventListener('homeKeyDown', this._keyHomeListener);
this.el.removeEventListener('endKeyDown', this._keyEndListener);
this.el.removeEventListener('click', this._clickListener);
this.el.removeEventListener('focusExit', this._focusExitListener);
this._observer.disconnect();
}
}
function createLinear(el, itemSelector, selectedOptions) {

@@ -308,2 +375,3 @@ var model = new LinearNavigationModel(el, itemSelector, selectedOptions);

}
/*

@@ -310,0 +378,0 @@ static createGrid(el, rowSelector, colSelector, selectedOptions) {

import * as KeyEmitter from "makeup-key-emitter";
import * as ExitEmitter from "makeup-exit-emitter";
const dataSetKey = "data-makeup-index";
const defaultOptions = {
axis: "both",
autoInit: 0,
autoReset: null,
ignoreButtons: false,
autoInit: "interactive",
autoReset: "current",
ignoreByDelegateSelector: null,
wrap: false
};
const itemFilter = (el) => !el.hidden;
function clearData(els) {
els.forEach((el) => el.removeAttribute(dataSetKey));
function isItemNavigable(el) {
return !el.hidden && el.getAttribute("aria-disabled") !== "true";
}
function setData(els) {
els.forEach((el, index) => el.setAttribute(dataSetKey, index));
function isIndexNavigable(items, index) {
return index >= 0 && index < items.length ? isItemNavigable(items[index]) : false;
}
function isButton(el) {
return el.tagName.toLowerCase() === "button" || el.type === "button";
function findNavigableItems(items) {
return items.filter(isItemNavigable);
}
function onKeyPrev(e) {
if (isButton(e.detail.target) === false || this.options.ignoreButtons === false) {
if (!this.atStart()) {
this.index--;
} else if (this.options.wrap) {
this.index = this.filteredItems.length - 1;
function findFirstNavigableIndex(items) {
return items.findIndex((item) => isItemNavigable(item));
}
function findLastNavigableIndex(items) {
return items.indexOf(findNavigableItems(items).reverse()[0]);
}
function findIndexByAttribute(items, attribute, value) {
return items.findIndex((item) => isItemNavigable(item) && item.getAttribute(attribute) === value);
}
function findFirstNavigableAriaCheckedIndex(items) {
return findIndexByAttribute(items, "aria-checked", "true");
}
function findFirstNavigableAriaSelectedIndex(items) {
return findIndexByAttribute(items, "aria-selected", "true");
}
function findIgnoredByDelegateItems(el, options) {
return options.ignoreByDelegateSelector !== null ? [...el.querySelectorAll(options.ignoreByDelegateSelector)] : [];
}
function findPreviousNavigableIndex(items, index, wrap) {
let previousNavigableIndex = -1;
if (index === null) {
} else if (atStart(items, index)) {
if (wrap === true) {
previousNavigableIndex = findLastNavigableIndex(items);
}
} else {
let i = index;
while (--i >= 0) {
if (isItemNavigable(items[i])) {
previousNavigableIndex = i;
break;
}
}
}
return previousNavigableIndex;
}
function onKeyNext(e) {
if (isButton(e.detail.target) === false || this.options.ignoreButtons === false) {
if (!this.atEnd()) {
this.index++;
} else if (this.options.wrap) {
this.index = 0;
function findNextNavigableIndex(items, index, wrap) {
let nextNavigableIndex = -1;
if (index === null) {
nextNavigableIndex = findFirstNavigableIndex(items);
} else if (atEnd(items, index)) {
if (wrap === true) {
nextNavigableIndex = findFirstNavigableIndex(items);
}
} else {
let i = index;
while (++i < items.length) {
if (isItemNavigable(items[i])) {
nextNavigableIndex = i;
break;
}
}
}
return nextNavigableIndex;
}
function onClick(e) {
let element = e.target;
let indexData = element.dataset.makeupIndex;
while (element !== this._el && !indexData) {
element = element.parentNode;
indexData = element.dataset.makeupIndex;
function findIndexPositionByType(typeOrNum, items, currentIndex) {
let index = -1;
switch (typeOrNum) {
case "none":
index = null;
break;
case "current":
index = currentIndex;
break;
case "interactive":
index = findFirstNavigableIndex(items);
break;
case "ariaChecked":
index = findFirstNavigableAriaCheckedIndex(items);
break;
case "ariaSelected":
index = findFirstNavigableAriaSelectedIndex(items);
break;
case "ariaSelectedOrInteractive":
index = findFirstNavigableAriaSelectedIndex(items);
index = index === -1 ? findFirstNavigableIndex(items) : index;
break;
default:
index = typeof typeOrNum === "number" || typeOrNum === null ? typeOrNum : -1;
}
if (indexData !== void 0) {
this.index = indexData;
return index;
}
function atStart(items, index) {
return index === findFirstNavigableIndex(items);
}
function atEnd(items, index) {
return index === findLastNavigableIndex(items);
}
function onKeyPrev(e) {
const ignoredByDelegateItems = findIgnoredByDelegateItems(this._el, this.options);
if (ignoredByDelegateItems.length === 0 || !ignoredByDelegateItems.includes(e.detail.target)) {
this.index = findPreviousNavigableIndex(this.items, this.index, this.options.wrap);
}
}
function onKeyNext(e) {
const ignoredByDelegateItems = findIgnoredByDelegateItems(this._el, this.options);
if (ignoredByDelegateItems.length === 0 || !ignoredByDelegateItems.includes(e.detail.target)) {
this.index = findNextNavigableIndex(this.items, this.index, this.options.wrap);
}
}
function onClick(e) {
const itemIndex = this.indexOf(e.target.closest(this._itemSelector));
if (isIndexNavigable(this.items, itemIndex)) {
this.index = itemIndex;
}
}
function onKeyHome(e) {
if (isButton(e.detail.target) === false || this.options.ignoreButtons === false) {
this.index = 0;
const ignoredByDelegateItems = findIgnoredByDelegateItems(this._el, this.options);
if (ignoredByDelegateItems.length === 0 || !ignoredByDelegateItems.includes(e.detail.target)) {
this.index = findFirstNavigableIndex(this.items);
}
}
function onKeyEnd(e) {
if (isButton(e.detail.target) === false || this.options.ignoreButtons === false) {
this.index = this.filteredItems.length;
const ignoredByDelegateItems = findIgnoredByDelegateItems(this._el, this.options);
if (ignoredByDelegateItems.length === 0 || !ignoredByDelegateItems.includes(e.detail.target)) {
this.index = findLastNavigableIndex(this.items);
}

@@ -65,9 +142,28 @@ }

}
function onMutation() {
clearData(this.items);
setData(this.filteredItems);
if (this.index >= this.items.length) {
this._index = this.options.autoReset || this.options.autoInit;
function onMutation(e) {
const fromIndex = this.index;
let toIndex = this.index;
const { addedNodes, attributeName, removedNodes, target, type } = e[0];
if (type === "attributes") {
if (target === this.currentItem) {
if (attributeName === "aria-disabled") {
toIndex = this.index;
} else if (attributeName === "hidden") {
toIndex = findFirstNavigableIndex(this.items);
}
} else {
toIndex = this.index;
}
} else if (type === "childList") {
if (removedNodes.length > 0 && [...removedNodes].includes(this._cachedElement)) {
toIndex = findFirstNavigableIndex(this.items);
} else if (removedNodes.length > 0 || addedNodes.length > 0) {
toIndex = this.indexOf(this._cachedElement);
}
}
this._el.dispatchEvent(new CustomEvent("navigationModelMutation"));
this._index = toIndex;
this._el.dispatchEvent(new CustomEvent("navigationModelMutation", {
bubbles: false,
detail: { fromIndex, toIndex }
}));
}

@@ -84,51 +180,53 @@ class NavigationModel {

super(el, itemSelector, selectedOptions);
if (this.options.autoInit !== null) {
this._index = this.options.autoInit;
this._el.dispatchEvent(new CustomEvent("navigationModelInit", {
detail: {
items: this.filteredItems,
toIndex: this.options.autoInit
},
bubbles: false
}));
}
const fromIndex = this._index;
const toIndex = findIndexPositionByType(this.options.autoInit, this.items, this.index);
this._index = toIndex;
this._cachedElement = this.items[toIndex];
this._el.dispatchEvent(new CustomEvent("navigationModelInit", {
bubbles: false,
detail: {
firstInteractiveIndex: this.firstNavigableIndex,
fromIndex,
items: this.items,
toIndex
}
}));
}
get currentItem() {
return this.items[this.index];
}
get items() {
return this._el.querySelectorAll(this._itemSelector);
return [...this._el.querySelectorAll(`${this._itemSelector}`)];
}
get filteredItems() {
return Array.prototype.slice.call(this.items).filter(itemFilter);
}
get index() {
return this._index;
}
set index(newIndex) {
if (newIndex > -1 && newIndex < this.filteredItems.length && newIndex !== this.index) {
set index(toIndex) {
if (toIndex === this.index) {
return;
} else if (!isIndexNavigable(this.items, toIndex)) {
} else {
const fromIndex = this.index;
this._cachedElement = this.items[toIndex];
this._index = toIndex;
this._el.dispatchEvent(new CustomEvent("navigationModelChange", {
detail: {
fromIndex: this.index,
toIndex: newIndex
},
bubbles: false
bubbles: false,
detail: { fromIndex, toIndex }
}));
this._index = newIndex;
}
}
indexOf(element) {
return this.items.indexOf(element);
}
reset() {
if (this.options.autoReset !== null) {
this._index = this.options.autoReset;
const fromIndex = this.index;
const toIndex = findIndexPositionByType(this.options.autoReset, this.items, this.index);
if (toIndex !== fromIndex) {
this._index = toIndex;
this._el.dispatchEvent(new CustomEvent("navigationModelReset", {
detail: {
toIndex: this.options.autoReset
},
bubbles: false
bubbles: false,
detail: { fromIndex, toIndex }
}));
}
}
atEnd() {
return this.index === this.filteredItems.length - 1;
}
atStart() {
return this.index <= 0;
}
}

@@ -146,3 +244,2 @@ class NavigationEmitter {

this._observer = new MutationObserver(onMutation.bind(model));
setData(model.filteredItems);
KeyEmitter.addKeyDown(this.el);

@@ -166,4 +263,5 @@ ExitEmitter.addFocusExit(this.el);

subtree: true,
attributeFilter: ["hidden"],
attributes: true
attributeFilter: ["aria-disabled", "hidden"],
attributes: true,
attributeOldValue: true
});

@@ -170,0 +268,0 @@ }

{
"name": "makeup-navigation-emitter",
"description": "Emits custom events based on keyboard navigation of one or two dimensional model",
"version": "0.5.1",
"version": "0.6.0",
"main": "./dist/cjs/index.js",

@@ -6,0 +6,0 @@ "module": "dist/mjs/index.js",

@@ -5,3 +5,3 @@ # makeup-navigation-emitter

This module can be used as the underlying logic & state for both roving-tabindex and active-descendant behaviour.
This module can be used as the underlying logic & state for both roving-tabindex and active-descendant (hierarchical & programmatic) behaviour.

@@ -12,2 +12,4 @@ ## Experimental

**NOTE**: All examples below show *abstract* markup examples/structures. In an effort to make clear what this module does and does not do, all examples **do not** include ARIA roles, state or properties.
## Example 1

@@ -17,3 +19,3 @@

With keyboard focus on any list item element, arrow keys will update the underlying model.
With keyboard focus on any list item element, arrow keys will update the underlying index position in relation to the list of items.

@@ -47,6 +49,8 @@ **NOTE:** this module will not actually modify the DOM or change any keyboard focus, that is the job of an observer such as makeup-roving-tabindex (which consumes this module).

Example support for an active descendant model of navigation with keyboard focus on ancestor of items (typical of listbox pattern). Again, the list items form a one-dimensional model of navigation.
Example support for an active descendant model of navigation with keyboard focus on ancestor of items (typical of listbox pattern).
With keyboard focus on the widget, arrow keys will update the underlying model.
With keyboard focus on the widget, arrow keys will update the underlying index position in relation to the list of items.
Note that this module will not highlight the active item, that is the job of an observer such as makeup-active-descendant.
```html

@@ -67,3 +71,3 @@ <div class="widget" tabindex="0">

var emitter = NavigationEmitter.createLinear(widgetEl, 'li', { autoInit: -1, autoReset: -1 }));
var emitter = NavigationEmitter.createLinear(widgetEl, 'li'));

@@ -77,5 +81,5 @@ widgetEl.addEventListener('navigationModelChange', function(e) {

Example support for an active descendant model of navigation with focus on non-ancestor of items (typical of combobox pattern). Once more, the list elements form the one-dimensional model.
Example support for an active descendant model of navigation with keyboard focus on non-ancestor of items (typical of combobox pattern).
With keyboard focus on the textbox, arrow keys will update the underlying model.
With keyboard focus on the textbox, arrow keys will update the underlying index position in relation to the list of items.

@@ -100,3 +104,3 @@ Note that this module will not highlight the active item, that is the job of an observer such as makeup-active-descendant.

var emitter = NavigationEmitter.createLinear(widgetEl, 'li', { autoInit: -1, autoReset: -1 }));
var emitter = NavigationEmitter.createLinear(widgetEl, 'li', { autoInit: 'none', autoReset: 'none' }));

@@ -110,6 +114,18 @@ widgetEl.addEventListener('navigationModelChange', function(e) {

* `autoInit`: specify an integer or -1 for initial index (default: 0)
* `autoReset`: specify an integer or -1 for index position when focus exits widget (default: null)
* `autoInit`: declares the initial item (default: "interactive"). Possible values are:
* "none": no index position is set (useful in programmatic active-descendant)
* "interactive": first non aria-disabled or hidden element (default)
* "ariaChecked": first element with aria-checked=true (useful in ARIA menu)
* "ariaSelected": first element with aria-selected=true (useful in ARIA tabs)
* "ariaSelectedOrInteractive": first element with aria-selected=true, falling back to "interactive" if not found (useful in ARIA listbox)
* *number*: specific index position of items (throws error if non-interactive)
* `autoReset`: declares the item after a reset and/or when keyboard focus exits the widget (default: "current"). Possible values are:
* "none": no index position is set (useful in programmatic active-descendant)
* "current": index remains current (radio button like behaviour)
* "interactive": index moves to first non aria-disabled or hidden element
* "ariaChecked": index moves to first element with aria-checked=true
* "ariaSelected": index moves to first element with aria-selected=true
* *number*: specific index position of items (throws error if non-interactive)
* `axis` : specify 'x' for left/right arrow keys, 'y' for up/down arrow keys, or 'both' (default: 'both')
* `ignoreButtons`: if set to true, nested button elements will not trigger navigationModelChange events. This is useful in a combobox + button scenario, where only the textbox should trigger navigationModelChange events (default: false)
* `ignoreByDelegateSelector`: CSS selector of descendant elements that will be ignored by the key event delegation (i.e. these elements will *not* operate the navigation emitter) (default: null)
* `wrap` : specify whether arrow keys should wrap/loop (default: false)

@@ -124,11 +140,10 @@

* `items`: returns all items that match item selector
* `filteredItems`: returns filtered items (e.g. non-hidden items)
* `matchingItems`: returns all items that match item selector
* `navigableItems`: returns navigable subset of matchingItems (e.g. non-hidden & non aria-disabled items)
## Events
* `navigationModelInit` - fired when the model is auto initialised
* `navigationModelChange` - fired when the index is set by any means other than auto init or auto reset
* `navigationModelReset` - fired when the model is auto reset
For all 3 events, the event detail object contains the `fromIndex` and `toIndex`.
* `navigationModelInit` - fired when the model is auto initialised (bubbles: false)
* `navigationModelChange` - fired when the index is set by any means other than auto init or auto reset (bubbles: false)
* `navigationModelReset` - fired when the model is auto reset (bubbles: false)
* `navigationModelMutation` - fired when any changes to the elements DOM (bubbles: false)
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc