123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373 |
- /* *
- *
- * (c) 2009-2021 Øystein Moseng
- *
- * Main keyboard navigation handling.
- *
- * License: www.highcharts.com/license
- *
- * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
- *
- * */
- 'use strict';
- import Chart from '../Core/Chart/Chart.js';
- import H from '../Core/Globals.js';
- var doc = H.doc, win = H.win;
- import U from '../Core/Utilities.js';
- var addEvent = U.addEvent, fireEvent = U.fireEvent;
- import HTMLUtilities from './Utils/HTMLUtilities.js';
- var getElement = HTMLUtilities.getElement;
- import EventProvider from './Utils/EventProvider.js';
- /* eslint-disable valid-jsdoc */
- // Add event listener to document to detect ESC key press and dismiss
- // hover/popup content.
- addEvent(doc, 'keydown', function (e) {
- var keycode = e.which || e.keyCode;
- var esc = 27;
- if (keycode === esc && H.charts) {
- H.charts.forEach(function (chart) {
- if (chart && chart.dismissPopupContent) {
- chart.dismissPopupContent();
- }
- });
- }
- });
- /**
- * Dismiss popup content in chart, including export menu and tooltip.
- */
- Chart.prototype.dismissPopupContent = function () {
- var chart = this;
- fireEvent(this, 'dismissPopupContent', {}, function () {
- if (chart.tooltip) {
- chart.tooltip.hide(0);
- }
- chart.hideExportMenu();
- });
- };
- /**
- * 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 _this = this;
- var ep = this.eventProvider = new EventProvider();
- this.chart = chart;
- this.components = components;
- this.modules = [];
- this.currentModuleIx = 0;
- // Run an update to get all modules
- this.update();
- ep.addEvent(this.tabindexContainer, 'keydown', function (e) { return _this.onKeydown(e); });
- ep.addEvent(this.tabindexContainer, 'focus', function (e) { return _this.onFocus(e); });
- ['mouseup', 'touchend'].forEach(function (eventName) {
- return ep.addEvent(doc, eventName, function () { return _this.onMouseUp(); });
- });
- ['mousedown', 'touchstart'].forEach(function (eventName) {
- return ep.addEvent(chart.renderTo, eventName, function () {
- _this.isClickingChart = true;
- });
- });
- ep.addEvent(chart.renderTo, 'mouseover', function () {
- _this.pointerIsOverChart = true;
- });
- ep.addEvent(chart.renderTo, 'mouseout', function () {
- _this.pointerIsOverChart = false;
- });
- // 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);
- }, []);
- this.updateExitAnchor();
- }
- else {
- this.modules = [];
- this.currentModuleIx = 0;
- this.removeExitAnchor();
- }
- },
- /**
- * Function to run on container focus
- * @private
- * @param {global.FocusEvent} e Browser focus event.
- */
- onFocus: function (e) {
- var chart = this.chart;
- var focusComesFromChart = (e.relatedTarget &&
- chart.container.contains(e.relatedTarget));
- // Init keyboard nav if tabbing into chart
- if (!this.exiting &&
- !this.tabbingInBackwards &&
- !this.isClickingChart &&
- !focusComesFromChart &&
- this.modules[0]) {
- this.modules[0].init(1);
- }
- this.exiting = false;
- },
- /**
- * Reset chart navigation state if we click outside the chart and it's
- * not already reset.
- * @private
- */
- onMouseUp: function () {
- delete this.isClickingChart;
- if (!this.keyboardReset && !this.pointerIsOverChart) {
- 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;
- // Used for sending focus out of the chart by the modules.
- this.exiting = 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
- this.exiting = true;
- if (direction > 0) {
- this.exitAnchor.focus();
- }
- else {
- this.tabindexContainer.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), chart = this.chart, container = chart.container;
- var tabindexContainer;
- if (chart.renderTo.hasAttribute('tabindex')) {
- container.removeAttribute('tabindex');
- tabindexContainer = chart.renderTo;
- }
- else {
- tabindexContainer = container;
- }
- this.tabindexContainer = tabindexContainer;
- var curTabindex = tabindexContainer.getAttribute('tabindex');
- if (shouldHaveTabindex && !curTabindex) {
- tabindexContainer.setAttribute('tabindex', '0');
- }
- else if (!shouldHaveTabindex) {
- chart.container.removeAttribute('tabindex');
- }
- },
- /**
- * @private
- */
- makeElementAnExitAnchor: function (el) {
- var chartTabindex = this.tabindexContainer.getAttribute('tabindex') || 0;
- el.setAttribute('class', 'highcharts-exit-anchor');
- el.setAttribute('tabindex', chartTabindex);
- 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');
- 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) {
- // Focus the container instead
- keyboardNavigation.tabbingInBackwards = true;
- keyboardNavigation.tabindexContainer.focus();
- delete keyboardNavigation.tabbingInBackwards;
- 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();
- this.chart.container.removeAttribute('tabindex');
- }
- };
- export default KeyboardNavigation;
|