KeyboardNavigation.js 11 KB

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