/* * * * (c) 2009-2019 Øystein Moseng * * Main keyboard navigation handling. * * License: www.highcharts.com/license * * */ 'use strict'; import H from '../../parts/Globals.js'; import KeyboardNavigationHandler from './KeyboardNavigationHandler.js'; var merge = H.merge, addEvent = H.addEvent, win = H.win, doc = win.document; /** * The KeyboardNavigation class, containing the overall keyboard navigation * logic for the chart. * * @requires module:modules/accessibility * * @private * @class * @param {Highcharts.Chart} chart * Chart object * @param {object} components * Map of component names to AccessibilityComponent objects. * @name Highcharts.KeyboardNavigation */ function KeyboardNavigation(chart, components, order) { this.init(chart, components, order); } KeyboardNavigation.prototype = { /** * Initialize the class * @private * @param {Highcharts.Chart} chart * Chart object * @param {object} components * Map of component names to AccessibilityComponent objects. */ init: function (chart, components) { var keyboardNavigation = this; this.chart = chart; this.components = components; this.modules = []; this.currentModuleIx = 0; // Make chart container reachable by tab if (!chart.container.hasAttribute('tabIndex')) { chart.container.setAttribute('tabindex', '0'); } // Add exit anchor for focus this.addExitAnchor(); // Add keydown event this.unbindKeydownHandler = addEvent( chart.renderTo, 'keydown', function (e) { keyboardNavigation.onKeydown(e); } ); // Add mouseup event on doc this.unbindMouseUpHandler = addEvent(doc, 'mouseup', function () { keyboardNavigation.onMouseUp(); }); // Run an update to get all modules this.update(); // Init first module if (this.modules.length) { this.modules[0].init(1); } }, /** * Update the modules for the keyboard navigation * @param {Array} order * Array specifying the tab order of the components. */ update: function (order) { var a11yOptions = this.chart.options.accessibility, keyboardOptions = a11yOptions && a11yOptions.keyboardNavigation, components = this.components; if ( keyboardOptions && keyboardOptions.enabled && order && order.length ) { // We (still) have keyboard navigation. Update module list this.modules = order.reduce(function (modules, componentName) { var navModules = components[componentName] .getKeyboardNavigation(); // If we didn't get back a list of modules, just push the one if (!navModules.length) { modules.push(navModules); return modules; } // Add all of the modules return modules.concat(navModules); }, [ // Add an empty module at the start of list, to allow users to // tab into the chart. new KeyboardNavigationHandler(this.chart, {}) ]); } else { // Clear module list and reset this.modules = []; this.currentModuleIx = 0; } }, /** * Reset chart navigation state if we click outside the chart and it's * not already reset. * @private */ onMouseUp: function () { if ( !this.keyboardReset && !(this.chart.pointer && this.chart.pointer.chartPosition) ) { var chart = this.chart, curMod = this.modules && this.modules[this.currentModuleIx || 0]; if (curMod && curMod.terminate) { curMod.terminate(); } if (chart.focusElement) { chart.focusElement.removeFocusBorder(); } this.currentModuleIx = 0; this.keyboardReset = true; } }, /** * Function to run on keydown * @private * @param {global.Event} ev * Browser keydown event */ onKeydown: function (ev) { var e = ev || win.event, preventDefault, curNavModule = this.modules && this.modules.length && this.modules[this.currentModuleIx]; // Used for resetting nav state when clicking outside chart this.keyboardReset = false; // If there is a nav module for the current index, run it. // Otherwise, we are outside of the chart in some direction. if (curNavModule) { var response = curNavModule.run(e); if (response === curNavModule.response.success) { preventDefault = true; } else if (response === curNavModule.response.prev) { preventDefault = this.prev(); } else if (response === curNavModule.response.next) { preventDefault = this.next(); } if (preventDefault) { e.preventDefault(); } } }, /** * Go to previous module. * @private */ prev: function () { return this.move(-1); }, /** * Go to next module. * @private */ next: function () { return this.move(1); }, /** * Move to prev/next module. * @private * @param {number} direction Direction to move. +1 for next, -1 for prev. * @return {boolean} True if there was a valid module in direction. */ move: function (direction) { var curModule = this.modules && this.modules[this.currentModuleIx]; if (curModule && curModule.terminate) { curModule.terminate(direction); } // Remove existing focus border if any if (this.chart.focusElement) { this.chart.focusElement.removeFocusBorder(); } this.currentModuleIx += direction; var newModule = this.modules && this.modules[this.currentModuleIx]; if (newModule) { if (newModule.validate && !newModule.validate()) { return this.move(direction); // Invalid module, recurse } if (newModule.init) { newModule.init(direction); // Valid module, init it return true; } } // No module this.currentModuleIx = 0; // Reset counter // Set focus to chart or exit anchor depending on direction if (direction > 0) { this.exiting = true; this.exitAnchor.focus(); } else { this.chart.renderTo.focus(); } return false; }, /** * Add exit anchor to the chart. We use this to move focus out of chart * whenever we want, by setting focus to this div and not preventing the * default tab action. We also use this when users come back into the chart * by tabbing back, in order to navigate from the end of the chart. * @private */ addExitAnchor: function () { var chart = this.chart, exitAnchor = this.exitAnchor = doc.createElement('h6'), keyboardNavigation = this, exitAnchorLabel = chart.langFormat( 'accessibility.svgContainerEnd', { chart: chart } ); exitAnchor.setAttribute('tabindex', '0'); exitAnchor.setAttribute('aria-label', exitAnchorLabel); exitAnchor.setAttribute('aria-hidden', false); // Hide exit anchor merge(true, exitAnchor.style, { position: 'absolute', width: '1px', height: '1px', zIndex: 0, overflow: 'hidden', outline: 'none' }); chart.renderTo.appendChild(exitAnchor); // Update position on render this.unbindExitAnchorUpdate = addEvent(chart, 'render', function () { this.renderTo.appendChild(exitAnchor); }); // Handle focus this.unbindExitAnchorFocus = addEvent( exitAnchor, 'focus', function (ev) { var e = ev || win.event, curModule; // If focusing and we are exiting, do nothing once. if (!keyboardNavigation.exiting) { // Not exiting, means we are coming in backwards chart.renderTo.focus(); e.preventDefault(); // Move to last valid keyboard nav module // Note the we don't run it, just set the index if ( keyboardNavigation.modules && keyboardNavigation.modules.length ) { keyboardNavigation.currentModuleIx = keyboardNavigation.modules.length - 1; curModule = keyboardNavigation.modules[ keyboardNavigation.currentModuleIx ]; // Validate the module if ( curModule && curModule.validate && !curModule.validate() ) { // Invalid. Try moving backwards to find next valid. keyboardNavigation.prev(); } else if (curModule) { // We have a valid module, init it curModule.init(-1); } } } else { // Don't skip the next focus, we only skip once. keyboardNavigation.exiting = false; } } ); }, /** * Remove all traces of keyboard navigation. * @private */ destroy: function () { // Remove exit anchor if (this.unbindExitAnchorFocus) { this.unbindExitAnchorFocus(); delete this.unbindExitAnchorFocus; } if (this.unbindExitAnchorUpdate) { this.unbindExitAnchorUpdate(); delete this.unbindExitAnchorUpdate; } if (this.exitAnchor && this.exitAnchor.parentNode) { this.exitAnchor.parentNode.removeChild(this.exitAnchor); delete this.exitAnchor; } // Remove keydown handler if (this.unbindKeydownHandler) { this.unbindKeydownHandler(); } // Remove mouseup handler if (this.unbindMouseUpHandler) { this.unbindMouseUpHandler(); } } }; export default KeyboardNavigation;