baseframe-js
Version:
A suite of useful Javascript plugins and functions to help with Front-end Development on websites
170 lines (169 loc) • 6.57 kB
JavaScript
import $ from 'cash-dom';
import { getDataOptions } from "./util/helpers";
import { isVisible } from './util/helpers';
import Store from "./core/Store";
const KEYS = {
esc: 'Escape',
left: 'ArrowLeft',
right: 'ArrowRight',
down: 'ArrowDown',
up: 'ArrowUp',
enter: 'Enter',
shift: 'Shift',
space: 'Space',
tab: 'Tab'
};
const VERSION = "1.3.0";
const DATA_NAME = 'AccessibleMenu';
const EVENT_NAME = 'accessibleMenu';
const DEFAULTS = {
keyDirections: ['horizontal', 'vertical', 'vertical'],
focusCss: 'focus',
focusInElems: 'a, [tabindex]',
focusLeaveElems: 'a, [tabindex], select, button'
};
const visible = (i, el) => isVisible(el);
export default class AccessibleMenu {
element;
$element;
params;
static defaults = DEFAULTS;
static version = VERSION;
static pluginName = DATA_NAME;
constructor(element, options) {
const s = this;
const dataOptions = getDataOptions(element, EVENT_NAME);
s.element = element;
s.$element = $(element);
s.params = $.extend({}, AccessibleMenu.defaults, options, dataOptions);
s.handleEvents();
return s;
}
static remove(element, plugin) {
$(element).each(function () {
const s = plugin || Store(this, DATA_NAME);
s.$element.off('focusin.' + EVENT_NAME);
s.$element.off('mouseleave.' + EVENT_NAME);
s.$element.off('blur.' + EVENT_NAME);
s.$element.off('keydown.' + EVENT_NAME);
Store(this, DATA_NAME, null);
});
}
prev(props) {
const s = this;
const p = props;
const l = p.$ulParents.length - 1;
const key = p.e.key;
if (key === KEYS.left && p.keyDirections[l] === "horizontal" ||
key === KEYS.up && p.keyDirections[l] === "vertical" ||
key === KEYS.left && p.keyDirections[l] === "vertical" &&
(l > 1 && p.keyDirections[l - 1] === "vertical" && $(p.activeElem).parent('li').index() === 0)) {
s.#focusListItem(p.activeElem, p.$ulParents, p.focusCss, true, p.focusInElems);
p.e.preventDefault();
}
}
next(props) {
const s = this;
const p = props;
const l = p.$ulParents.length - 1;
const atRootUl = p.$ulParents.length === 1;
const key = p.e.key;
if (
//go to sibling <li>
key === KEYS.right && p.keyDirections[l] === "horizontal" ||
key === KEYS.down && p.keyDirections[l] === "vertical") {
const isLastAtRoolLevel = atRootUl && $(p.activeElem).closest('li').last();
const $currentLi = $(p.activeElem).closest('li');
const isLastListItem = !$currentLi.next('li').length;
if (isLastAtRoolLevel && isLastListItem) {
s.#escapeFromUlAtRootNext(s.params.focusLeaveElems, p.$ulParents, p.activeElem);
}
else {
s.#focusListItem(p.activeElem, p.$ulParents, p.focusCss, false, p.focusInElems);
p.e.preventDefault();
}
}
if (
//go to the nestled <li>
key === KEYS.right && p.keyDirections[l] === "vertical" ||
key === KEYS.down && p.keyDirections[l] === "horizontal") {
s.#focusNestledListItem(p.activeElem, p.focusCss, p.focusInElems);
p.e.preventDefault();
}
}
handleEvents() {
const s = this;
let to = null;
$(s.element).on('focusin.' + EVENT_NAME, this.params.focusInElems, function (e) {
to && clearTimeout(to);
$(this).parent('li').addClass('focus')
.siblings('li').removeClass('focus');
}).on('mouseleave.' + EVENT_NAME, function () {
$(this).find('li.focus').removeClass('focus');
}).on('focusout.' + EVENT_NAME, function () {
to = setTimeout(() => {
$(this).find('li.focus').removeClass('focus');
}, 200);
});
$(s.element).on('keydown.' + EVENT_NAME, function (e) {
const { focusCss, keyDirections, focusInElems } = s.params;
const activeElem = document.activeElement;
const $ulParents = $(activeElem).parents('ul');
const props = { e, $ulParents, activeElem, focusCss, keyDirections, focusInElems };
s.#escapeKey(e, $ulParents, focusCss, focusInElems);
s.prev(props);
s.next(props);
});
}
#escapeKey(e, $ulParents, focusCss, focusInElems) {
if (e.key == KEYS.esc) {
if ($ulParents.length > 1) {
const $anchor = $ulParents.eq(0).closest('li').find(focusInElems).filter(visible);
$anchor[0].focus();
$anchor.parent('li').addClass(focusCss);
}
e.preventDefault();
}
}
#focusListItem(activeElem, $ulParents, focusCss, prev, focusInElems) {
const $aeLi = $(activeElem).parent('li');
const $el = $aeLi[prev ? 'prev' : 'next']('li').filter(visible);
if ($el.length) {
$el.addClass(focusCss).siblings('li').removeClass(focusCss);
$el.find(focusInElems)[0].focus();
}
else {
if ($ulParents.length > 1) {
const $anchor = $ulParents.eq(0).parent('li').find('a').filter(visible);
if ($anchor.length) {
$anchor[0].focus();
$anchor.parent('li').eq(0).addClass(focusCss);
}
}
}
}
#focusNestledListItem(activeElem, focusCss, focusInElems) {
const $el = $(activeElem).parent('li').find('li').filter(visible);
if ($el.length) {
$el.addClass(focusCss).siblings('li').removeClass(focusCss);
$el.find(focusInElems).filter(visible)[0].focus();
}
}
#escapeFromUlAtRootNext(focusLeaveElems, $ulParents, activeElem) {
const $rootUl = $ulParents.eq(0);
const focusableElems = document.querySelectorAll(focusLeaveElems);
let atCurrElem = false;
for (let i = 0, l = focusableElems.length; i < l; i++) {
const elem = focusableElems[i];
if (!atCurrElem && activeElem?.isSameNode(elem)) {
atCurrElem = true;
}
if (atCurrElem && !$rootUl.has(elem).length) {
if (elem instanceof HTMLElement) {
elem.focus();
break;
}
}
}
}
}