KeyboardNavigation.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. /* *
  2. *
  3. * (c) 2009-2020 Øystein Moseng
  4. *
  5. * Main keyboard navigation handling.
  6. *
  7. * License: www.highcharts.com/license
  8. *
  9. * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
  10. *
  11. * */
  12. 'use strict';
  13. import H from '../Core/Globals.js';
  14. var doc = H.doc, win = H.win;
  15. import U from '../Core/Utilities.js';
  16. var addEvent = U.addEvent, fireEvent = U.fireEvent;
  17. import HTMLUtilities from './Utils/HTMLUtilities.js';
  18. var getElement = HTMLUtilities.getElement;
  19. import EventProvider from './Utils/EventProvider.js';
  20. /* eslint-disable valid-jsdoc */
  21. // Add event listener to document to detect ESC key press and dismiss
  22. // hover/popup content.
  23. addEvent(doc, 'keydown', function (e) {
  24. var keycode = e.which || e.keyCode;
  25. var esc = 27;
  26. if (keycode === esc && H.charts) {
  27. H.charts.forEach(function (chart) {
  28. if (chart && chart.dismissPopupContent) {
  29. chart.dismissPopupContent();
  30. }
  31. });
  32. }
  33. });
  34. /**
  35. * Dismiss popup content in chart, including export menu and tooltip.
  36. */
  37. H.Chart.prototype.dismissPopupContent = function () {
  38. var chart = this;
  39. fireEvent(this, 'dismissPopupContent', {}, function () {
  40. if (chart.tooltip) {
  41. chart.tooltip.hide(0);
  42. }
  43. chart.hideExportMenu();
  44. });
  45. };
  46. /**
  47. * The KeyboardNavigation class, containing the overall keyboard navigation
  48. * logic for the chart.
  49. *
  50. * @requires module:modules/accessibility
  51. *
  52. * @private
  53. * @class
  54. * @param {Highcharts.Chart} chart
  55. * Chart object
  56. * @param {object} components
  57. * Map of component names to AccessibilityComponent objects.
  58. * @name Highcharts.KeyboardNavigation
  59. */
  60. function KeyboardNavigation(chart, components) {
  61. this.init(chart, components);
  62. }
  63. KeyboardNavigation.prototype = {
  64. /**
  65. * Initialize the class
  66. * @private
  67. * @param {Highcharts.Chart} chart
  68. * Chart object
  69. * @param {object} components
  70. * Map of component names to AccessibilityComponent objects.
  71. */
  72. init: function (chart, components) {
  73. var _this = this;
  74. var ep = this.eventProvider = new EventProvider();
  75. this.chart = chart;
  76. this.components = components;
  77. this.modules = [];
  78. this.currentModuleIx = 0;
  79. // Run an update to get all modules
  80. this.update();
  81. ep.addEvent(this.tabindexContainer, 'keydown', function (e) { return _this.onKeydown(e); });
  82. ep.addEvent(this.tabindexContainer, 'focus', function (e) { return _this.onFocus(e); });
  83. ep.addEvent(doc, 'mouseup', function () { return _this.onMouseUp(); });
  84. ep.addEvent(chart.renderTo, 'mousedown', function () {
  85. _this.isClickingChart = true;
  86. });
  87. ep.addEvent(chart.renderTo, 'mouseover', function () {
  88. _this.pointerIsOverChart = true;
  89. });
  90. ep.addEvent(chart.renderTo, 'mouseout', function () {
  91. _this.pointerIsOverChart = false;
  92. });
  93. // Init first module
  94. if (this.modules.length) {
  95. this.modules[0].init(1);
  96. }
  97. },
  98. /**
  99. * Update the modules for the keyboard navigation.
  100. * @param {Array<string>} [order]
  101. * Array specifying the tab order of the components.
  102. */
  103. update: function (order) {
  104. var a11yOptions = this.chart.options.accessibility, keyboardOptions = a11yOptions && a11yOptions.keyboardNavigation, components = this.components;
  105. this.updateContainerTabindex();
  106. if (keyboardOptions &&
  107. keyboardOptions.enabled &&
  108. order &&
  109. order.length) {
  110. // We (still) have keyboard navigation. Update module list
  111. this.modules = order.reduce(function (modules, componentName) {
  112. var navModules = components[componentName].getKeyboardNavigation();
  113. return modules.concat(navModules);
  114. }, []);
  115. this.updateExitAnchor();
  116. }
  117. else {
  118. this.modules = [];
  119. this.currentModuleIx = 0;
  120. this.removeExitAnchor();
  121. }
  122. },
  123. /**
  124. * Function to run on container focus
  125. * @private
  126. * @param {global.FocusEvent} e Browser focus event.
  127. */
  128. onFocus: function (e) {
  129. var _a;
  130. var chart = this.chart;
  131. var focusComesFromChart = (e.relatedTarget &&
  132. chart.container.contains(e.relatedTarget));
  133. // Init keyboard nav if tabbing into chart
  134. if (!this.isClickingChart && !focusComesFromChart) {
  135. (_a = this.modules[0]) === null || _a === void 0 ? void 0 : _a.init(1);
  136. }
  137. },
  138. /**
  139. * Reset chart navigation state if we click outside the chart and it's
  140. * not already reset.
  141. * @private
  142. */
  143. onMouseUp: function () {
  144. delete this.isClickingChart;
  145. if (!this.keyboardReset && !this.pointerIsOverChart) {
  146. var chart = this.chart, curMod = this.modules &&
  147. this.modules[this.currentModuleIx || 0];
  148. if (curMod && curMod.terminate) {
  149. curMod.terminate();
  150. }
  151. if (chart.focusElement) {
  152. chart.focusElement.removeFocusBorder();
  153. }
  154. this.currentModuleIx = 0;
  155. this.keyboardReset = true;
  156. }
  157. },
  158. /**
  159. * Function to run on keydown
  160. * @private
  161. * @param {global.KeyboardEvent} ev Browser keydown event.
  162. */
  163. onKeydown: function (ev) {
  164. var e = ev || win.event, preventDefault, curNavModule = this.modules && this.modules.length &&
  165. this.modules[this.currentModuleIx];
  166. // Used for resetting nav state when clicking outside chart
  167. this.keyboardReset = false;
  168. // If there is a nav module for the current index, run it.
  169. // Otherwise, we are outside of the chart in some direction.
  170. if (curNavModule) {
  171. var response = curNavModule.run(e);
  172. if (response === curNavModule.response.success) {
  173. preventDefault = true;
  174. }
  175. else if (response === curNavModule.response.prev) {
  176. preventDefault = this.prev();
  177. }
  178. else if (response === curNavModule.response.next) {
  179. preventDefault = this.next();
  180. }
  181. if (preventDefault) {
  182. e.preventDefault();
  183. e.stopPropagation();
  184. }
  185. }
  186. },
  187. /**
  188. * Go to previous module.
  189. * @private
  190. */
  191. prev: function () {
  192. return this.move(-1);
  193. },
  194. /**
  195. * Go to next module.
  196. * @private
  197. */
  198. next: function () {
  199. return this.move(1);
  200. },
  201. /**
  202. * Move to prev/next module.
  203. * @private
  204. * @param {number} direction
  205. * Direction to move. +1 for next, -1 for prev.
  206. * @return {boolean}
  207. * True if there was a valid module in direction.
  208. */
  209. move: function (direction) {
  210. var curModule = this.modules && this.modules[this.currentModuleIx];
  211. if (curModule && curModule.terminate) {
  212. curModule.terminate(direction);
  213. }
  214. // Remove existing focus border if any
  215. if (this.chart.focusElement) {
  216. this.chart.focusElement.removeFocusBorder();
  217. }
  218. this.currentModuleIx += direction;
  219. var newModule = this.modules && this.modules[this.currentModuleIx];
  220. if (newModule) {
  221. if (newModule.validate && !newModule.validate()) {
  222. return this.move(direction); // Invalid module, recurse
  223. }
  224. if (newModule.init) {
  225. newModule.init(direction); // Valid module, init it
  226. return true;
  227. }
  228. }
  229. // No module
  230. this.currentModuleIx = 0; // Reset counter
  231. // Set focus to chart or exit anchor depending on direction
  232. if (direction > 0) {
  233. this.exiting = true;
  234. this.exitAnchor.focus();
  235. }
  236. else {
  237. this.tabindexContainer.focus();
  238. }
  239. return false;
  240. },
  241. /**
  242. * We use an exit anchor to move focus out of chart whenever we want, by
  243. * setting focus to this div and not preventing the default tab action. We
  244. * also use this when users come back into the chart by tabbing back, in
  245. * order to navigate from the end of the chart.
  246. * @private
  247. */
  248. updateExitAnchor: function () {
  249. var endMarkerId = 'highcharts-end-of-chart-marker-' + this.chart.index, endMarker = getElement(endMarkerId);
  250. this.removeExitAnchor();
  251. if (endMarker) {
  252. this.makeElementAnExitAnchor(endMarker);
  253. this.exitAnchor = endMarker;
  254. }
  255. else {
  256. this.createExitAnchor();
  257. }
  258. },
  259. /**
  260. * Chart container should have tabindex if navigation is enabled.
  261. * @private
  262. */
  263. updateContainerTabindex: function () {
  264. var a11yOptions = this.chart.options.accessibility, keyboardOptions = a11yOptions && a11yOptions.keyboardNavigation, shouldHaveTabindex = !(keyboardOptions && keyboardOptions.enabled === false), chart = this.chart, container = chart.container;
  265. var tabindexContainer;
  266. if (chart.renderTo.hasAttribute('tabindex')) {
  267. container.removeAttribute('tabindex');
  268. tabindexContainer = chart.renderTo;
  269. }
  270. else {
  271. tabindexContainer = container;
  272. }
  273. this.tabindexContainer = tabindexContainer;
  274. var curTabindex = tabindexContainer.getAttribute('tabindex');
  275. if (shouldHaveTabindex && !curTabindex) {
  276. tabindexContainer.setAttribute('tabindex', '0');
  277. }
  278. else if (!shouldHaveTabindex) {
  279. chart.container.removeAttribute('tabindex');
  280. }
  281. },
  282. /**
  283. * @private
  284. */
  285. makeElementAnExitAnchor: function (el) {
  286. var chartTabindex = this.tabindexContainer.getAttribute('tabindex') || 0;
  287. el.setAttribute('class', 'highcharts-exit-anchor');
  288. el.setAttribute('tabindex', chartTabindex);
  289. el.setAttribute('aria-hidden', false);
  290. // Handle focus
  291. this.addExitAnchorEventsToEl(el);
  292. },
  293. /**
  294. * Add new exit anchor to the chart.
  295. *
  296. * @private
  297. */
  298. createExitAnchor: function () {
  299. var chart = this.chart, exitAnchor = this.exitAnchor = doc.createElement('div');
  300. chart.renderTo.appendChild(exitAnchor);
  301. this.makeElementAnExitAnchor(exitAnchor);
  302. },
  303. /**
  304. * @private
  305. */
  306. removeExitAnchor: function () {
  307. if (this.exitAnchor && this.exitAnchor.parentNode) {
  308. this.exitAnchor.parentNode
  309. .removeChild(this.exitAnchor);
  310. delete this.exitAnchor;
  311. }
  312. },
  313. /**
  314. * @private
  315. */
  316. addExitAnchorEventsToEl: function (element) {
  317. var chart = this.chart, keyboardNavigation = this;
  318. this.eventProvider.addEvent(element, 'focus', function (ev) {
  319. var e = ev || win.event, curModule, focusComesFromChart = (e.relatedTarget &&
  320. chart.container.contains(e.relatedTarget)), comingInBackwards = !(focusComesFromChart || keyboardNavigation.exiting);
  321. if (comingInBackwards) {
  322. keyboardNavigation.tabindexContainer.focus();
  323. e.preventDefault();
  324. // Move to last valid keyboard nav module
  325. // Note the we don't run it, just set the index
  326. if (keyboardNavigation.modules &&
  327. keyboardNavigation.modules.length) {
  328. keyboardNavigation.currentModuleIx =
  329. keyboardNavigation.modules.length - 1;
  330. curModule = keyboardNavigation.modules[keyboardNavigation.currentModuleIx];
  331. // Validate the module
  332. if (curModule &&
  333. curModule.validate && !curModule.validate()) {
  334. // Invalid. Try moving backwards to find next valid.
  335. keyboardNavigation.prev();
  336. }
  337. else if (curModule) {
  338. // We have a valid module, init it
  339. curModule.init(-1);
  340. }
  341. }
  342. }
  343. else {
  344. // Don't skip the next focus, we only skip once.
  345. keyboardNavigation.exiting = false;
  346. }
  347. });
  348. },
  349. /**
  350. * Remove all traces of keyboard navigation.
  351. * @private
  352. */
  353. destroy: function () {
  354. this.removeExitAnchor();
  355. this.eventProvider.removeAddedEvents();
  356. this.chart.container.removeAttribute('tabindex');
  357. }
  358. };
  359. export default KeyboardNavigation;