keyboard-navigation.src.js 49 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608
  1. /**
  2. * Accessibility module - Keyboard navigation
  3. *
  4. * (c) 2010-2017 Highsoft AS
  5. * Author: Oystein Moseng
  6. *
  7. * License: www.highcharts.com/license
  8. */
  9. 'use strict';
  10. import H from '../parts/Globals.js';
  11. import '../parts/Utilities.js';
  12. import '../parts/Chart.js';
  13. import '../parts/Series.js';
  14. import '../parts/Point.js';
  15. import '../parts/Tooltip.js';
  16. import '../parts/SvgRenderer.js';
  17. var win = H.win,
  18. doc = win.document,
  19. addEvent = H.addEvent,
  20. fireEvent = H.fireEvent,
  21. merge = H.merge,
  22. pick = H.pick;
  23. /*
  24. * Add focus border functionality to SVGElements. Draws a new rect on top of
  25. * element around its bounding box.
  26. */
  27. H.extend(H.SVGElement.prototype, {
  28. /**
  29. * @private
  30. * @function Highcharts.SVGElement#addFocusBorder
  31. *
  32. * @param {number} margin
  33. *
  34. * @param {Higcharts.CSSObject} style
  35. */
  36. addFocusBorder: function (margin, style) {
  37. // Allow updating by just adding new border
  38. if (this.focusBorder) {
  39. this.removeFocusBorder();
  40. }
  41. // Add the border rect
  42. var bb = this.getBBox(),
  43. pad = pick(margin, 3);
  44. this.focusBorder = this.renderer.rect(
  45. bb.x - pad,
  46. bb.y - pad,
  47. bb.width + 2 * pad,
  48. bb.height + 2 * pad,
  49. style && style.borderRadius
  50. )
  51. .addClass('highcharts-focus-border')
  52. .attr({
  53. zIndex: 99
  54. })
  55. .add(this.parentGroup);
  56. if (!this.renderer.styledMode) {
  57. this.focusBorder.attr({
  58. stroke: style && style.stroke,
  59. 'stroke-width': style && style.strokeWidth
  60. });
  61. }
  62. },
  63. /**
  64. * @private
  65. * @function Highcharts.SVGElement#removeFocusBorder
  66. */
  67. removeFocusBorder: function () {
  68. if (this.focusBorder) {
  69. this.focusBorder.destroy();
  70. delete this.focusBorder;
  71. }
  72. }
  73. });
  74. /*
  75. * Set for which series types it makes sense to move to the closest point with
  76. * up/down arrows, and which series types should just move to next series.
  77. */
  78. H.Series.prototype.keyboardMoveVertical = true;
  79. ['column', 'pie'].forEach(function (type) {
  80. if (H.seriesTypes[type]) {
  81. H.seriesTypes[type].prototype.keyboardMoveVertical = false;
  82. }
  83. });
  84. /**
  85. * Strip HTML tags away from a string. Used for aria-label attributes, painting
  86. * on a canvas will fail if the text contains tags.
  87. *
  88. * @private
  89. * @function stripTags
  90. *
  91. * @param {string} s
  92. * The input string
  93. *
  94. * @return {string}
  95. * The filtered string
  96. */
  97. function stripTags(s) {
  98. return typeof s === 'string' ? s.replace(/<\/?[^>]+(>|$)/g, '') : s;
  99. }
  100. /**
  101. * Get the index of a point in a series. This is needed when using e.g. data
  102. * grouping.
  103. *
  104. * @private
  105. * @function getPointIndex
  106. *
  107. * @param {Highcharts.Point} point
  108. * The point to find index of.
  109. *
  110. * @return {number}
  111. * The index in the series.points array of the point.
  112. */
  113. function getPointIndex(point) {
  114. var index = point.index,
  115. points = point.series.points,
  116. i = points.length;
  117. if (points[index] !== point) {
  118. while (i--) {
  119. if (points[i] === point) {
  120. return i;
  121. }
  122. }
  123. } else {
  124. return index;
  125. }
  126. }
  127. // Set default keyboard navigation options
  128. H.setOptions({
  129. /**
  130. * @since 5.0.0
  131. * @optionparent accessibility
  132. */
  133. accessibility: {
  134. /**
  135. * Options for keyboard navigation.
  136. *
  137. * @since 5.0.0
  138. */
  139. keyboardNavigation: {
  140. /**
  141. * Enable keyboard navigation for the chart.
  142. *
  143. * @since 5.0.0
  144. */
  145. enabled: true,
  146. /**
  147. * Options for the focus border drawn around elements while
  148. * navigating through them.
  149. *
  150. * @sample highcharts/accessibility/custom-focus
  151. * Custom focus ring
  152. *
  153. * @since 6.0.3
  154. */
  155. focusBorder: {
  156. /**
  157. * Enable/disable focus border for chart.
  158. *
  159. * @since 6.0.3
  160. */
  161. enabled: true,
  162. /**
  163. * Hide the browser's default focus indicator.
  164. *
  165. * @since 6.0.4
  166. */
  167. hideBrowserFocusOutline: true,
  168. /**
  169. * Style options for the focus border drawn around elements
  170. * while navigating through them. Note that some browsers in
  171. * addition draw their own borders for focused elements. These
  172. * automatic borders can not be styled by Highcharts.
  173. *
  174. * In styled mode, the border is given the
  175. * `.highcharts-focus-border` class.
  176. *
  177. * @type {Highcharts.CSSObject}
  178. * @default {"color": "#335cad", "lineWidth": 2, "borderRadius": 3}
  179. * @since 6.0.3
  180. */
  181. style: {
  182. /** @ignore-option */
  183. color: '#335cad',
  184. /** @ignore-option */
  185. lineWidth: 2,
  186. /** @ignore-option */
  187. borderRadius: 3
  188. },
  189. /**
  190. * Focus border margin around the elements.
  191. *
  192. * @since 6.0.3
  193. */
  194. margin: 2
  195. },
  196. /**
  197. * Set the keyboard navigation mode for the chart. Can be "normal"
  198. * or "serialize". In normal mode, left/right arrow keys move
  199. * between points in a series, while up/down arrow keys move between
  200. * series. Up/down navigation acts intelligently to figure out which
  201. * series makes sense to move to from any given point.
  202. *
  203. * In "serialize" mode, points are instead navigated as a single
  204. * list. Left/right behaves as in "normal" mode. Up/down arrow keys
  205. * will behave like left/right. This is useful for unifying
  206. * navigation behavior with/without screen readers enabled.
  207. *
  208. * @type {string}
  209. * @default normal
  210. * @since 6.0.4
  211. * @validvalue ["normal", "serialize"]
  212. * @apioption accessibility.keyboardNavigation.mode
  213. */
  214. /**
  215. * Skip null points when navigating through points with the
  216. * keyboard.
  217. *
  218. * @since 5.0.0
  219. */
  220. skipNullPoints: true
  221. }
  222. }
  223. });
  224. /**
  225. * Keyboard navigation for the legend. Requires the Accessibility module.
  226. *
  227. * @since 5.0.14
  228. * @apioption legend.keyboardNavigation
  229. */
  230. /**
  231. * Enable/disable keyboard navigation for the legend. Requires the Accessibility
  232. * module.
  233. *
  234. * @see [accessibility.keyboardNavigation](
  235. * #accessibility.keyboardNavigation.enabled)
  236. *
  237. * @type {boolean}
  238. * @default true
  239. * @since 5.0.13
  240. * @apioption legend.keyboardNavigation.enabled
  241. */
  242. /**
  243. * Abstraction layer for keyboard navigation. Keep a map of keyCodes to handler
  244. * functions, and a next/prev move handler for tab order. The module's keyCode
  245. * handlers determine when to move to another module. Validate holds a function
  246. * to determine if there are prerequisites for this module to run that are not
  247. * met. Init holds a function to run once before any keyCodes are interpreted.
  248. * Terminate holds a function to run once before moving to next/prev module.
  249. *
  250. * @private
  251. * @class
  252. * @name KeyboardNavigationModule
  253. *
  254. * @param {Highcharts.Chart} chart
  255. * The chart object keeps track of a list of KeyboardNavigationModules.
  256. *
  257. * @param {*} options
  258. */
  259. function KeyboardNavigationModule(chart, options) {
  260. this.chart = chart;
  261. this.id = options.id;
  262. this.keyCodeMap = options.keyCodeMap;
  263. this.validate = options.validate;
  264. this.init = options.init;
  265. this.terminate = options.terminate;
  266. }
  267. KeyboardNavigationModule.prototype = {
  268. /**
  269. * Find handler function(s) for key code in the keyCodeMap and run it.
  270. *
  271. * @private
  272. * @function KeyboardNavigationModule#run
  273. *
  274. * @param {global.Event} e
  275. *
  276. * @return {boolean}
  277. */
  278. run: function (e) {
  279. var navModule = this,
  280. keyCode = e.which || e.keyCode,
  281. found = false,
  282. handled = false;
  283. this.keyCodeMap.forEach(function (codeSet) {
  284. if (codeSet[0].indexOf(keyCode) > -1) {
  285. found = true;
  286. handled = codeSet[1].call(navModule, keyCode, e) !== false;
  287. }
  288. });
  289. // Default tab handler, move to next/prev module
  290. if (!found && keyCode === 9) {
  291. handled = this.move(e.shiftKey ? -1 : 1);
  292. }
  293. return handled;
  294. },
  295. /**
  296. * Move to next/prev valid module, or undefined if none, and init it.
  297. * Returns true on success and false if there is no valid module to move to.
  298. *
  299. * @private
  300. * @function KeyboardNavigationModule#move
  301. *
  302. * @param {number} direction
  303. *
  304. * @return {boolean}
  305. */
  306. move: function (direction) {
  307. var chart = this.chart;
  308. if (this.terminate) {
  309. this.terminate(direction);
  310. }
  311. chart.keyboardNavigationModuleIndex += direction;
  312. var newModule = chart.keyboardNavigationModules[
  313. chart.keyboardNavigationModuleIndex
  314. ];
  315. // Remove existing focus border if any
  316. if (chart.focusElement) {
  317. chart.focusElement.removeFocusBorder();
  318. }
  319. // Verify new module
  320. if (newModule) {
  321. if (newModule.validate && !newModule.validate()) {
  322. return this.move(direction); // Invalid module, recurse
  323. }
  324. if (newModule.init) {
  325. newModule.init(direction); // Valid module, init it
  326. return true;
  327. }
  328. }
  329. // No module
  330. chart.keyboardNavigationModuleIndex = 0; // Reset counter
  331. // Set focus to chart or exit anchor depending on direction
  332. if (direction > 0) {
  333. this.chart.exiting = true;
  334. this.chart.tabExitAnchor.focus();
  335. } else {
  336. this.chart.renderTo.focus();
  337. }
  338. return false;
  339. }
  340. };
  341. /**
  342. * Utility function to attempt to fake a click event on an element.
  343. *
  344. * @private
  345. * @function fakeClickEvent
  346. *
  347. * @param {Highcharts.HTMLDOMElement|Highcharts.SVGDOMElement}
  348. */
  349. function fakeClickEvent(element) {
  350. var fakeEvent;
  351. if (element && element.onclick && doc.createEvent) {
  352. fakeEvent = doc.createEvent('Events');
  353. fakeEvent.initEvent('click', true, false);
  354. element.onclick(fakeEvent);
  355. }
  356. }
  357. /**
  358. * Determine if a series should be skipped
  359. *
  360. * @private
  361. * @function isSkipSeries
  362. *
  363. * @param {Highcharts.Series} series
  364. *
  365. * @return {boolean}
  366. */
  367. function isSkipSeries(series) {
  368. var a11yOptions = series.chart.options.accessibility;
  369. return series.options.skipKeyboardNavigation ||
  370. series.options.enableMouseTracking === false || // #8440
  371. !series.visible ||
  372. // Skip all points in a series where pointDescriptionThreshold is
  373. // reached
  374. (a11yOptions.pointDescriptionThreshold &&
  375. a11yOptions.pointDescriptionThreshold <= series.points.length);
  376. }
  377. /**
  378. * Determine if a point should be skipped
  379. *
  380. * @private
  381. * @function isSkipPoint
  382. *
  383. * @param {Highcharts.Point} point
  384. *
  385. * @return {boolean}
  386. */
  387. function isSkipPoint(point) {
  388. var a11yOptions = point.series.chart.options.accessibility;
  389. return point.isNull && a11yOptions.keyboardNavigation.skipNullPoints ||
  390. point.visible === false ||
  391. isSkipSeries(point.series);
  392. }
  393. /**
  394. * Get the point in a series that is closest (in distance) to a reference point.
  395. * Optionally supply weight factors for x and y directions.
  396. *
  397. * @private
  398. * @function getClosestPoint
  399. *
  400. * @param {Highcharts.Point} point
  401. *
  402. * @param {Highcharts.Series} series
  403. *
  404. * @param {number} [xWeight]
  405. *
  406. * @param {number} [yWeight]
  407. *
  408. * @return {Highcharts.Point|undefined}
  409. */
  410. function getClosestPoint(point, series, xWeight, yWeight) {
  411. var minDistance = Infinity,
  412. dPoint,
  413. minIx,
  414. distance,
  415. i = series.points.length;
  416. if (point.plotX === undefined || point.plotY === undefined) {
  417. return;
  418. }
  419. while (i--) {
  420. dPoint = series.points[i];
  421. if (dPoint.plotX === undefined || dPoint.plotY === undefined) {
  422. continue;
  423. }
  424. distance = (point.plotX - dPoint.plotX) *
  425. (point.plotX - dPoint.plotX) * (xWeight || 1) +
  426. (point.plotY - dPoint.plotY) *
  427. (point.plotY - dPoint.plotY) * (yWeight || 1);
  428. if (distance < minDistance) {
  429. minDistance = distance;
  430. minIx = i;
  431. }
  432. }
  433. return minIx !== undefined && series.points[minIx];
  434. }
  435. /**
  436. * Pan along axis in a direction (1 or -1), optionally with a defined
  437. * granularity (number of steps it takes to walk across current view)
  438. *
  439. * @private
  440. * @function Highcharts.Axis#panStep
  441. *
  442. * @param {number} direction
  443. *
  444. * @param {number} [granularity]
  445. */
  446. H.Axis.prototype.panStep = function (direction, granularity) {
  447. var gran = granularity || 3,
  448. extremes = this.getExtremes(),
  449. step = (extremes.max - extremes.min) / gran * direction,
  450. newMax = extremes.max + step,
  451. newMin = extremes.min + step,
  452. size = newMax - newMin;
  453. if (direction < 0 && newMin < extremes.dataMin) {
  454. newMin = extremes.dataMin;
  455. newMax = newMin + size;
  456. } else if (direction > 0 && newMax > extremes.dataMax) {
  457. newMax = extremes.dataMax;
  458. newMin = newMax - size;
  459. }
  460. this.setExtremes(newMin, newMax);
  461. };
  462. /**
  463. * Set chart's focus to an SVGElement. Calls focus() on it, and draws the focus
  464. * border.
  465. *
  466. * @private
  467. * @function Highcharts.Chart#setFocusToElement
  468. *
  469. * @param {Highcharts.SVGElement} svgElement
  470. * Element to draw the border around.
  471. *
  472. * @param {Highcharts.SVGElement} [focusElement]
  473. * If supplied, it draws the border around svgElement and sets the focus
  474. * to focusElement.
  475. */
  476. H.Chart.prototype.setFocusToElement = function (svgElement, focusElement) {
  477. var focusBorderOptions = this.options.accessibility
  478. .keyboardNavigation.focusBorder,
  479. browserFocusElement = focusElement || svgElement;
  480. // Set browser focus if possible
  481. if (
  482. browserFocusElement.element &&
  483. browserFocusElement.element.focus
  484. ) {
  485. browserFocusElement.element.focus();
  486. // Hide default focus ring
  487. if (focusBorderOptions.hideBrowserFocusOutline) {
  488. browserFocusElement.css({ outline: 'none' });
  489. }
  490. }
  491. if (focusBorderOptions.enabled) {
  492. // Remove old focus border
  493. if (this.focusElement) {
  494. this.focusElement.removeFocusBorder();
  495. }
  496. // Draw focus border (since some browsers don't do it automatically)
  497. svgElement.addFocusBorder(focusBorderOptions.margin, {
  498. stroke: focusBorderOptions.style.color,
  499. strokeWidth: focusBorderOptions.style.lineWidth,
  500. borderRadius: focusBorderOptions.style.borderRadius
  501. });
  502. this.focusElement = svgElement;
  503. }
  504. };
  505. /**
  506. * Highlights a point (show tooltip and display hover state).
  507. *
  508. * @private
  509. * @function Highcharts.Point#highlight
  510. *
  511. * @return {Highcharts.Point}
  512. * This highlighted point.
  513. */
  514. H.Point.prototype.highlight = function () {
  515. var chart = this.series.chart;
  516. if (!this.isNull) {
  517. this.onMouseOver(); // Show the hover marker and tooltip
  518. } else {
  519. if (chart.tooltip) {
  520. chart.tooltip.hide(0);
  521. }
  522. // Don't call blur on the element, as it messes up the chart div's focus
  523. }
  524. // We focus only after calling onMouseOver because the state change can
  525. // change z-index and mess up the element.
  526. if (this.graphic) {
  527. chart.setFocusToElement(this.graphic);
  528. }
  529. chart.highlightedPoint = this;
  530. return this;
  531. };
  532. /**
  533. * Function to highlight next/previous point in chart.
  534. *
  535. * @private
  536. * @function Highcharts.Chart#highlightAdjacentPoint
  537. *
  538. * @param {boolean} next
  539. * Flag for the direction.
  540. *
  541. * @return {Highcharts.Point|false}
  542. * Returns highlighted point on success, false on failure (no adjacent
  543. * point to highlight in chosen direction).
  544. */
  545. H.Chart.prototype.highlightAdjacentPoint = function (next) {
  546. var chart = this,
  547. series = chart.series,
  548. curPoint = chart.highlightedPoint,
  549. curPointIndex = curPoint && getPointIndex(curPoint) || 0,
  550. curPoints = curPoint && curPoint.series.points,
  551. lastSeries = chart.series && chart.series[chart.series.length - 1],
  552. lastPoint = lastSeries && lastSeries.points &&
  553. lastSeries.points[lastSeries.points.length - 1],
  554. newSeries,
  555. newPoint;
  556. // If no points, return false
  557. if (!series[0] || !series[0].points) {
  558. return false;
  559. }
  560. if (!curPoint) {
  561. // No point is highlighted yet. Try first/last point depending on move
  562. // direction
  563. newPoint = next ? series[0].points[0] : lastPoint;
  564. } else {
  565. // We have a highlighted point.
  566. // Grab next/prev point & series
  567. newSeries = series[curPoint.series.index + (next ? 1 : -1)];
  568. newPoint = curPoints[curPointIndex + (next ? 1 : -1)];
  569. if (!newPoint && newSeries) {
  570. // Done with this series, try next one
  571. newPoint = newSeries.points[next ? 0 : newSeries.points.length - 1];
  572. }
  573. // If there is no adjacent point, we return false
  574. if (!newPoint) {
  575. return false;
  576. }
  577. }
  578. // Recursively skip points
  579. if (isSkipPoint(newPoint)) {
  580. // If we skip this whole series, move to the end of the series before we
  581. // recurse, just to optimize
  582. newSeries = newPoint.series;
  583. if (isSkipSeries(newSeries)) {
  584. chart.highlightedPoint = next ?
  585. newSeries.points[newSeries.points.length - 1] :
  586. newSeries.points[0];
  587. } else {
  588. // Otherwise, just move one point
  589. chart.highlightedPoint = newPoint;
  590. }
  591. // Retry
  592. return chart.highlightAdjacentPoint(next);
  593. }
  594. // There is an adjacent point, highlight it
  595. return newPoint.highlight();
  596. };
  597. /**
  598. * Highlight first valid point in a series. Returns the point if successfully
  599. * highlighted, otherwise false. If there is a highlighted point in the series,
  600. * use that as starting point.
  601. *
  602. * @private
  603. * @function Highcharts.Series#highlightFirstValidPoint
  604. *
  605. * @return {Highcharts.Point|false}
  606. */
  607. H.Series.prototype.highlightFirstValidPoint = function () {
  608. var curPoint = this.chart.highlightedPoint,
  609. start = (curPoint && curPoint.series) === this ?
  610. getPointIndex(curPoint) :
  611. 0,
  612. points = this.points;
  613. if (points) {
  614. for (var i = start, len = points.length; i < len; ++i) {
  615. if (!isSkipPoint(points[i])) {
  616. return points[i].highlight();
  617. }
  618. }
  619. for (var j = start; j >= 0; --j) {
  620. if (!isSkipPoint(points[j])) {
  621. return points[j].highlight();
  622. }
  623. }
  624. }
  625. return false;
  626. };
  627. /**
  628. * Highlight next/previous series in chart. Returns false if no adjacent series
  629. * in the direction, otherwise returns new highlighted point.
  630. *
  631. * @private
  632. * @function Highcharts.Chart#highlightAdjacentSeries
  633. *
  634. * @param {boolean} down
  635. *
  636. * @return {Highcharts.Point|false}
  637. */
  638. H.Chart.prototype.highlightAdjacentSeries = function (down) {
  639. var chart = this,
  640. newSeries,
  641. newPoint,
  642. adjacentNewPoint,
  643. curPoint = chart.highlightedPoint,
  644. lastSeries = chart.series && chart.series[chart.series.length - 1],
  645. lastPoint = lastSeries && lastSeries.points &&
  646. lastSeries.points[lastSeries.points.length - 1];
  647. // If no point is highlighted, highlight the first/last point
  648. if (!chart.highlightedPoint) {
  649. newSeries = down ? (chart.series && chart.series[0]) : lastSeries;
  650. newPoint = down ?
  651. (newSeries && newSeries.points && newSeries.points[0]) : lastPoint;
  652. return newPoint ? newPoint.highlight() : false;
  653. }
  654. newSeries = chart.series[curPoint.series.index + (down ? -1 : 1)];
  655. if (!newSeries) {
  656. return false;
  657. }
  658. // We have a new series in this direction, find the right point
  659. // Weigh xDistance as counting much higher than Y distance
  660. newPoint = getClosestPoint(curPoint, newSeries, 4);
  661. if (!newPoint) {
  662. return false;
  663. }
  664. // New series and point exists, but we might want to skip it
  665. if (isSkipSeries(newSeries)) {
  666. // Skip the series
  667. newPoint.highlight();
  668. adjacentNewPoint = chart.highlightAdjacentSeries(down); // Try recurse
  669. if (!adjacentNewPoint) {
  670. // Recurse failed
  671. curPoint.highlight();
  672. return false;
  673. }
  674. // Recurse succeeded
  675. return adjacentNewPoint;
  676. }
  677. // Highlight the new point or any first valid point back or forwards from it
  678. newPoint.highlight();
  679. return newPoint.series.highlightFirstValidPoint();
  680. };
  681. /**
  682. * Highlight the closest point vertically.
  683. *
  684. * @private
  685. * @function Highcharts.Chart#highlightAdjacentPointVertical
  686. *
  687. * @param {boolean} down
  688. *
  689. * @return {Highcharts.Point|false}
  690. */
  691. H.Chart.prototype.highlightAdjacentPointVertical = function (down) {
  692. var curPoint = this.highlightedPoint,
  693. minDistance = Infinity,
  694. bestPoint;
  695. if (curPoint.plotX === undefined || curPoint.plotY === undefined) {
  696. return false;
  697. }
  698. this.series.forEach(function (series) {
  699. if (isSkipSeries(series)) {
  700. return;
  701. }
  702. series.points.forEach(function (point) {
  703. if (point.plotY === undefined || point.plotX === undefined ||
  704. point === curPoint) {
  705. return;
  706. }
  707. var yDistance = point.plotY - curPoint.plotY,
  708. width = Math.abs(point.plotX - curPoint.plotX),
  709. distance = Math.abs(yDistance) * Math.abs(yDistance) +
  710. width * width * 4; // Weigh horizontal distance highly
  711. // Reverse distance number if axis is reversed
  712. if (series.yAxis.reversed) {
  713. yDistance *= -1;
  714. }
  715. if (
  716. yDistance < 0 && down || yDistance > 0 && !down || // Wrong dir
  717. distance < 5 || // Points in same spot => infinite loop
  718. isSkipPoint(point)
  719. ) {
  720. return;
  721. }
  722. if (distance < minDistance) {
  723. minDistance = distance;
  724. bestPoint = point;
  725. }
  726. });
  727. });
  728. return bestPoint ? bestPoint.highlight() : false;
  729. };
  730. /**
  731. * Show the export menu and focus the first item (if exists).
  732. *
  733. * @private
  734. * @function Highcharts.Chart#showExportMenu
  735. */
  736. H.Chart.prototype.showExportMenu = function () {
  737. if (this.exportSVGElements && this.exportSVGElements[0]) {
  738. this.exportSVGElements[0].element.onclick();
  739. this.highlightExportItem(0);
  740. }
  741. };
  742. /**
  743. * Hide export menu.
  744. *
  745. * @private
  746. * @function Highcharts.Chart#hideExportMenu
  747. */
  748. H.Chart.prototype.hideExportMenu = function () {
  749. var chart = this,
  750. exportList = chart.exportDivElements;
  751. if (exportList && chart.exportContextMenu) {
  752. // Reset hover states etc.
  753. exportList.forEach(function (el) {
  754. if (el.className === 'highcharts-menu-item' && el.onmouseout) {
  755. el.onmouseout();
  756. }
  757. });
  758. chart.highlightedExportItem = 0;
  759. // Hide the menu div
  760. chart.exportContextMenu.hideMenu();
  761. // Make sure the chart has focus and can capture keyboard events
  762. chart.container.focus();
  763. }
  764. };
  765. /**
  766. * Highlight export menu item by index.
  767. *
  768. * @private
  769. * @function Highcharts.Chart#highlightExportItem
  770. *
  771. * @param {number} ix
  772. *
  773. * @return {true|undefined}
  774. */
  775. H.Chart.prototype.highlightExportItem = function (ix) {
  776. var listItem = this.exportDivElements && this.exportDivElements[ix],
  777. curHighlighted =
  778. this.exportDivElements &&
  779. this.exportDivElements[this.highlightedExportItem],
  780. hasSVGFocusSupport;
  781. if (
  782. listItem &&
  783. listItem.tagName === 'DIV' &&
  784. !(listItem.children && listItem.children.length)
  785. ) {
  786. // Test if we have focus support for SVG elements
  787. hasSVGFocusSupport = !!(
  788. this.renderTo.getElementsByTagName('g')[0] || {}
  789. ).focus;
  790. // Only focus if we can set focus back to the elements after
  791. // destroying the menu (#7422)
  792. if (listItem.focus && hasSVGFocusSupport) {
  793. listItem.focus();
  794. }
  795. if (curHighlighted && curHighlighted.onmouseout) {
  796. curHighlighted.onmouseout();
  797. }
  798. if (listItem.onmouseover) {
  799. listItem.onmouseover();
  800. }
  801. this.highlightedExportItem = ix;
  802. return true;
  803. }
  804. };
  805. /**
  806. * Try to highlight the last valid export menu item.
  807. *
  808. * @private
  809. * @function Highcharts.Chart#highlightLastExportItem
  810. */
  811. H.Chart.prototype.highlightLastExportItem = function () {
  812. var chart = this,
  813. i;
  814. if (chart.exportDivElements) {
  815. i = chart.exportDivElements.length;
  816. while (i--) {
  817. if (chart.highlightExportItem(i)) {
  818. break;
  819. }
  820. }
  821. }
  822. };
  823. /**
  824. * Highlight range selector button by index.
  825. *
  826. * @private
  827. * @function Highcharts.Chart#highlightRangeSelectorButton
  828. *
  829. * @param {number} ix
  830. *
  831. * @return {boolean}
  832. */
  833. H.Chart.prototype.highlightRangeSelectorButton = function (ix) {
  834. var buttons = this.rangeSelector.buttons;
  835. // Deselect old
  836. if (buttons[this.highlightedRangeSelectorItemIx]) {
  837. buttons[this.highlightedRangeSelectorItemIx].setState(
  838. this.oldRangeSelectorItemState || 0
  839. );
  840. }
  841. // Select new
  842. this.highlightedRangeSelectorItemIx = ix;
  843. if (buttons[ix]) {
  844. this.setFocusToElement(buttons[ix].box, buttons[ix]);
  845. this.oldRangeSelectorItemState = buttons[ix].state;
  846. buttons[ix].setState(2);
  847. return true;
  848. }
  849. return false;
  850. };
  851. /**
  852. * Highlight legend item by index.
  853. *
  854. * @private
  855. * @function Highcharts.Chart#highlightLegendItem
  856. *
  857. * @param {number} ix
  858. *
  859. * @return {boolean}
  860. */
  861. H.Chart.prototype.highlightLegendItem = function (ix) {
  862. var items = this.legend.allItems,
  863. oldIx = this.highlightedLegendItemIx;
  864. if (items[ix]) {
  865. if (items[oldIx]) {
  866. fireEvent(
  867. items[oldIx].legendGroup.element,
  868. 'mouseout'
  869. );
  870. }
  871. // Scroll if we have to
  872. if (items[ix].pageIx !== undefined &&
  873. items[ix].pageIx + 1 !== this.legend.currentPage) {
  874. this.legend.scroll(1 + items[ix].pageIx - this.legend.currentPage);
  875. }
  876. // Focus
  877. this.highlightedLegendItemIx = ix;
  878. this.setFocusToElement(items[ix].legendItem, items[ix].legendGroup);
  879. fireEvent(items[ix].legendGroup.element, 'mouseover');
  880. return true;
  881. }
  882. return false;
  883. };
  884. /**
  885. * Add keyboard navigation handling modules to chart.
  886. *
  887. * @private
  888. * @function Highcharts.Chart#addKeyboardNavigationModules
  889. */
  890. H.Chart.prototype.addKeyboardNavigationModules = function () {
  891. var chart = this;
  892. /**
  893. * @private
  894. * @function navModuleFactory
  895. *
  896. * @param {string} id
  897. *
  898. * @param {Array<Array<number>,Function>} keyMap
  899. *
  900. * @param {Highcharts.Dictionary<Function>} options
  901. *
  902. * @return {KeyboardNavigationModule}
  903. */
  904. function navModuleFactory(id, keyMap, options) {
  905. return new KeyboardNavigationModule(chart, merge({
  906. keyCodeMap: keyMap
  907. }, { id: id }, options));
  908. }
  909. /**
  910. * List of the different keyboard handling modes we use depending on where
  911. * we are in the chart. Each mode has a set of handling functions mapped to
  912. * key codes. Each mode determines when to move to the next/prev mode.
  913. *
  914. * @private
  915. * @name Highcharts.Chart#keyboardNavigationModules
  916. * @type {Array<KeyboardNavigationModule>}
  917. */
  918. chart.keyboardNavigationModules = [
  919. // Entry point catching the first tab, allowing users to tab into points
  920. // more intuitively.
  921. navModuleFactory('entry', []),
  922. // Points
  923. navModuleFactory('points', [
  924. // Left/Right
  925. [[37, 39], function (keyCode) {
  926. var right = keyCode === 39;
  927. if (!chart.highlightAdjacentPoint(right)) {
  928. // Failed to highlight next, wrap to last/first
  929. return this.init(right ? 1 : -1);
  930. }
  931. return true;
  932. }],
  933. // Up/Down
  934. [[38, 40], function (keyCode) {
  935. var down = keyCode !== 38,
  936. navOptions = chart.options.accessibility.keyboardNavigation;
  937. if (navOptions.mode && navOptions.mode === 'serialize') {
  938. // Act like left/right
  939. if (!chart.highlightAdjacentPoint(down)) {
  940. return this.init(down ? 1 : -1);
  941. }
  942. return true;
  943. }
  944. // Normal mode, move between series
  945. var highlightMethod = chart.highlightedPoint &&
  946. chart.highlightedPoint.series.keyboardMoveVertical ?
  947. 'highlightAdjacentPointVertical' :
  948. 'highlightAdjacentSeries';
  949. chart[highlightMethod](down);
  950. return true;
  951. }],
  952. // Enter/Spacebar
  953. [[13, 32], function () {
  954. if (chart.highlightedPoint) {
  955. chart.highlightedPoint.firePointEvent('click');
  956. }
  957. }]
  958. ], {
  959. // Always start highlighting from scratch when entering this module
  960. init: function (dir) {
  961. var numSeries = chart.series.length,
  962. i = dir > 0 ? 0 : numSeries,
  963. res;
  964. if (dir > 0) {
  965. delete chart.highlightedPoint;
  966. // Find first valid point to highlight
  967. while (i < numSeries) {
  968. res = chart.series[i].highlightFirstValidPoint();
  969. if (res) {
  970. return res;
  971. }
  972. ++i;
  973. }
  974. } else {
  975. // Find last valid point to highlight
  976. while (i--) {
  977. chart.highlightedPoint = chart.series[i].points[
  978. chart.series[i].points.length - 1
  979. ];
  980. // Highlight first valid point in the series will also
  981. // look backwards. It always starts from currently
  982. // highlighted point.
  983. res = chart.series[i].highlightFirstValidPoint();
  984. if (res) {
  985. return res;
  986. }
  987. }
  988. }
  989. },
  990. // If leaving points, don't show tooltip anymore
  991. terminate: function () {
  992. if (chart.tooltip) {
  993. chart.tooltip.hide(0);
  994. }
  995. delete chart.highlightedPoint;
  996. }
  997. }),
  998. // Exporting
  999. navModuleFactory('exporting', [
  1000. // Left/Up
  1001. [[37, 38], function () {
  1002. var i = chart.highlightedExportItem || 0,
  1003. reachedEnd = true;
  1004. // Try to highlight prev item in list. Highlighting e.g.
  1005. // separators will fail.
  1006. while (i--) {
  1007. if (chart.highlightExportItem(i)) {
  1008. reachedEnd = false;
  1009. break;
  1010. }
  1011. }
  1012. if (reachedEnd) {
  1013. chart.highlightLastExportItem();
  1014. return true;
  1015. }
  1016. }],
  1017. // Right/Down
  1018. [[39, 40], function () {
  1019. var highlightedExportItem = chart.highlightedExportItem || 0,
  1020. reachedEnd = true;
  1021. // Try to highlight next item in list. Highlighting e.g.
  1022. // separators will fail.
  1023. for (
  1024. var i = highlightedExportItem + 1;
  1025. i < chart.exportDivElements.length;
  1026. ++i
  1027. ) {
  1028. if (chart.highlightExportItem(i)) {
  1029. reachedEnd = false;
  1030. break;
  1031. }
  1032. }
  1033. if (reachedEnd) {
  1034. chart.highlightExportItem(0);
  1035. return true;
  1036. }
  1037. }],
  1038. // Enter/Spacebar
  1039. [[13, 32], function () {
  1040. fakeClickEvent(
  1041. chart.exportDivElements[chart.highlightedExportItem]
  1042. );
  1043. }]
  1044. ], {
  1045. // Only run exporting navigation if exporting support exists and is
  1046. // enabled on chart
  1047. validate: function () {
  1048. return (
  1049. chart.exportChart &&
  1050. !(
  1051. chart.options.exporting &&
  1052. chart.options.exporting.enabled === false
  1053. )
  1054. );
  1055. },
  1056. // Show export menu
  1057. init: function (direction) {
  1058. chart.highlightedPoint = null;
  1059. chart.showExportMenu();
  1060. // If coming back to export menu from other module, try to
  1061. // highlight last item in menu
  1062. if (direction < 0) {
  1063. chart.highlightLastExportItem();
  1064. }
  1065. },
  1066. // Hide the menu
  1067. terminate: function () {
  1068. chart.hideExportMenu();
  1069. }
  1070. }),
  1071. // Map zoom
  1072. navModuleFactory('mapZoom', [
  1073. // Up/down/left/right
  1074. [[38, 40, 37, 39], function (keyCode) {
  1075. chart[keyCode === 38 || keyCode === 40 ? 'yAxis' : 'xAxis'][0]
  1076. .panStep(keyCode < 39 ? -1 : 1);
  1077. }],
  1078. // Tabs
  1079. [[9], function (keyCode, e) {
  1080. var button;
  1081. // Deselect old
  1082. chart.mapNavButtons[chart.focusedMapNavButtonIx].setState(0);
  1083. if (
  1084. e.shiftKey && !chart.focusedMapNavButtonIx ||
  1085. !e.shiftKey && chart.focusedMapNavButtonIx
  1086. ) { // trying to go somewhere we can't?
  1087. chart.mapZoom(); // Reset zoom
  1088. // Nowhere to go, go to prev/next module
  1089. return this.move(e.shiftKey ? -1 : 1);
  1090. }
  1091. chart.focusedMapNavButtonIx += e.shiftKey ? -1 : 1;
  1092. button = chart.mapNavButtons[chart.focusedMapNavButtonIx];
  1093. chart.setFocusToElement(button.box, button);
  1094. button.setState(2);
  1095. }],
  1096. // Enter/Spacebar
  1097. [[13, 32], function () {
  1098. fakeClickEvent(
  1099. chart.mapNavButtons[chart.focusedMapNavButtonIx].element
  1100. );
  1101. }]
  1102. ], {
  1103. // Only run this module if we have map zoom on the chart
  1104. validate: function () {
  1105. return (
  1106. chart.mapZoom &&
  1107. chart.mapNavButtons &&
  1108. chart.mapNavButtons.length === 2
  1109. );
  1110. },
  1111. // Make zoom buttons do their magic
  1112. init: function (direction) {
  1113. var zoomIn = chart.mapNavButtons[0],
  1114. zoomOut = chart.mapNavButtons[1],
  1115. initialButton = direction > 0 ? zoomIn : zoomOut;
  1116. chart.mapNavButtons.forEach(function (button, i) {
  1117. button.element.setAttribute('tabindex', -1);
  1118. button.element.setAttribute('role', 'button');
  1119. button.element.setAttribute(
  1120. 'aria-label',
  1121. chart.langFormat(
  1122. 'accessibility.mapZoom' + (i ? 'Out' : 'In'),
  1123. { chart: chart }
  1124. )
  1125. );
  1126. });
  1127. chart.setFocusToElement(initialButton.box, initialButton);
  1128. initialButton.setState(2);
  1129. chart.focusedMapNavButtonIx = direction > 0 ? 0 : 1;
  1130. }
  1131. }),
  1132. // Highstock range selector (minus input boxes)
  1133. navModuleFactory('rangeSelector', [
  1134. // Left/Right/Up/Down
  1135. [[37, 39, 38, 40], function (keyCode) {
  1136. var direction = (keyCode === 37 || keyCode === 38) ? -1 : 1;
  1137. // Try to highlight next/prev button
  1138. if (
  1139. !chart.highlightRangeSelectorButton(
  1140. chart.highlightedRangeSelectorItemIx + direction
  1141. )
  1142. ) {
  1143. return this.move(direction);
  1144. }
  1145. }],
  1146. // Enter/Spacebar
  1147. [[13, 32], function () {
  1148. // Don't allow click if button used to be disabled
  1149. if (chart.oldRangeSelectorItemState !== 3) {
  1150. fakeClickEvent(
  1151. chart.rangeSelector.buttons[
  1152. chart.highlightedRangeSelectorItemIx
  1153. ].element
  1154. );
  1155. }
  1156. }]
  1157. ], {
  1158. // Only run this module if we have range selector
  1159. validate: function () {
  1160. return (
  1161. chart.rangeSelector &&
  1162. chart.rangeSelector.buttons &&
  1163. chart.rangeSelector.buttons.length
  1164. );
  1165. },
  1166. // Make elements focusable and accessible
  1167. init: function (direction) {
  1168. chart.rangeSelector.buttons.forEach(function (button) {
  1169. button.element.setAttribute('tabindex', '-1');
  1170. button.element.setAttribute('role', 'button');
  1171. button.element.setAttribute(
  1172. 'aria-label',
  1173. chart.langFormat(
  1174. 'accessibility.rangeSelectorButton',
  1175. {
  1176. chart: chart,
  1177. buttonText: button.text && button.text.textStr
  1178. }
  1179. )
  1180. );
  1181. });
  1182. // Focus first/last button
  1183. chart.highlightRangeSelectorButton(
  1184. direction > 0 ? 0 : chart.rangeSelector.buttons.length - 1
  1185. );
  1186. }
  1187. }),
  1188. // Highstock range selector, input boxes
  1189. navModuleFactory('rangeSelectorInput', [
  1190. // Tab/Up/Down
  1191. [[9, 38, 40], function (keyCode, e) {
  1192. var direction =
  1193. (keyCode === 9 && e.shiftKey || keyCode === 38) ? -1 : 1,
  1194. newIx = chart.highlightedInputRangeIx =
  1195. chart.highlightedInputRangeIx + direction;
  1196. // Try to highlight next/prev item in list.
  1197. if (newIx > 1 || newIx < 0) { // Out of range
  1198. return this.move(direction);
  1199. }
  1200. chart.rangeSelector[newIx ? 'maxInput' : 'minInput'].focus();
  1201. }]
  1202. ], {
  1203. // Only run if we have range selector with input boxes
  1204. validate: function () {
  1205. var inputVisible = (
  1206. chart.rangeSelector &&
  1207. chart.rangeSelector.inputGroup &&
  1208. chart.rangeSelector.inputGroup.element
  1209. .getAttribute('visibility') !== 'hidden'
  1210. );
  1211. return (
  1212. inputVisible &&
  1213. chart.options.rangeSelector.inputEnabled !== false &&
  1214. chart.rangeSelector.minInput &&
  1215. chart.rangeSelector.maxInput
  1216. );
  1217. },
  1218. // Highlight first/last input box
  1219. init: function (direction) {
  1220. chart.highlightedInputRangeIx = direction > 0 ? 0 : 1;
  1221. chart.rangeSelector[
  1222. chart.highlightedInputRangeIx ? 'maxInput' : 'minInput'
  1223. ].focus();
  1224. }
  1225. }),
  1226. // Legend navigation
  1227. navModuleFactory('legend', [
  1228. // Left/Right/Up/Down
  1229. [[37, 39, 38, 40], function (keyCode) {
  1230. var direction = (keyCode === 37 || keyCode === 38) ? -1 : 1;
  1231. // Try to highlight next/prev legend item
  1232. if (!chart.highlightLegendItem(
  1233. chart.highlightedLegendItemIx + direction
  1234. ) && chart.legend.allItems.length > 1) {
  1235. // Wrap around if more than 1 item
  1236. this.init(direction);
  1237. }
  1238. }],
  1239. // Enter/Spacebar
  1240. [[13, 32], function () {
  1241. var legendElement = chart.legend.allItems[
  1242. chart.highlightedLegendItemIx
  1243. ].legendItem.element;
  1244. fakeClickEvent(
  1245. !chart.legend.options.useHTML ? // #8561
  1246. legendElement.parentNode : legendElement
  1247. );
  1248. }]
  1249. ], {
  1250. // Only run this module if we have at least one legend - wait for
  1251. // it - item. Don't run if the legend is populated by a colorAxis.
  1252. // Don't run if legend navigation is disabled.
  1253. validate: function () {
  1254. return chart.legend && chart.legend.allItems &&
  1255. chart.legend.display &&
  1256. !(chart.colorAxis && chart.colorAxis.length) &&
  1257. (chart.options.legend &&
  1258. chart.options.legend.keyboardNavigation &&
  1259. chart.options.legend.keyboardNavigation.enabled) !== false;
  1260. },
  1261. // Make elements focusable and accessible
  1262. init: function (direction) {
  1263. chart.legend.allItems.forEach(function (item) {
  1264. item.legendGroup.element.setAttribute('tabindex', '-1');
  1265. item.legendGroup.element.setAttribute('role', 'button');
  1266. item.legendGroup.element.setAttribute(
  1267. 'aria-label',
  1268. chart.langFormat(
  1269. 'accessibility.legendItem',
  1270. {
  1271. chart: chart,
  1272. itemName: stripTags(item.name)
  1273. }
  1274. )
  1275. );
  1276. });
  1277. // Focus first/last item
  1278. chart.highlightLegendItem(
  1279. direction > 0 ? 0 : chart.legend.allItems.length - 1
  1280. );
  1281. }
  1282. })
  1283. ];
  1284. };
  1285. /**
  1286. * Add exit anchor to the chart. We use this to move focus out of chart whenever
  1287. * we want, by setting focus to this div and not preventing the default tab
  1288. * action. We also use this when users come back into the chart by tabbing back,
  1289. * in order to navigate from the end of the chart.
  1290. *
  1291. * @private
  1292. * @function Highcharts.Chart#addExitAnchor
  1293. *
  1294. * @return {Function}
  1295. * Returns the unbind function for the exit anchor's event handler.
  1296. */
  1297. H.Chart.prototype.addExitAnchor = function () {
  1298. var chart = this;
  1299. chart.tabExitAnchor = doc.createElement('div');
  1300. chart.tabExitAnchor.setAttribute('tabindex', '0');
  1301. // Hide exit anchor
  1302. merge(true, chart.tabExitAnchor.style, {
  1303. position: 'absolute',
  1304. left: '-9999px',
  1305. top: 'auto',
  1306. width: '1px',
  1307. height: '1px',
  1308. overflow: 'hidden'
  1309. });
  1310. chart.renderTo.appendChild(chart.tabExitAnchor);
  1311. return addEvent(
  1312. chart.tabExitAnchor,
  1313. 'focus',
  1314. function (ev) {
  1315. var e = ev || win.event,
  1316. curModule;
  1317. // If focusing and we are exiting, do nothing once.
  1318. if (!chart.exiting) {
  1319. // Not exiting, means we are coming in backwards
  1320. chart.renderTo.focus();
  1321. e.preventDefault();
  1322. // Move to last valid keyboard nav module
  1323. // Note the we don't run it, just set the index
  1324. chart.keyboardNavigationModuleIndex =
  1325. chart.keyboardNavigationModules.length - 1;
  1326. curModule = chart.keyboardNavigationModules[
  1327. chart.keyboardNavigationModuleIndex
  1328. ];
  1329. // Validate the module
  1330. if (curModule.validate && !curModule.validate()) {
  1331. // Invalid.
  1332. // Move inits next valid module in direction
  1333. curModule.move(-1);
  1334. } else {
  1335. // We have a valid module, init it
  1336. curModule.init(-1);
  1337. }
  1338. } else {
  1339. // Don't skip the next focus, we only skip once.
  1340. chart.exiting = false;
  1341. }
  1342. }
  1343. );
  1344. };
  1345. /**
  1346. * Clear the chart and reset the navigation state.
  1347. *
  1348. * @private
  1349. * @function Highcharts.Chart#resetKeyboardNavigation
  1350. */
  1351. H.Chart.prototype.resetKeyboardNavigation = function () {
  1352. var chart = this,
  1353. curMod = (
  1354. chart.keyboardNavigationModules &&
  1355. chart.keyboardNavigationModules[
  1356. chart.keyboardNavigationModuleIndex || 0
  1357. ]
  1358. );
  1359. if (curMod && curMod.terminate) {
  1360. curMod.terminate();
  1361. }
  1362. if (chart.focusElement) {
  1363. chart.focusElement.removeFocusBorder();
  1364. }
  1365. chart.keyboardNavigationModuleIndex = 0;
  1366. chart.keyboardReset = true;
  1367. };
  1368. // On destroy, we need to clean up the focus border and the state.
  1369. H.addEvent(H.Series, 'destroy', function () {
  1370. var chart = this.chart;
  1371. if (chart.highlightedPoint && chart.highlightedPoint.series === this) {
  1372. delete chart.highlightedPoint;
  1373. if (chart.focusElement) {
  1374. chart.focusElement.removeFocusBorder();
  1375. }
  1376. }
  1377. });
  1378. // Add keyboard navigation events on chart load.
  1379. H.Chart.prototype.callbacks.push(function (chart) {
  1380. var a11yOptions = chart.options.accessibility;
  1381. if (a11yOptions.enabled && a11yOptions.keyboardNavigation.enabled) {
  1382. // Init nav modules. We start at the first module, and as the user
  1383. // navigates through the chart the index will increase to use different
  1384. // handler modules.
  1385. chart.addKeyboardNavigationModules();
  1386. chart.keyboardNavigationModuleIndex = 0;
  1387. // Make chart container reachable by tab
  1388. if (
  1389. chart.container.hasAttribute &&
  1390. !chart.container.hasAttribute('tabIndex')
  1391. ) {
  1392. chart.container.setAttribute('tabindex', '0');
  1393. }
  1394. // Add tab exit anchor
  1395. if (!chart.tabExitAnchor) {
  1396. chart.unbindExitAnchorFocus = chart.addExitAnchor();
  1397. }
  1398. // Handle keyboard events by routing them to active keyboard nav module
  1399. chart.unbindKeydownHandler = addEvent(chart.renderTo, 'keydown',
  1400. function (ev) {
  1401. var e = ev || win.event,
  1402. curNavModule = chart.keyboardNavigationModules[
  1403. chart.keyboardNavigationModuleIndex
  1404. ];
  1405. chart.keyboardReset = false;
  1406. // If there is a nav module for the current index, run it.
  1407. // Otherwise, we are outside of the chart in some direction.
  1408. if (curNavModule) {
  1409. if (curNavModule.run(e)) {
  1410. // Successfully handled this key event, stop default
  1411. e.preventDefault();
  1412. }
  1413. }
  1414. });
  1415. // Reset chart navigation state if we click outside the chart and it's
  1416. // not already reset
  1417. chart.unbindBlurHandler = addEvent(doc, 'mouseup', function () {
  1418. if (
  1419. !chart.keyboardReset &&
  1420. !(chart.pointer && chart.pointer.chartPosition)
  1421. ) {
  1422. chart.resetKeyboardNavigation();
  1423. }
  1424. });
  1425. // Add cleanup handlers
  1426. addEvent(chart, 'destroy', function () {
  1427. chart.resetKeyboardNavigation();
  1428. if (chart.unbindExitAnchorFocus && chart.tabExitAnchor) {
  1429. chart.unbindExitAnchorFocus();
  1430. }
  1431. if (chart.unbindKeydownHandler && chart.renderTo) {
  1432. chart.unbindKeydownHandler();
  1433. }
  1434. if (chart.unbindBlurHandler) {
  1435. chart.unbindBlurHandler();
  1436. }
  1437. });
  1438. }
  1439. });