| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320 |
- /* *
- *
- * (c) 2009-2019 Øystein Moseng
- *
- * Main keyboard navigation handling.
- *
- * License: www.highcharts.com/license
- *
- * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
- *
- * */
- 'use strict';
- import H from '../../parts/Globals.js';
- var merge = H.merge, win = H.win, doc = win.document;
- import HTMLUtilities from './utils/htmlUtilities.js';
- var getElement = HTMLUtilities.getElement;
- import KeyboardNavigationHandler from './KeyboardNavigationHandler.js';
- import EventProvider from './utils/EventProvider.js';
- /* eslint-disable valid-jsdoc */
- /**
- * 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) {
- this.init(chart, components);
- }
- 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, e = this.eventProvider = new EventProvider();
- this.chart = chart;
- this.components = components;
- this.modules = [];
- this.currentModuleIx = 0;
- // Add keydown event
- e.addEvent(chart.renderTo, 'keydown', function (e) {
- keyboardNavigation.onKeydown(e);
- });
- // Add mouseup event on doc
- e.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<string>} [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;
- this.updateContainerTabindex();
- 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();
- 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, {
- init: function () { }
- })
- ]);
- this.updateExitAnchor();
- }
- else {
- this.modules = [];
- this.currentModuleIx = 0;
- this.removeExitAnchor();
- }
- },
- /**
- * 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.KeyboardEvent} 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();
- e.stopPropagation();
- }
- }
- },
- /**
- * 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;
- },
- /**
- * We use an exit anchor 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
- */
- updateExitAnchor: function () {
- var endMarkerId = 'highcharts-end-of-chart-marker-' + this.chart.index, endMarker = getElement(endMarkerId);
- this.removeExitAnchor();
- if (endMarker) {
- this.makeElementAnExitAnchor(endMarker);
- this.exitAnchor = endMarker;
- }
- else {
- this.createExitAnchor();
- }
- },
- /**
- * Chart container should have tabindex if navigation is enabled.
- * @private
- */
- updateContainerTabindex: function () {
- var a11yOptions = this.chart.options.accessibility, keyboardOptions = a11yOptions && a11yOptions.keyboardNavigation, shouldHaveTabindex = !(keyboardOptions && keyboardOptions.enabled === false), container = this.chart.container, curTabindex = container.getAttribute('tabIndex');
- if (shouldHaveTabindex && !curTabindex) {
- container.setAttribute('tabindex', '0');
- }
- else if (!shouldHaveTabindex && curTabindex === '0') {
- container.removeAttribute('tabindex');
- }
- },
- /**
- * @private
- */
- makeElementAnExitAnchor: function (el) {
- el.setAttribute('class', 'highcharts-exit-anchor');
- el.setAttribute('tabindex', '0');
- el.setAttribute('aria-hidden', false);
- // Handle focus
- this.addExitAnchorEventsToEl(el);
- },
- /**
- * Add new exit anchor to the chart.
- *
- * @private
- */
- createExitAnchor: function () {
- var chart = this.chart, exitAnchor = this.exitAnchor = doc.createElement('div');
- // Hide exit anchor
- merge(true, exitAnchor.style, {
- position: 'absolute',
- width: '1px',
- height: '1px',
- zIndex: 0,
- overflow: 'hidden',
- outline: 'none'
- });
- chart.renderTo.appendChild(exitAnchor);
- this.makeElementAnExitAnchor(exitAnchor);
- },
- /**
- * @private
- */
- removeExitAnchor: function () {
- if (this.exitAnchor && this.exitAnchor.parentNode) {
- this.exitAnchor.parentNode
- .removeChild(this.exitAnchor);
- delete this.exitAnchor;
- }
- },
- /**
- * @private
- */
- addExitAnchorEventsToEl: function (element) {
- var chart = this.chart, keyboardNavigation = this;
- this.eventProvider.addEvent(element, 'focus', function (ev) {
- var e = ev || win.event, curModule, focusComesFromChart = (e.relatedTarget &&
- chart.container.contains(e.relatedTarget)), comingInBackwards = !(focusComesFromChart || keyboardNavigation.exiting);
- if (comingInBackwards) {
- 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 () {
- this.removeExitAnchor();
- this.eventProvider.removeAddedEvents();
- if (this.chart.container.getAttribute('tabindex') === '0') {
- this.chart.container.removeAttribute('tabindex');
- }
- }
- };
- export default KeyboardNavigation;
|