mmenu-js
Version:
The best javascript plugin for app look-alike on- and off-canvas menus with sliding submenus for your website and webapp.
238 lines (206 loc) • 8.41 kB
text/typescript
import Mmenu from '../../core/oncanvas/mmenu.oncanvas';
import options from './_options';
import { extendShorthandOptions } from './_options';
import * as DOM from '../../_modules/dom';
import * as events from '../../_modules/eventlisteners';
import * as support from '../../_modules/support';
import { extend } from '../../_modules/helpers';
// Add the options.
Mmenu.options.keyboardNavigation = options;
export default function(this: Mmenu) {
// Keyboard navigation on touchscreens opens the virtual keyboard :/
// Lets prevent that.
if (support.touch) {
return;
}
var options = extendShorthandOptions(this.opts.keyboardNavigation);
this.opts.keyboardNavigation = extend(
options,
Mmenu.options.keyboardNavigation
);
// Enable keyboard navigation
if (options.enable) {
let menuStart = DOM.create('button.mm-tabstart.mm-sronly'),
menuEnd = DOM.create('button.mm-tabend.mm-sronly'),
blockerEnd = DOM.create('button.mm-tabend.mm-sronly');
this.bind('initMenu:after', () => {
if (options.enhance) {
this.node.menu.classList.add('mm-menu_keyboardfocus');
}
initWindow.call(this, options.enhance);
});
this.bind('initOpened:before', () => {
this.node.menu.prepend(menuStart);
this.node.menu.append(menuEnd);
DOM.children(
this.node.menu,
'.mm-navbars-top, .mm-navbars-bottom'
).forEach(navbars => {
navbars.querySelectorAll('.mm-navbar__title').forEach(title => {
title.setAttribute('tabindex', '-1');
});
});
});
this.bind('initBlocker:after', () => {
Mmenu.node.blck.append(blockerEnd);
DOM.children(Mmenu.node.blck, 'a')[0].classList.add('mm-tabstart');
});
let focusable = 'input, select, textarea, button, label, a[href]';
const setFocus = (panel?: HTMLElement) => {
panel =
panel || DOM.children(this.node.pnls, '.mm-panel_opened')[0];
var focus: HTMLElement = null;
// Focus already is on an element in a navbar in this menu.
var navbar = document.activeElement.closest('.mm-navbar');
if (navbar) {
if (navbar.closest('.mm-menu') == this.node.menu) {
return;
}
}
// Set the focus to the first focusable element by default.
if (options.enable == 'default') {
// First visible anchor in a listview in the current panel.
focus = DOM.find(
panel,
'.mm-listview a[href]:not(.mm-hidden)'
)[0];
// First focusable and visible element in the current panel.
if (!focus) {
focus = DOM.find(panel, focusable + ':not(.mm-hidden)')[0];
}
// First focusable and visible element in a navbar.
if (!focus) {
let elements: HTMLElement[] = [];
DOM.children(
this.node.menu,
'.mm-navbars_top, .mm-navbars_bottom'
).forEach(navbar => {
elements.push(
...DOM.find(navbar, focusable + ':not(.mm-hidden)')
);
});
focus = elements[0];
}
}
// Default.
if (!focus) {
focus = DOM.children(this.node.menu, '.mm-tabstart')[0];
}
if (focus) {
focus.focus();
}
};
this.bind('open:finish', setFocus);
this.bind('openPanel:finish', setFocus);
// Add screenreader / aria support.
this.bind('initOpened:after:sr-aria', () => {
[this.node.menu, Mmenu.node.blck].forEach(element => {
DOM.children(element, '.mm-tabstart, .mm-tabend').forEach(
tabber => {
Mmenu.sr_aria(tabber, 'hidden', true);
Mmenu.sr_role(tabber, 'presentation');
}
);
});
});
}
}
/**
* Initialize the window for keyboard navigation.
* @param {boolean} enhance - Whether or not to also rich enhance the keyboard behavior.
**/
const initWindow = function(this: Mmenu, enhance: boolean) {
// Re-enable tabbing in general
events.off(document.body, 'keydown.tabguard');
// Intersept the target when tabbing.
events.off(document.body, 'focusin.tabguard');
events.on(document.body, 'focusin.tabguard', (evnt: KeyboardEvent) => {
if (this.node.wrpr.matches('.mm-wrapper_opened')) {
let target = evnt.target as HTMLElement;
if (target.matches('.mm-tabend')) {
let next;
// Jump from menu to blocker.
if (target.parentElement.matches('.mm-menu')) {
if (Mmenu.node.blck) {
next = Mmenu.node.blck;
}
}
// Jump to opened menu.
if (target.parentElement.matches('.mm-wrapper__blocker')) {
next = DOM.find(
document.body,
'.mm-menu_offcanvas.mm-menu_opened'
)[0];
}
// If no available element found, stay in current element.
if (!next) {
next = target.parentElement;
}
if (next) {
DOM.children(next, '.mm-tabstart')[0].focus();
}
}
}
});
// Add Additional keyboard behavior.
events.off(document.body, 'keydown.navigate');
events.on(document.body, 'keydown.navigate', (evnt: KeyboardEvent) => {
var target = evnt.target as HTMLElement;
var menu = target.closest('.mm-menu') as HTMLElement;
if (menu) {
let api: mmApi = menu['mmApi'];
if (!target.matches('input, textarea')) {
switch (evnt.keyCode) {
// press enter to toggle and check
case 13:
if (
target.matches('.mm-toggle') ||
target.matches('.mm-check')
) {
target.dispatchEvent(new Event('click'));
}
break;
// prevent spacebar or arrows from scrolling the page
case 32: // space
case 37: // left
case 38: // top
case 39: // right
case 40: // bottom
evnt.preventDefault();
break;
}
}
if (enhance) {
// special case for input
if (target.matches('input')) {
switch (evnt.keyCode) {
// empty searchfield with esc
case 27:
(target as HTMLInputElement).value = '';
break;
}
} else {
let api: mmApi = menu['mmApi'];
switch (evnt.keyCode) {
// close submenu with backspace
case 8:
let parent: HTMLElement = DOM.find(
menu,
'.mm-panel_opened'
)[0]['mmParent'];
if (parent) {
api.openPanel(parent.closest('.mm-panel'));
}
break;
// close menu with esc
case 27:
if (menu.matches('.mm-menu_offcanvas')) {
api.close();
}
break;
}
}
}
}
});
};