UNPKG

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
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; } } } } }); };