accessibility.src.js 98 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630
  1. /**
  2. * @license Highcharts JS v6.1.0 (2018-04-13)
  3. * Accessibility module
  4. *
  5. * (c) 2010-2017 Highsoft AS
  6. * Author: Oystein Moseng
  7. *
  8. * License: www.highcharts.com/license
  9. */
  10. 'use strict';
  11. (function (factory) {
  12. if (typeof module === 'object' && module.exports) {
  13. module.exports = factory;
  14. } else {
  15. factory(Highcharts);
  16. }
  17. }(function (Highcharts) {
  18. (function (H) {
  19. /**
  20. * Accessibility module - internationalization support
  21. *
  22. * (c) 2010-2018 Highsoft AS
  23. * Author: Øystein Moseng
  24. *
  25. * License: www.highcharts.com/license
  26. */
  27. var each = H.each,
  28. pick = H.pick;
  29. /**
  30. * String trim that works for IE6-8 as well.
  31. * @param {string} str The input string
  32. * @return {string} The trimmed string
  33. */
  34. function stringTrim(str) {
  35. return str.trim && str.trim() || str.replace(/^\s+|\s+$/g, '');
  36. }
  37. /**
  38. * i18n utility function. Format a single array or plural statement in a format
  39. * string. If the statement is not an array or plural statement, returns the
  40. * statement within brackets. Invalid array statements return an empty string.
  41. */
  42. function formatExtendedStatement(statement, ctx) {
  43. var eachStart = statement.indexOf('#each('),
  44. pluralStart = statement.indexOf('#plural('),
  45. indexStart = statement.indexOf('['),
  46. indexEnd = statement.indexOf(']'),
  47. arr,
  48. result;
  49. // Dealing with an each-function?
  50. if (eachStart > -1) {
  51. var eachEnd = statement.slice(eachStart).indexOf(')') + eachStart,
  52. preEach = statement.substring(0, eachStart),
  53. postEach = statement.substring(eachEnd + 1),
  54. eachStatement = statement.substring(eachStart + 6, eachEnd),
  55. eachArguments = eachStatement.split(','),
  56. lenArg = Number(eachArguments[1]),
  57. len;
  58. result = '';
  59. arr = ctx[eachArguments[0]];
  60. if (arr) {
  61. lenArg = isNaN(lenArg) ? arr.length : lenArg;
  62. len = lenArg < 0 ?
  63. arr.length + lenArg :
  64. Math.min(lenArg, arr.length); // Overshoot
  65. // Run through the array for the specified length
  66. for (var i = 0; i < len; ++i) {
  67. result += preEach + arr[i] + postEach;
  68. }
  69. }
  70. return result.length ? result : '';
  71. }
  72. // Dealing with a plural-function?
  73. if (pluralStart > -1) {
  74. var pluralEnd = statement.slice(pluralStart).indexOf(')') + pluralStart,
  75. pluralStatement = statement.substring(pluralStart + 8, pluralEnd),
  76. pluralArguments = pluralStatement.split(','),
  77. num = Number(ctx[pluralArguments[0]]);
  78. switch (num) {
  79. case 0:
  80. result = pick(pluralArguments[4], pluralArguments[1]);
  81. break;
  82. case 1:
  83. result = pick(pluralArguments[2], pluralArguments[1]);
  84. break;
  85. case 2:
  86. result = pick(pluralArguments[3], pluralArguments[1]);
  87. break;
  88. default:
  89. result = pluralArguments[1];
  90. }
  91. return result ? stringTrim(result) : '';
  92. }
  93. // Array index
  94. if (indexStart > -1) {
  95. var arrayName = statement.substring(0, indexStart),
  96. ix = Number(statement.substring(indexStart + 1, indexEnd)),
  97. val;
  98. arr = ctx[arrayName];
  99. if (!isNaN(ix) && arr) {
  100. if (ix < 0) {
  101. val = arr[arr.length + ix];
  102. // Handle negative overshoot
  103. if (val === undefined) {
  104. val = arr[0];
  105. }
  106. } else {
  107. val = arr[ix];
  108. // Handle positive overshoot
  109. if (val === undefined) {
  110. val = arr[arr.length - 1];
  111. }
  112. }
  113. }
  114. return val !== undefined ? val : '';
  115. }
  116. // Standard substitution, delegate to H.format or similar
  117. return '{' + statement + '}';
  118. }
  119. /**
  120. * i18n formatting function. Extends H.format() functionality by also handling
  121. * arrays and plural conditionals. Arrays can be indexed as follows:
  122. *
  123. * Format: 'This is the first index: {myArray[0]}. The last: {myArray[-1]}.'
  124. * Context: { myArray: [0, 1, 2, 3, 4, 5] }
  125. * Result: 'This is the first index: 0. The last: 5.'
  126. *
  127. * They can also be iterated using the #each() function. This will repeat the
  128. * contents of the bracket expression for each element. Example:
  129. *
  130. * Format: 'List contains: {#each(myArray)cm }'
  131. * Context: { myArray: [0, 1, 2] }
  132. * Result: 'List contains: 0cm 1cm 2cm '
  133. *
  134. * The #each() function optionally takes a length parameter. If positive, this
  135. * parameter specifies the max number of elements to iterate through. If
  136. * negative, the function will subtract the number from the length of the array.
  137. * Use this to stop iterating before the array ends. Example:
  138. *
  139. * Format: 'List contains: {#each(myArray, -1) }and {myArray[-1]}.'
  140. * Context: { myArray: [0, 1, 2, 3] }
  141. * Result: 'List contains: 0, 1, 2, and 3.'
  142. *
  143. * Use the #plural() function to pick a string depending on whether or not a
  144. * context object is 1. Arguments are #plural(obj, plural, singular). Example:
  145. *
  146. * Format: 'Has {numPoints} {#plural(numPoints, points, point}.'
  147. * Context: { numPoints: 5 }
  148. * Result: 'Has 5 points.'
  149. *
  150. * Optionally there are additional parameters for dual and none:
  151. * #plural(obj,plural,singular,dual,none)
  152. * Example:
  153. *
  154. * Format: 'Has {#plural(numPoints, many points, one point, two points, none}.'
  155. * Context: { numPoints: 2 }
  156. * Result: 'Has two points.'
  157. *
  158. * The dual or none parameters will take precedence if they are supplied.
  159. *
  160. * @param {string} formatString The string to format.
  161. * @param {object} context Context to apply to the format string.
  162. * @param {Time} time A `Time` instance for date formatting, passed on to
  163. * H.format().
  164. * @return {string} The formatted string.
  165. */
  166. H.i18nFormat = function (formatString, context, time) {
  167. var getFirstBracketStatement = function (sourceStr, offset) {
  168. var str = sourceStr.slice(offset || 0),
  169. startBracket = str.indexOf('{'),
  170. endBracket = str.indexOf('}');
  171. if (startBracket > -1 && endBracket > startBracket) {
  172. return {
  173. statement: str.substring(startBracket + 1, endBracket),
  174. begin: offset + startBracket + 1,
  175. end: offset + endBracket
  176. };
  177. }
  178. },
  179. tokens = [],
  180. bracketRes,
  181. constRes,
  182. cursor = 0;
  183. // Tokenize format string into bracket statements and constants
  184. do {
  185. bracketRes = getFirstBracketStatement(formatString, cursor);
  186. constRes = formatString.substring(
  187. cursor,
  188. bracketRes && bracketRes.begin - 1
  189. );
  190. // If we have constant content before this bracket statement, add it
  191. if (constRes.length) {
  192. tokens.push({
  193. value: constRes,
  194. type: 'constant'
  195. });
  196. }
  197. // Add the bracket statement
  198. if (bracketRes) {
  199. tokens.push({
  200. value: bracketRes.statement,
  201. type: 'statement'
  202. });
  203. }
  204. cursor = bracketRes && bracketRes.end + 1;
  205. } while (bracketRes);
  206. // Perform the formatting. The formatArrayStatement function returns the
  207. // statement in brackets if it is not an array statement, which means it
  208. // gets picked up by H.format below.
  209. each(tokens, function (token) {
  210. if (token.type === 'statement') {
  211. token.value = formatExtendedStatement(token.value, context);
  212. }
  213. });
  214. // Join string back together and pass to H.format to pick up non-array
  215. // statements.
  216. return H.format(H.reduce(tokens, function (acc, cur) {
  217. return acc + cur.value;
  218. }, ''), context, time);
  219. };
  220. /**
  221. * Apply context to a format string from lang options of the chart.
  222. * @param {string} langKey Key (using dot notation) into lang option structure
  223. * @param {object} context Context to apply to the format string
  224. * @return {string} The formatted string
  225. */
  226. H.Chart.prototype.langFormat = function (langKey, context, time) {
  227. var keys = langKey.split('.'),
  228. formatString = this.options.lang,
  229. i = 0;
  230. for (; i < keys.length; ++i) {
  231. formatString = formatString && formatString[keys[i]];
  232. }
  233. return typeof formatString === 'string' && H.i18nFormat(
  234. formatString, context, time
  235. );
  236. };
  237. H.setOptions({
  238. lang: {
  239. /**
  240. * Configure the accessibility strings in the chart. Requires the
  241. * [accessibility module](//code.highcharts.com/modules/accessibility.
  242. * js) to be loaded. For a description of the module and information
  243. * on its features, see [Highcharts Accessibility](http://www.highcharts.
  244. * com/docs/chart-concepts/accessibility).
  245. *
  246. * For more dynamic control over the accessibility functionality, see
  247. * [accessibility.pointDescriptionFormatter](
  248. * accessibility.pointDescriptionFormatter),
  249. * [accessibility.seriesDescriptionFormatter](
  250. * accessibility.seriesDescriptionFormatter), and
  251. * [accessibility.screenReaderSectionFormatter](
  252. * accessibility.screenReaderSectionFormatter).
  253. *
  254. * @since 6.0.6
  255. * @type {Object}
  256. * @optionparent lang.accessibility
  257. */
  258. accessibility: {
  259. /* eslint-disable max-len */
  260. screenReaderRegionLabel: 'Chart screen reader information.',
  261. navigationHint: 'Use regions/landmarks to skip ahead to chart {#plural(numSeries, and navigate between data series,)}',
  262. defaultChartTitle: 'Chart',
  263. longDescriptionHeading: 'Long description.',
  264. noDescription: 'No description available.',
  265. structureHeading: 'Structure.',
  266. viewAsDataTable: 'View as data table.',
  267. chartHeading: 'Chart graphic.',
  268. chartContainerLabel: 'Interactive chart. {title}. Use up and down arrows to navigate with most screen readers.',
  269. rangeSelectorMinInput: 'Select start date.',
  270. rangeSelectorMaxInput: 'Select end date.',
  271. tableSummary: 'Table representation of chart.',
  272. mapZoomIn: 'Zoom chart',
  273. mapZoomOut: 'Zoom out chart',
  274. rangeSelectorButton: 'Select range {buttonText}',
  275. legendItem: 'Toggle visibility of series {itemName}',
  276. /**
  277. * Title element text for the chart SVG element. Leave this
  278. * empty to disable adding the title element. Browsers will display
  279. * this content when hovering over elements in the chart. Assistive
  280. * technology may use this element to label the chart.
  281. *
  282. * @since 6.0.8
  283. */
  284. svgContainerTitle: '{chartTitle}',
  285. /**
  286. * Descriptions of lesser known series types. The relevant
  287. * description is added to the screen reader information region
  288. * when these series types are used.
  289. *
  290. * @since 6.0.6
  291. * @type {Object}
  292. * @optionparent lang.accessibility.seriesTypeDescriptions
  293. */
  294. seriesTypeDescriptions: {
  295. boxplot: 'Box plot charts are typically used to display ' +
  296. 'groups of statistical data. Each data point in the ' +
  297. 'chart can have up to 5 values: minimum, lower quartile, ' +
  298. 'median, upper quartile, and maximum.',
  299. arearange: 'Arearange charts are line charts displaying a ' +
  300. 'range between a lower and higher value for each point.',
  301. areasplinerange: 'These charts are line charts displaying a ' +
  302. 'range between a lower and higher value for each point.',
  303. bubble: 'Bubble charts are scatter charts where each data ' +
  304. 'point also has a size value.',
  305. columnrange: 'Columnrange charts are column charts ' +
  306. 'displaying a range between a lower and higher value for ' +
  307. 'each point.',
  308. errorbar: 'Errorbar series are used to display the ' +
  309. 'variability of the data.',
  310. funnel: 'Funnel charts are used to display reduction of data ' +
  311. 'in stages.',
  312. pyramid: 'Pyramid charts consist of a single pyramid with ' +
  313. 'item heights corresponding to each point value.',
  314. waterfall: 'A waterfall chart is a column chart where each ' +
  315. 'column contributes towards a total end value.'
  316. },
  317. /**
  318. * Chart type description strings. This is added to the chart
  319. * information region.
  320. *
  321. * If there is only a single series type used in the chart, we use
  322. * the format string for the series type, or default if missing.
  323. * There is one format string for cases where there is only a single
  324. * series in the chart, and one for multiple series of the same
  325. * type.
  326. *
  327. * @since 6.0.6
  328. * @type {Object}
  329. * @optionparent lang.accessibility.chartTypes
  330. */
  331. chartTypes: {
  332. emptyChart: 'Empty chart',
  333. mapTypeDescription: 'Map of {mapTitle} with {numSeries} data series.',
  334. unknownMap: 'Map of unspecified region with {numSeries} data series.',
  335. combinationChart: 'Combination chart with {numSeries} data series.',
  336. defaultSingle: 'Chart with {numPoints} data {#plural(numPoints, points, point)}.',
  337. defaultMultiple: 'Chart with {numSeries} data series.',
  338. splineSingle: 'Line chart with {numPoints} data {#plural(numPoints, points, point)}.',
  339. splineMultiple: 'Line chart with {numSeries} lines.',
  340. lineSingle: 'Line chart with {numPoints} data {#plural(numPoints, points, point)}.',
  341. lineMultiple: 'Line chart with {numSeries} lines.',
  342. columnSingle: 'Bar chart with {numPoints} {#plural(numPoints, bars, bar)}.',
  343. columnMultiple: 'Bar chart with {numSeries} data series.',
  344. barSingle: 'Bar chart with {numPoints} {#plural(numPoints, bars, bar)}.',
  345. barMultiple: 'Bar chart with {numSeries} data series.',
  346. pieSingle: 'Pie chart with {numPoints} {#plural(numPoints, slices, slice)}.',
  347. pieMultiple: 'Pie chart with {numSeries} pies.',
  348. scatterSingle: 'Scatter chart with {numPoints} {#plural(numPoints, points, point)}.',
  349. scatterMultiple: 'Scatter chart with {numSeries} data series.',
  350. boxplotSingle: 'Boxplot with {numPoints} {#plural(numPoints, boxes, box)}.',
  351. boxplotMultiple: 'Boxplot with {numSeries} data series.',
  352. bubbleSingle: 'Bubble chart with {numPoints} {#plural(numPoints, bubbles, bubble)}.',
  353. bubbleMultiple: 'Bubble chart with {numSeries} data series.'
  354. },
  355. /**
  356. * Axis description format strings.
  357. *
  358. * @since 6.0.6
  359. * @type {Object}
  360. * @optionparent lang.accessibility.axis
  361. */
  362. axis: {
  363. xAxisDescriptionSingular: 'The chart has 1 X axis displaying {names[0]}.',
  364. xAxisDescriptionPlural: 'The chart has {numAxes} X axes displaying {#each(names, -1) }and {names[-1]}',
  365. yAxisDescriptionSingular: 'The chart has 1 Y axis displaying {names[0]}.',
  366. yAxisDescriptionPlural: 'The chart has {numAxes} Y axes displaying {#each(names, -1) }and {names[-1]}'
  367. },
  368. /**
  369. * Exporting menu format strings for accessibility module.
  370. *
  371. * @since 6.0.6
  372. * @type {Object}
  373. * @optionparent lang.accessibility.exporting
  374. */
  375. exporting: {
  376. chartMenuLabel: 'Chart export',
  377. menuButtonLabel: 'View export menu',
  378. exportRegionLabel: 'Chart export menu'
  379. },
  380. /**
  381. * Lang configuration for different series types. For more dynamic
  382. * control over the series element descriptions, see
  383. * [accessibility.seriesDescriptionFormatter](
  384. * accessibility.seriesDescriptionFormatter).
  385. *
  386. * @since 6.0.6
  387. * @type {Object}
  388. * @optionparent lang.accessibility.series
  389. */
  390. series: {
  391. /**
  392. * Lang configuration for the series main summary. Each series
  393. * type has two modes:
  394. * 1. This series type is the only series type used in the
  395. * chart
  396. * 2. This is a combination chart with multiple series types
  397. *
  398. * If a definition does not exist for the specific series type
  399. * and mode, the 'default' lang definitions are used.
  400. *
  401. * @since 6.0.6
  402. * @type {Object}
  403. * @optionparent lang.accessibility.series.summary
  404. */
  405. summary: {
  406. default: '{name}, series {ix} of {numSeries} with {numPoints} data {#plural(numPoints, points, point)}.',
  407. defaultCombination: '{name}, series {ix} of {numSeries} with {numPoints} data {#plural(numPoints, points, point)}.',
  408. line: '{name}, line {ix} of {numSeries} with {numPoints} data {#plural(numPoints, points, point)}.',
  409. lineCombination: '{name}, series {ix} of {numSeries}. Line with {numPoints} data {#plural(numPoints, points, point)}.',
  410. spline: '{name}, line {ix} of {numSeries} with {numPoints} data {#plural(numPoints, points, point)}.',
  411. splineCombination: '{name}, series {ix} of {numSeries}. Line with {numPoints} data {#plural(numPoints, points, point)}.',
  412. column: '{name}, bar series {ix} of {numSeries} with {numPoints} {#plural(numPoints, bars, bar)}.',
  413. columnCombination: '{name}, series {ix} of {numSeries}. Bar series with {numPoints} {#plural(numPoints, bars, bar)}.',
  414. bar: '{name}, bar series {ix} of {numSeries} with {numPoints} {#plural(numPoints, bars, bar)}.',
  415. barCombination: '{name}, series {ix} of {numSeries}. Bar series with {numPoints} {#plural(numPoints, bars, bar)}.',
  416. pie: '{name}, pie {ix} of {numSeries} with {numPoints} {#plural(numPoints, slices, slice)}.',
  417. pieCombination: '{name}, series {ix} of {numSeries}. Pie with {numPoints} {#plural(numPoints, slices, slice)}.',
  418. scatter: '{name}, scatter plot {ix} of {numSeries} with {numPoints} {#plural(numPoints, points, point)}.',
  419. scatterCombination: '{name}, series {ix} of {numSeries}, scatter plot with {numPoints} {#plural(numPoints, points, point)}.',
  420. boxplot: '{name}, boxplot {ix} of {numSeries} with {numPoints} {#plural(numPoints, boxes, box)}.',
  421. boxplotCombination: '{name}, series {ix} of {numSeries}. Boxplot with {numPoints} {#plural(numPoints, boxes, box)}.',
  422. bubble: '{name}, bubble series {ix} of {numSeries} with {numPoints} {#plural(numPoints, bubbles, bubble)}.',
  423. bubbleCombination: '{name}, series {ix} of {numSeries}. Bubble series with {numPoints} {#plural(numPoints, bubbles, bubble)}.',
  424. map: '{name}, map {ix} of {numSeries} with {numPoints} {#plural(numPoints, areas, area)}.',
  425. mapCombination: '{name}, series {ix} of {numSeries}. Map with {numPoints} {#plural(numPoints, areas, area)}.',
  426. mapline: '{name}, line {ix} of {numSeries} with {numPoints} data {#plural(numPoints, points, point)}.',
  427. maplineCombination: '{name}, series {ix} of {numSeries}. Line with {numPoints} data {#plural(numPoints, points, point)}.',
  428. mapbubble: '{name}, bubble series {ix} of {numSeries} with {numPoints} {#plural(numPoints, bubbles, bubble)}.',
  429. mapbubbleCombination: '{name}, series {ix} of {numSeries}. Bubble series with {numPoints} {#plural(numPoints, bubbles, bubble)}.'
  430. },
  431. /* eslint-enable max-len */
  432. /**
  433. * User supplied description text. This is added after the main
  434. * summary if present.
  435. *
  436. * @type {String}
  437. * @since 6.0.6
  438. */
  439. description: '{description}',
  440. /**
  441. * xAxis description for series if there are multiple xAxes in
  442. * the chart.
  443. *
  444. * @type {String}
  445. * @since 6.0.6
  446. */
  447. xAxisDescription: 'X axis, {name}',
  448. /**
  449. * yAxis description for series if there are multiple yAxes in
  450. * the chart.
  451. *
  452. * @type {String}
  453. * @since 6.0.6
  454. */
  455. yAxisDescription: 'Y axis, {name}'
  456. }
  457. }
  458. }
  459. });
  460. }(Highcharts));
  461. (function (H) {
  462. /**
  463. * Accessibility module - Screen Reader support
  464. *
  465. * (c) 2010-2017 Highsoft AS
  466. * Author: Oystein Moseng
  467. *
  468. * License: www.highcharts.com/license
  469. */
  470. var win = H.win,
  471. doc = win.document,
  472. each = H.each,
  473. map = H.map,
  474. erase = H.erase,
  475. addEvent = H.addEvent,
  476. merge = H.merge,
  477. // CSS style to hide element from visual users while still exposing it to
  478. // screen readers
  479. hiddenStyle = {
  480. position: 'absolute',
  481. left: '-9999px',
  482. top: 'auto',
  483. width: '1px',
  484. height: '1px',
  485. overflow: 'hidden'
  486. };
  487. // If a point has one of the special keys defined, we expose all keys to the
  488. // screen reader.
  489. H.Series.prototype.commonKeys = ['name', 'id', 'category', 'x', 'value', 'y'];
  490. H.Series.prototype.specialKeys = [
  491. 'z', 'open', 'high', 'q3', 'median', 'q1', 'low', 'close'
  492. ];
  493. if (H.seriesTypes.pie) {
  494. // A pie is always simple. Don't quote me on that.
  495. H.seriesTypes.pie.prototype.specialKeys = [];
  496. }
  497. /**
  498. * HTML encode some characters vulnerable for XSS.
  499. * @param {string} html The input string
  500. * @return {string} The excaped string
  501. */
  502. function htmlencode(html) {
  503. return html
  504. .replace(/&/g, '&amp;')
  505. .replace(/</g, '&lt;')
  506. .replace(/>/g, '&gt;')
  507. .replace(/"/g, '&quot;')
  508. .replace(/'/g, '&#x27;')
  509. .replace(/\//g, '&#x2F;');
  510. }
  511. /**
  512. * Strip HTML tags away from a string. Used for aria-label attributes, painting
  513. * on a canvas will fail if the text contains tags.
  514. * @param {String} s The input string
  515. * @return {String} The filtered string
  516. */
  517. function stripTags(s) {
  518. return typeof s === 'string' ? s.replace(/<\/?[^>]+(>|$)/g, '') : s;
  519. }
  520. /**
  521. * Accessibility options
  522. */
  523. H.setOptions({
  524. /**
  525. * Options for configuring accessibility for the chart. Requires the
  526. * [accessibility module](//code.highcharts.com/modules/accessibility.
  527. * js) to be loaded. For a description of the module and information
  528. * on its features, see [Highcharts Accessibility](http://www.highcharts.
  529. * com/docs/chart-concepts/accessibility).
  530. *
  531. * @since 5.0.0
  532. * @type {Object}
  533. * @optionparent accessibility
  534. */
  535. accessibility: {
  536. /**
  537. * Whether or not to add series descriptions to charts with a single
  538. * series.
  539. *
  540. * @type {Boolean}
  541. * @default false
  542. * @since 5.0.0
  543. * @apioption accessibility.describeSingleSeries
  544. */
  545. /**
  546. * Function to run upon clicking the "View as Data Table" link in the
  547. * screen reader region.
  548. *
  549. * By default Highcharts will insert and set focus to a data table
  550. * representation of the chart.
  551. *
  552. * @type {Function}
  553. * @since 5.0.0
  554. * @apioption accessibility.onTableAnchorClick
  555. */
  556. /**
  557. * Date format to use for points on datetime axes when describing them
  558. * to screen reader users.
  559. *
  560. * Defaults to the same format as in tooltip.
  561. *
  562. * For an overview of the replacement codes, see
  563. * [dateFormat](#Highcharts.dateFormat).
  564. *
  565. * @type {String}
  566. * @see [pointDateFormatter](#accessibility.pointDateFormatter)
  567. * @since 5.0.0
  568. * @apioption accessibility.pointDateFormat
  569. */
  570. /**
  571. * Formatter function to determine the date/time format used with
  572. * points on datetime axes when describing them to screen reader users.
  573. * Receives one argument, `point`, referring to the point to describe.
  574. * Should return a date format string compatible with
  575. * [dateFormat](#Highcharts.dateFormat).
  576. *
  577. * @type {Function}
  578. * @see [pointDateFormat](#accessibility.pointDateFormat)
  579. * @since 5.0.0
  580. * @apioption accessibility.pointDateFormatter
  581. */
  582. /**
  583. * Formatter function to use instead of the default for point
  584. * descriptions.
  585. * Receives one argument, `point`, referring to the point to describe.
  586. * Should return a String with the description of the point for a screen
  587. * reader user.
  588. *
  589. * @type {Function}
  590. * @see [point.description](#series.line.data.description)
  591. * @since 5.0.0
  592. * @apioption accessibility.pointDescriptionFormatter
  593. */
  594. /**
  595. * Formatter function to use instead of the default for series
  596. * descriptions. Receives one argument, `series`, referring to the
  597. * series to describe. Should return a String with the description of
  598. * the series for a screen reader user.
  599. *
  600. * @type {Function}
  601. * @see [series.description](#plotOptions.series.description)
  602. * @since 5.0.0
  603. * @apioption accessibility.seriesDescriptionFormatter
  604. */
  605. /**
  606. * Enable accessibility features for the chart.
  607. *
  608. * @type {Boolean}
  609. * @default true
  610. * @since 5.0.0
  611. */
  612. enabled: true,
  613. /**
  614. * When a series contains more points than this, we no longer expose
  615. * information about individual points to screen readers.
  616. *
  617. * Set to `false` to disable.
  618. *
  619. * @type {Number|Boolean}
  620. * @since 5.0.0
  621. */
  622. pointDescriptionThreshold: false, // set to false to disable
  623. /**
  624. * A formatter function to create the HTML contents of the hidden screen
  625. * reader information region. Receives one argument, `chart`, referring
  626. * to the chart object. Should return a String with the HTML content
  627. * of the region.
  628. *
  629. * The link to view the chart as a data table will be added
  630. * automatically after the custom HTML content.
  631. *
  632. * @type {Function}
  633. * @default undefined
  634. * @since 5.0.0
  635. */
  636. screenReaderSectionFormatter: function (chart) {
  637. var options = chart.options,
  638. chartTypes = chart.types || [],
  639. formatContext = {
  640. chart: chart,
  641. numSeries: chart.series && chart.series.length
  642. },
  643. // Build axis info - but not for pies and maps. Consider not
  644. // adding for certain other types as well (funnel, pyramid?)
  645. axesDesc = (
  646. chartTypes.length === 1 && chartTypes[0] === 'pie' ||
  647. chartTypes[0] === 'map'
  648. ) && {} || chart.getAxesDescription();
  649. return '<div>' + chart.langFormat(
  650. 'accessibility.navigationHint', formatContext
  651. ) + '</div><h3>' +
  652. (
  653. options.title.text ?
  654. htmlencode(options.title.text) :
  655. chart.langFormat(
  656. 'accessibility.defaultChartTitle', formatContext
  657. )
  658. ) +
  659. (
  660. options.subtitle && options.subtitle.text ?
  661. '. ' + htmlencode(options.subtitle.text) :
  662. ''
  663. ) +
  664. '</h3><h4>' + chart.langFormat(
  665. 'accessibility.longDescriptionHeading', formatContext
  666. ) + '</h4><div>' +
  667. (
  668. options.chart.description || chart.langFormat(
  669. 'accessibility.noDescription', formatContext
  670. )
  671. ) +
  672. '</div><h4>' + chart.langFormat(
  673. 'accessibility.structureHeading', formatContext
  674. ) + '</h4><div>' +
  675. (
  676. options.chart.typeDescription ||
  677. chart.getTypeDescription()
  678. ) + '</div>' +
  679. (axesDesc.xAxis ? (
  680. '<div>' + axesDesc.xAxis + '</div>'
  681. ) : '') +
  682. (axesDesc.yAxis ? (
  683. '<div>' + axesDesc.yAxis + '</div>'
  684. ) : '');
  685. }
  686. }
  687. });
  688. /**
  689. * A text description of the chart.
  690. *
  691. * If the Accessibility module is loaded, this is included by default
  692. * as a long description of the chart and its contents in the hidden
  693. * screen reader information region.
  694. *
  695. * @type {String}
  696. * @see [typeDescription](#chart.typeDescription)
  697. * @default undefined
  698. * @since 5.0.0
  699. * @apioption chart.description
  700. */
  701. /**
  702. * A text description of the chart type.
  703. *
  704. * If the Accessibility module is loaded, this will be included in the
  705. * description of the chart in the screen reader information region.
  706. *
  707. *
  708. * Highcharts will by default attempt to guess the chart type, but for
  709. * more complex charts it is recommended to specify this property for
  710. * clarity.
  711. *
  712. * @type {String}
  713. * @default undefined
  714. * @since 5.0.0
  715. * @apioption chart.typeDescription
  716. */
  717. // Utility function. Reverses child nodes of a DOM element
  718. function reverseChildNodes(node) {
  719. var i = node.childNodes.length;
  720. while (i--) {
  721. node.appendChild(node.childNodes[i]);
  722. }
  723. }
  724. // Whenever drawing series, put info on DOM elements
  725. H.addEvent(H.Series, 'afterRender', function () {
  726. if (this.chart.options.accessibility.enabled) {
  727. this.setA11yDescription();
  728. }
  729. });
  730. // Put accessible info on series and points of a series
  731. H.Series.prototype.setA11yDescription = function () {
  732. var a11yOptions = this.chart.options.accessibility,
  733. firstPointEl = (
  734. this.points &&
  735. this.points.length &&
  736. this.points[0].graphic &&
  737. this.points[0].graphic.element
  738. ),
  739. seriesEl = (
  740. firstPointEl &&
  741. firstPointEl.parentNode || this.graph &&
  742. this.graph.element || this.group &&
  743. this.group.element
  744. ); // Could be tracker series depending on series type
  745. if (seriesEl) {
  746. // For some series types the order of elements do not match the order of
  747. // points in series. In that case we have to reverse them in order for
  748. // AT to read them out in an understandable order
  749. if (seriesEl.lastChild === firstPointEl) {
  750. reverseChildNodes(seriesEl);
  751. }
  752. // Make individual point elements accessible if possible. Note: If
  753. // markers are disabled there might not be any elements there to make
  754. // accessible.
  755. if (
  756. this.points && (
  757. this.points.length < a11yOptions.pointDescriptionThreshold ||
  758. a11yOptions.pointDescriptionThreshold === false
  759. )
  760. ) {
  761. each(this.points, function (point) {
  762. if (point.graphic) {
  763. point.graphic.element.setAttribute('role', 'img');
  764. point.graphic.element.setAttribute('tabindex', '-1');
  765. point.graphic.element.setAttribute('aria-label', stripTags(
  766. point.series.options.pointDescriptionFormatter &&
  767. point.series.options.pointDescriptionFormatter(point) ||
  768. a11yOptions.pointDescriptionFormatter &&
  769. a11yOptions.pointDescriptionFormatter(point) ||
  770. point.buildPointInfoString()
  771. ));
  772. }
  773. });
  774. }
  775. // Make series element accessible
  776. if (this.chart.series.length > 1 || a11yOptions.describeSingleSeries) {
  777. seriesEl.setAttribute(
  778. 'role',
  779. this.options.exposeElementToA11y ? 'img' : 'region'
  780. );
  781. seriesEl.setAttribute('tabindex', '-1');
  782. seriesEl.setAttribute(
  783. 'aria-label',
  784. stripTags(
  785. a11yOptions.seriesDescriptionFormatter &&
  786. a11yOptions.seriesDescriptionFormatter(this) ||
  787. this.buildSeriesInfoString()
  788. )
  789. );
  790. }
  791. }
  792. };
  793. // Return string with information about series
  794. H.Series.prototype.buildSeriesInfoString = function () {
  795. var chart = this.chart,
  796. desc = this.description || this.options.description,
  797. description = desc && chart.langFormat(
  798. 'accessibility.series.description', {
  799. description: desc,
  800. series: this
  801. }
  802. ),
  803. xAxisInfo = chart.langFormat(
  804. 'accessibility.series.xAxisDescription',
  805. {
  806. name: this.xAxis && this.xAxis.getDescription(),
  807. series: this
  808. }
  809. ),
  810. yAxisInfo = chart.langFormat(
  811. 'accessibility.series.yAxisDescription',
  812. {
  813. name: this.yAxis && this.yAxis.getDescription(),
  814. series: this
  815. }
  816. ),
  817. summaryContext = {
  818. name: this.name || '',
  819. ix: this.index + 1,
  820. numSeries: chart.series.length,
  821. numPoints: this.points.length,
  822. series: this
  823. },
  824. combination = chart.types.length === 1 ? '' : 'Combination',
  825. summary = chart.langFormat(
  826. 'accessibility.series.summary.' + this.type + combination,
  827. summaryContext
  828. ) || chart.langFormat(
  829. 'accessibility.series.summary.default' + combination,
  830. summaryContext
  831. );
  832. return summary + (description ? ' ' + description : '') + (
  833. chart.yAxis.length > 1 && this.yAxis ?
  834. ' ' + yAxisInfo : ''
  835. ) + (
  836. chart.xAxis.length > 1 && this.xAxis ?
  837. ' ' + xAxisInfo : ''
  838. );
  839. };
  840. // Return string with information about point
  841. H.Point.prototype.buildPointInfoString = function () {
  842. var point = this,
  843. series = point.series,
  844. a11yOptions = series.chart.options.accessibility,
  845. infoString = '',
  846. dateTimePoint = series.xAxis && series.xAxis.isDatetimeAxis,
  847. timeDesc =
  848. dateTimePoint &&
  849. series.chart.time.dateFormat(
  850. a11yOptions.pointDateFormatter &&
  851. a11yOptions.pointDateFormatter(point) ||
  852. a11yOptions.pointDateFormat ||
  853. H.Tooltip.prototype.getXDateFormat.call(
  854. {
  855. getDateFormat: H.Tooltip.prototype.getDateFormat,
  856. chart: series.chart
  857. },
  858. point,
  859. series.chart.options.tooltip,
  860. series.xAxis
  861. ),
  862. point.x
  863. ),
  864. hasSpecialKey = H.find(series.specialKeys, function (key) {
  865. return point[key] !== undefined;
  866. });
  867. // If the point has one of the less common properties defined, display all
  868. // that are defined
  869. if (hasSpecialKey) {
  870. if (dateTimePoint) {
  871. infoString = timeDesc;
  872. }
  873. each(series.commonKeys.concat(series.specialKeys), function (key) {
  874. if (point[key] !== undefined && !(dateTimePoint && key === 'x')) {
  875. infoString += (infoString ? '. ' : '') +
  876. key + ', ' +
  877. point[key];
  878. }
  879. });
  880. } else {
  881. // Pick and choose properties for a succint label
  882. infoString =
  883. (
  884. this.name ||
  885. timeDesc ||
  886. this.category ||
  887. this.id ||
  888. 'x, ' + this.x
  889. ) + ', ' +
  890. (this.value !== undefined ? this.value : this.y);
  891. }
  892. return (this.index + 1) + '. ' + infoString + '.' +
  893. (this.description ? ' ' + this.description : '');
  894. };
  895. // Get descriptive label for axis
  896. H.Axis.prototype.getDescription = function () {
  897. return (
  898. this.userOptions && this.userOptions.description ||
  899. this.axisTitle && this.axisTitle.textStr ||
  900. this.options.id ||
  901. this.categories && 'categories' ||
  902. this.isDatetimeAxis && 'Time' ||
  903. 'values'
  904. );
  905. };
  906. // Whenever adding or removing series, keep track of types present in chart
  907. addEvent(H.Series, 'afterInit', function () {
  908. var chart = this.chart;
  909. if (chart.options.accessibility.enabled) {
  910. chart.types = chart.types || [];
  911. // Add type to list if does not exist
  912. if (chart.types.indexOf(this.type) < 0) {
  913. chart.types.push(this.type);
  914. }
  915. }
  916. });
  917. addEvent(H.Series, 'remove', function () {
  918. var chart = this.chart,
  919. removedSeries = this,
  920. hasType = false;
  921. // Check if any of the other series have the same type as this one.
  922. // Otherwise remove it from the list.
  923. each(chart.series, function (s) {
  924. if (
  925. s !== removedSeries &&
  926. chart.types.indexOf(removedSeries.type) < 0
  927. ) {
  928. hasType = true;
  929. }
  930. });
  931. if (!hasType) {
  932. erase(chart.types, removedSeries.type);
  933. }
  934. });
  935. // Return simplified description of chart type. Some types will not be familiar
  936. // to most screen reader users, but in those cases we try to add a description
  937. // of the type.
  938. H.Chart.prototype.getTypeDescription = function () {
  939. var firstType = this.types && this.types[0],
  940. firstSeries = this.series && this.series[0] || {},
  941. mapTitle = firstSeries.mapTitle,
  942. typeDesc = this.langFormat(
  943. 'accessibility.seriesTypeDescriptions.' + firstType,
  944. { chart: this }
  945. ),
  946. formatContext = {
  947. numSeries: this.series.length,
  948. numPoints: firstSeries.points && firstSeries.points.length,
  949. chart: this,
  950. mapTitle: mapTitle
  951. },
  952. multi = this.series && this.series.length === 1 ? 'Single' : 'Multiple';
  953. if (!firstType) {
  954. return this.langFormat(
  955. 'accessibility.chartTypes.emptyChart', formatContext
  956. );
  957. } else if (firstType === 'map') {
  958. return mapTitle ?
  959. this.langFormat(
  960. 'accessibility.chartTypes.mapTypeDescription',
  961. formatContext
  962. ) :
  963. this.langFormat(
  964. 'accessibility.chartTypes.unknownMap',
  965. formatContext
  966. );
  967. } else if (this.types.length > 1) {
  968. return this.langFormat(
  969. 'accessibility.chartTypes.combinationChart', formatContext
  970. );
  971. }
  972. return (
  973. this.langFormat(
  974. 'accessibility.chartTypes.' + firstType + multi,
  975. formatContext
  976. ) ||
  977. this.langFormat(
  978. 'accessibility.chartTypes.default' + multi,
  979. formatContext
  980. )
  981. ) +
  982. (typeDesc ? ' ' + typeDesc : '');
  983. };
  984. // Return object with text description of each of the chart's axes
  985. H.Chart.prototype.getAxesDescription = function () {
  986. var numXAxes = this.xAxis.length,
  987. numYAxes = this.yAxis.length,
  988. desc = {};
  989. if (numXAxes) {
  990. desc.xAxis = this.langFormat(
  991. 'accessibility.axis.xAxisDescription' + (
  992. numXAxes > 1 ? 'Plural' : 'Singular'
  993. ),
  994. {
  995. chart: this,
  996. names: map(this.xAxis, function (axis) {
  997. return axis.getDescription();
  998. }),
  999. numAxes: numXAxes
  1000. }
  1001. );
  1002. }
  1003. if (numYAxes) {
  1004. desc.yAxis = this.langFormat(
  1005. 'accessibility.axis.yAxisDescription' + (
  1006. numYAxes > 1 ? 'Plural' : 'Singular'
  1007. ),
  1008. {
  1009. chart: this,
  1010. names: map(this.yAxis, function (axis) {
  1011. return axis.getDescription();
  1012. }),
  1013. numAxes: numYAxes
  1014. }
  1015. );
  1016. }
  1017. return desc;
  1018. };
  1019. // Set a11y attribs on exporting menu
  1020. H.Chart.prototype.addAccessibleContextMenuAttribs = function () {
  1021. var exportList = this.exportDivElements;
  1022. if (exportList) {
  1023. // Set tabindex on the menu items to allow focusing by script
  1024. // Set role to give screen readers a chance to pick up the contents
  1025. each(exportList, function (item) {
  1026. if (item.tagName === 'DIV' &&
  1027. !(item.children && item.children.length)) {
  1028. item.setAttribute('role', 'menuitem');
  1029. item.setAttribute('tabindex', -1);
  1030. }
  1031. });
  1032. // Set accessibility properties on parent div
  1033. exportList[0].parentNode.setAttribute('role', 'menu');
  1034. exportList[0].parentNode.setAttribute('aria-label',
  1035. this.langFormat(
  1036. 'accessibility.exporting.chartMenuLabel', { chart: this }
  1037. )
  1038. );
  1039. }
  1040. };
  1041. // Add screen reader region to chart.
  1042. // tableId is the HTML id of the table to focus when clicking the table anchor
  1043. // in the screen reader region.
  1044. H.Chart.prototype.addScreenReaderRegion = function (id, tableId) {
  1045. var chart = this,
  1046. hiddenSection = chart.screenReaderRegion = doc.createElement('div'),
  1047. tableShortcut = doc.createElement('h4'),
  1048. tableShortcutAnchor = doc.createElement('a'),
  1049. chartHeading = doc.createElement('h4');
  1050. hiddenSection.setAttribute('id', id);
  1051. hiddenSection.setAttribute('role', 'region');
  1052. hiddenSection.setAttribute(
  1053. 'aria-label',
  1054. chart.langFormat(
  1055. 'accessibility.screenReaderRegionLabel', { chart: this }
  1056. )
  1057. );
  1058. hiddenSection.innerHTML = chart.options.accessibility
  1059. .screenReaderSectionFormatter(chart);
  1060. // Add shortcut to data table if export-data is loaded
  1061. if (chart.getCSV) {
  1062. tableShortcutAnchor.innerHTML = chart.langFormat(
  1063. 'accessibility.viewAsDataTable', { chart: chart }
  1064. );
  1065. tableShortcutAnchor.href = '#' + tableId;
  1066. // Make this unreachable by user tabbing
  1067. tableShortcutAnchor.setAttribute('tabindex', '-1');
  1068. tableShortcutAnchor.onclick =
  1069. chart.options.accessibility.onTableAnchorClick || function () {
  1070. chart.viewData();
  1071. doc.getElementById(tableId).focus();
  1072. };
  1073. tableShortcut.appendChild(tableShortcutAnchor);
  1074. hiddenSection.appendChild(tableShortcut);
  1075. }
  1076. // Note: JAWS seems to refuse to read aria-label on the container, so add an
  1077. // h4 element as title for the chart.
  1078. chartHeading.innerHTML = chart.langFormat(
  1079. 'accessibility.chartHeading', { chart: chart }
  1080. );
  1081. chart.renderTo.insertBefore(chartHeading, chart.renderTo.firstChild);
  1082. chart.renderTo.insertBefore(hiddenSection, chart.renderTo.firstChild);
  1083. // Hide the section and the chart heading
  1084. merge(true, chartHeading.style, hiddenStyle);
  1085. merge(true, hiddenSection.style, hiddenStyle);
  1086. };
  1087. // Make chart container accessible, and wrap table functionality
  1088. H.Chart.prototype.callbacks.push(function (chart) {
  1089. var options = chart.options,
  1090. a11yOptions = options.accessibility;
  1091. if (!a11yOptions.enabled) {
  1092. return;
  1093. }
  1094. var titleElement,
  1095. exportGroupElement = doc.createElementNS(
  1096. 'http://www.w3.org/2000/svg',
  1097. 'g'
  1098. ),
  1099. descElement = chart.container.getElementsByTagName('desc')[0],
  1100. textElements = chart.container.getElementsByTagName('text'),
  1101. titleId = 'highcharts-title-' + chart.index,
  1102. tableId = 'highcharts-data-table-' + chart.index,
  1103. hiddenSectionId = 'highcharts-information-region-' + chart.index,
  1104. chartTitle = options.title.text || chart.langFormat(
  1105. 'accessibility.defaultChartTitle', { chart: chart }
  1106. ),
  1107. svgContainerTitle = stripTags(chart.langFormat(
  1108. 'accessibility.svgContainerTitle', {
  1109. chartTitle: chartTitle
  1110. }
  1111. ));
  1112. // Add SVG title tag if it is set
  1113. if (svgContainerTitle.length) {
  1114. titleElement = doc.createElementNS(
  1115. 'http://www.w3.org/2000/svg',
  1116. 'title'
  1117. );
  1118. titleElement.textContent = svgContainerTitle;
  1119. titleElement.id = titleId;
  1120. descElement.parentNode.insertBefore(titleElement, descElement);
  1121. }
  1122. chart.renderTo.setAttribute('role', 'region');
  1123. chart.renderTo.setAttribute(
  1124. 'aria-label',
  1125. chart.langFormat(
  1126. 'accessibility.chartContainerLabel',
  1127. {
  1128. title: stripTags(chartTitle),
  1129. chart: chart
  1130. }
  1131. )
  1132. );
  1133. // Set screen reader properties on export menu
  1134. if (
  1135. chart.exportSVGElements &&
  1136. chart.exportSVGElements[0] &&
  1137. chart.exportSVGElements[0].element
  1138. ) {
  1139. var oldExportCallback = chart.exportSVGElements[0].element.onclick,
  1140. parent = chart.exportSVGElements[0].element.parentNode;
  1141. chart.exportSVGElements[0].element.onclick = function () {
  1142. oldExportCallback.apply(
  1143. this,
  1144. Array.prototype.slice.call(arguments)
  1145. );
  1146. chart.addAccessibleContextMenuAttribs();
  1147. chart.highlightExportItem(0);
  1148. };
  1149. chart.exportSVGElements[0].element.setAttribute('role', 'button');
  1150. chart.exportSVGElements[0].element.setAttribute(
  1151. 'aria-label',
  1152. chart.langFormat(
  1153. 'accessibility.exporting.menuButtonLabel', { chart: chart }
  1154. )
  1155. );
  1156. exportGroupElement.appendChild(chart.exportSVGElements[0].element);
  1157. exportGroupElement.setAttribute('role', 'region');
  1158. exportGroupElement.setAttribute('aria-label', chart.langFormat(
  1159. 'accessibility.exporting.exportRegionLabel', { chart: chart }
  1160. ));
  1161. parent.appendChild(exportGroupElement);
  1162. }
  1163. // Set screen reader properties on input boxes for range selector. We need
  1164. // to do this regardless of whether or not these are visible, as they are
  1165. // by default part of the page's tabindex unless we set them to -1.
  1166. if (chart.rangeSelector) {
  1167. each(['minInput', 'maxInput'], function (key, i) {
  1168. if (chart.rangeSelector[key]) {
  1169. chart.rangeSelector[key].setAttribute('tabindex', '-1');
  1170. chart.rangeSelector[key].setAttribute('role', 'textbox');
  1171. chart.rangeSelector[key].setAttribute(
  1172. 'aria-label',
  1173. chart.langFormat(
  1174. 'accessibility.rangeSelector' +
  1175. (i ? 'MaxInput' : 'MinInput'), { chart: chart }
  1176. )
  1177. );
  1178. }
  1179. });
  1180. }
  1181. // Hide text elements from screen readers
  1182. each(textElements, function (el) {
  1183. el.setAttribute('aria-hidden', 'true');
  1184. });
  1185. // Add top-secret screen reader region
  1186. chart.addScreenReaderRegion(hiddenSectionId, tableId);
  1187. // Add ID and summary attr to table HTML
  1188. H.wrap(chart, 'getTable', function (proceed) {
  1189. return proceed.apply(this, Array.prototype.slice.call(arguments, 1))
  1190. .replace(
  1191. '<table>',
  1192. '<table id="' + tableId + '" summary="' + chart.langFormat(
  1193. 'accessibility.tableSummary', { chart: chart }
  1194. ) + '">'
  1195. );
  1196. });
  1197. });
  1198. }(Highcharts));
  1199. (function (H) {
  1200. /**
  1201. * Accessibility module - Keyboard navigation
  1202. *
  1203. * (c) 2010-2017 Highsoft AS
  1204. * Author: Oystein Moseng
  1205. *
  1206. * License: www.highcharts.com/license
  1207. */
  1208. var win = H.win,
  1209. doc = win.document,
  1210. each = H.each,
  1211. addEvent = H.addEvent,
  1212. fireEvent = H.fireEvent,
  1213. merge = H.merge,
  1214. pick = H.pick,
  1215. hasSVGFocusSupport;
  1216. // Add focus border functionality to SVGElements.
  1217. // Draws a new rect on top of element around its bounding box.
  1218. H.extend(H.SVGElement.prototype, {
  1219. addFocusBorder: function (margin, style) {
  1220. // Allow updating by just adding new border
  1221. if (this.focusBorder) {
  1222. this.removeFocusBorder();
  1223. }
  1224. // Add the border rect
  1225. var bb = this.getBBox(),
  1226. pad = pick(margin, 3);
  1227. this.focusBorder = this.renderer.rect(
  1228. bb.x - pad,
  1229. bb.y - pad,
  1230. bb.width + 2 * pad,
  1231. bb.height + 2 * pad,
  1232. style && style.borderRadius
  1233. )
  1234. .addClass('highcharts-focus-border')
  1235. .attr({
  1236. stroke: style && style.stroke,
  1237. 'stroke-width': style && style.strokeWidth
  1238. })
  1239. .attr({
  1240. zIndex: 99
  1241. })
  1242. .add(this.parentGroup);
  1243. },
  1244. removeFocusBorder: function () {
  1245. if (this.focusBorder) {
  1246. this.focusBorder.destroy();
  1247. delete this.focusBorder;
  1248. }
  1249. }
  1250. });
  1251. // Set for which series types it makes sense to move to the closest point with
  1252. // up/down arrows, and which series types should just move to next series.
  1253. H.Series.prototype.keyboardMoveVertical = true;
  1254. each(['column', 'pie'], function (type) {
  1255. if (H.seriesTypes[type]) {
  1256. H.seriesTypes[type].prototype.keyboardMoveVertical = false;
  1257. }
  1258. });
  1259. /**
  1260. * Strip HTML tags away from a string. Used for aria-label attributes, painting
  1261. * on a canvas will fail if the text contains tags.
  1262. * @param {String} s The input string
  1263. * @return {String} The filtered string
  1264. */
  1265. function stripTags(s) {
  1266. return typeof s === 'string' ? s.replace(/<\/?[^>]+(>|$)/g, '') : s;
  1267. }
  1268. /**
  1269. * Set default keyboard navigation options
  1270. */
  1271. H.setOptions({
  1272. accessibility: {
  1273. /**
  1274. * Options for keyboard navigation.
  1275. *
  1276. * @type {Object}
  1277. * @since 5.0.0
  1278. * @apioption accessibility.keyboardNavigation
  1279. */
  1280. keyboardNavigation: {
  1281. /**
  1282. * Enable keyboard navigation for the chart.
  1283. *
  1284. * @type {Boolean}
  1285. * @default true
  1286. * @since 5.0.0
  1287. * @apioption accessibility.keyboardNavigation.enabled
  1288. */
  1289. enabled: true,
  1290. /**
  1291. * Options for the focus border drawn around elements while
  1292. * navigating through them.
  1293. *
  1294. * @type {Object}
  1295. * @sample highcharts/accessibility/custom-focus
  1296. * Custom focus ring
  1297. * @since 6.0.3
  1298. * @apioption accessibility.keyboardNavigation.focusBorder
  1299. */
  1300. focusBorder: {
  1301. /**
  1302. * Enable/disable focus border for chart.
  1303. *
  1304. * @type {Boolean}
  1305. * @default true
  1306. * @since 6.0.3
  1307. * @apioption accessibility.keyboardNavigation.focusBorder.enabled
  1308. */
  1309. enabled: true,
  1310. /**
  1311. * Hide the browser's default focus indicator.
  1312. *
  1313. * @type {Boolean}
  1314. * @default true
  1315. * @since 6.0.4
  1316. * @apioption accessibility.keyboardNavigation.focusBorder.hideBrowserFocusOutline
  1317. */
  1318. hideBrowserFocusOutline: true,
  1319. /**
  1320. * Style options for the focus border drawn around elements
  1321. * while navigating through them. Note that some browsers in
  1322. * addition draw their own borders for focused elements. These
  1323. * automatic borders can not be styled by Highcharts.
  1324. *
  1325. * In styled mode, the border is given the
  1326. * `.highcharts-focus-border` class.
  1327. *
  1328. * @type {Object}
  1329. * @since 6.0.3
  1330. * @apioption accessibility.keyboardNavigation.focusBorder.style
  1331. */
  1332. style: {
  1333. /**
  1334. * Color of the focus border.
  1335. *
  1336. * @type {Color}
  1337. * @default #000000
  1338. * @since 6.0.3
  1339. * @apioption accessibility.keyboardNavigation.focusBorder.style.color
  1340. */
  1341. color: '#335cad',
  1342. /**
  1343. * Line width of the focus border.
  1344. *
  1345. * @type {Number}
  1346. * @default 2
  1347. * @since 6.0.3
  1348. * @apioption accessibility.keyboardNavigation.focusBorder.style.lineWidth
  1349. */
  1350. lineWidth: 2,
  1351. /**
  1352. * Border radius of the focus border.
  1353. *
  1354. * @type {Number}
  1355. * @default 3
  1356. * @since 6.0.3
  1357. * @apioption accessibility.keyboardNavigation.focusBorder.style.borderRadius
  1358. */
  1359. borderRadius: 3
  1360. },
  1361. /**
  1362. * Focus border margin around the elements.
  1363. *
  1364. * @type {Number}
  1365. * @default 2
  1366. * @since 6.0.3
  1367. * @apioption accessibility.keyboardNavigation.focusBorder.margin
  1368. */
  1369. margin: 2
  1370. },
  1371. /**
  1372. * Set the keyboard navigation mode for the chart. Can be "normal"
  1373. * or "serialize". In normal mode, left/right arrow keys move
  1374. * between points in a series, while up/down arrow keys move between
  1375. * series. Up/down navigation acts intelligently to figure out which
  1376. * series makes sense to move to from any given point.
  1377. *
  1378. * In "serialize" mode, points are instead navigated as a single
  1379. * list. Left/right behaves as in "normal" mode. Up/down arrow keys
  1380. * will behave like left/right. This is useful for unifying
  1381. * navigation behavior with/without screen readers enabled.
  1382. *
  1383. * @type {String}
  1384. * @default normal
  1385. * @since 6.0.4
  1386. * @apioption accessibility.keyboardNavigation.mode
  1387. */
  1388. /**
  1389. * Skip null points when navigating through points with the
  1390. * keyboard.
  1391. *
  1392. * @type {Boolean}
  1393. * @default true
  1394. * @since 5.0.0
  1395. * @apioption accessibility.keyboardNavigation.skipNullPoints
  1396. */
  1397. skipNullPoints: true
  1398. }
  1399. }
  1400. });
  1401. /**
  1402. * Keyboard navigation for the legend. Requires the Accessibility module.
  1403. * @since 5.0.14
  1404. * @apioption legend.keyboardNavigation
  1405. */
  1406. /**
  1407. * Enable/disable keyboard navigation for the legend. Requires the Accessibility
  1408. * module.
  1409. *
  1410. * @type {Boolean}
  1411. * @see [accessibility.keyboardNavigation](
  1412. * #accessibility.keyboardNavigation.enabled)
  1413. * @default true
  1414. * @since 5.0.13
  1415. * @apioption legend.keyboardNavigation.enabled
  1416. */
  1417. // Abstraction layer for keyboard navigation. Keep a map of keyCodes to
  1418. // handler functions, and a next/prev move handler for tab order. The
  1419. // module's keyCode handlers determine when to move to another module.
  1420. // Validate holds a function to determine if there are prerequisites for
  1421. // this module to run that are not met. Init holds a function to run once
  1422. // before any keyCodes are interpreted. Terminate holds a function to run
  1423. // once before moving to next/prev module.
  1424. // The chart object keeps track of a list of KeyboardNavigationModules.
  1425. function KeyboardNavigationModule(chart, options) {
  1426. this.chart = chart;
  1427. this.id = options.id;
  1428. this.keyCodeMap = options.keyCodeMap;
  1429. this.validate = options.validate;
  1430. this.init = options.init;
  1431. this.terminate = options.terminate;
  1432. }
  1433. KeyboardNavigationModule.prototype = {
  1434. // Find handler function(s) for key code in the keyCodeMap and run it.
  1435. run: function (e) {
  1436. var navModule = this,
  1437. keyCode = e.which || e.keyCode,
  1438. found = false,
  1439. handled = false;
  1440. each(this.keyCodeMap, function (codeSet) {
  1441. if (codeSet[0].indexOf(keyCode) > -1) {
  1442. found = true;
  1443. handled = codeSet[1].call(navModule, keyCode, e) === false ?
  1444. // If explicitly returning false, we haven't handled it
  1445. false :
  1446. true;
  1447. }
  1448. });
  1449. // Default tab handler, move to next/prev module
  1450. if (!found && keyCode === 9) {
  1451. handled = this.move(e.shiftKey ? -1 : 1);
  1452. }
  1453. return handled;
  1454. },
  1455. // Move to next/prev valid module, or undefined if none, and init
  1456. // it. Returns true on success and false if there is no valid module
  1457. // to move to.
  1458. move: function (direction) {
  1459. var chart = this.chart;
  1460. if (this.terminate) {
  1461. this.terminate(direction);
  1462. }
  1463. chart.keyboardNavigationModuleIndex += direction;
  1464. var newModule = chart.keyboardNavigationModules[
  1465. chart.keyboardNavigationModuleIndex
  1466. ];
  1467. // Remove existing focus border if any
  1468. if (chart.focusElement) {
  1469. chart.focusElement.removeFocusBorder();
  1470. }
  1471. // Verify new module
  1472. if (newModule) {
  1473. if (newModule.validate && !newModule.validate()) {
  1474. return this.move(direction); // Invalid module, recurse
  1475. }
  1476. if (newModule.init) {
  1477. newModule.init(direction); // Valid module, init it
  1478. return true;
  1479. }
  1480. }
  1481. // No module
  1482. chart.keyboardNavigationModuleIndex = 0; // Reset counter
  1483. // Set focus to chart or exit anchor depending on direction
  1484. if (direction > 0) {
  1485. this.chart.exiting = true;
  1486. this.chart.tabExitAnchor.focus();
  1487. } else {
  1488. this.chart.renderTo.focus();
  1489. }
  1490. return false;
  1491. }
  1492. };
  1493. // Utility function to attempt to fake a click event on an element
  1494. function fakeClickEvent(element) {
  1495. var fakeEvent;
  1496. if (element && element.onclick && doc.createEvent) {
  1497. fakeEvent = doc.createEvent('Events');
  1498. fakeEvent.initEvent('click', true, false);
  1499. element.onclick(fakeEvent);
  1500. }
  1501. }
  1502. // Determine if a point should be skipped
  1503. function isSkipPoint(point) {
  1504. var a11yOptions = point.series.chart.options.accessibility;
  1505. return point.isNull && a11yOptions.keyboardNavigation.skipNullPoints ||
  1506. point.series.options.skipKeyboardNavigation ||
  1507. !point.series.visible ||
  1508. point.visible === false ||
  1509. // Skip all points in a series where pointDescriptionThreshold is
  1510. // reached
  1511. (a11yOptions.pointDescriptionThreshold &&
  1512. a11yOptions.pointDescriptionThreshold <= point.series.points.length);
  1513. }
  1514. // Get the point in a series that is closest (in distance) to a reference point
  1515. // Optionally supply weight factors for x and y directions
  1516. function getClosestPoint(point, series, xWeight, yWeight) {
  1517. var minDistance = Infinity,
  1518. dPoint,
  1519. minIx,
  1520. distance,
  1521. i = series.points.length;
  1522. if (point.plotX === undefined || point.plotY === undefined) {
  1523. return;
  1524. }
  1525. while (i--) {
  1526. dPoint = series.points[i];
  1527. if (dPoint.plotX === undefined || dPoint.plotY === undefined) {
  1528. continue;
  1529. }
  1530. distance = (point.plotX - dPoint.plotX) *
  1531. (point.plotX - dPoint.plotX) * (xWeight || 1) +
  1532. (point.plotY - dPoint.plotY) *
  1533. (point.plotY - dPoint.plotY) * (yWeight || 1);
  1534. if (distance < minDistance) {
  1535. minDistance = distance;
  1536. minIx = i;
  1537. }
  1538. }
  1539. return minIx !== undefined && series.points[minIx];
  1540. }
  1541. // Pan along axis in a direction (1 or -1), optionally with a defined
  1542. // granularity (number of steps it takes to walk across current view)
  1543. H.Axis.prototype.panStep = function (direction, granularity) {
  1544. var gran = granularity || 3,
  1545. extremes = this.getExtremes(),
  1546. step = (extremes.max - extremes.min) / gran * direction,
  1547. newMax = extremes.max + step,
  1548. newMin = extremes.min + step,
  1549. size = newMax - newMin;
  1550. if (direction < 0 && newMin < extremes.dataMin) {
  1551. newMin = extremes.dataMin;
  1552. newMax = newMin + size;
  1553. } else if (direction > 0 && newMax > extremes.dataMax) {
  1554. newMax = extremes.dataMax;
  1555. newMin = newMax - size;
  1556. }
  1557. this.setExtremes(newMin, newMax);
  1558. };
  1559. // Set chart's focus to an SVGElement. Calls focus() on it, and draws the focus
  1560. // border. If the focusElement argument is supplied, it draws the border around
  1561. // svgElement and sets the focus to focusElement.
  1562. H.Chart.prototype.setFocusToElement = function (svgElement, focusElement) {
  1563. var focusBorderOptions = this.options.accessibility
  1564. .keyboardNavigation.focusBorder,
  1565. browserFocusElement = focusElement || svgElement;
  1566. // Set browser focus if possible
  1567. if (
  1568. browserFocusElement.element &&
  1569. browserFocusElement.element.focus
  1570. ) {
  1571. browserFocusElement.element.focus();
  1572. // Hide default focus ring
  1573. if (focusBorderOptions.hideBrowserFocusOutline) {
  1574. browserFocusElement.css({ outline: 'none' });
  1575. }
  1576. }
  1577. if (focusBorderOptions.enabled) {
  1578. // Remove old focus border
  1579. if (this.focusElement) {
  1580. this.focusElement.removeFocusBorder();
  1581. }
  1582. // Draw focus border (since some browsers don't do it automatically)
  1583. svgElement.addFocusBorder(focusBorderOptions.margin, {
  1584. stroke: focusBorderOptions.style.color,
  1585. strokeWidth: focusBorderOptions.style.lineWidth,
  1586. borderRadius: focusBorderOptions.style.borderRadius
  1587. });
  1588. this.focusElement = svgElement;
  1589. }
  1590. };
  1591. // Highlight a point (show tooltip and display hover state). Returns the
  1592. // highlighted point.
  1593. H.Point.prototype.highlight = function () {
  1594. var chart = this.series.chart;
  1595. if (!this.isNull) {
  1596. this.onMouseOver(); // Show the hover marker and tooltip
  1597. } else {
  1598. if (chart.tooltip) {
  1599. chart.tooltip.hide(0);
  1600. }
  1601. // Don't call blur on the element, as it messes up the chart div's focus
  1602. }
  1603. // We focus only after calling onMouseOver because the state change can
  1604. // change z-index and mess up the element.
  1605. if (this.graphic) {
  1606. chart.setFocusToElement(this.graphic);
  1607. }
  1608. chart.highlightedPoint = this;
  1609. return this;
  1610. };
  1611. // Function to highlight next/previous point in chart
  1612. // Returns highlighted point on success, false on failure (no adjacent point to
  1613. // highlight in chosen direction)
  1614. H.Chart.prototype.highlightAdjacentPoint = function (next) {
  1615. var chart = this,
  1616. series = chart.series,
  1617. curPoint = chart.highlightedPoint,
  1618. curPointIndex = curPoint && curPoint.index || 0,
  1619. curPoints = curPoint && curPoint.series.points,
  1620. lastSeries = chart.series && chart.series[chart.series.length - 1],
  1621. lastPoint = lastSeries && lastSeries.points &&
  1622. lastSeries.points[lastSeries.points.length - 1],
  1623. newSeries,
  1624. newPoint;
  1625. // If no points, return false
  1626. if (!series[0] || !series[0].points) {
  1627. return false;
  1628. }
  1629. if (!curPoint) {
  1630. // No point is highlighted yet. Try first/last point depending on move
  1631. // direction
  1632. newPoint = next ? series[0].points[0] : lastPoint;
  1633. } else {
  1634. // We have a highlighted point.
  1635. // Find index of current point in series.points array. Necessary for
  1636. // dataGrouping (and maybe zoom?)
  1637. if (curPoints[curPointIndex] !== curPoint) {
  1638. for (var i = 0; i < curPoints.length; ++i) {
  1639. if (curPoints[i] === curPoint) {
  1640. curPointIndex = i;
  1641. break;
  1642. }
  1643. }
  1644. }
  1645. // Grab next/prev point & series
  1646. newSeries = series[curPoint.series.index + (next ? 1 : -1)];
  1647. newPoint = curPoints[curPointIndex + (next ? 1 : -1)] ||
  1648. // Done with this series, try next one
  1649. newSeries &&
  1650. newSeries.points[next ? 0 : newSeries.points.length - 1];
  1651. // If there is no adjacent point, we return false
  1652. if (!newPoint) {
  1653. return false;
  1654. }
  1655. }
  1656. // Recursively skip null points or points in series that should be skipped
  1657. if (isSkipPoint(newPoint)) {
  1658. chart.highlightedPoint = newPoint;
  1659. return chart.highlightAdjacentPoint(next);
  1660. }
  1661. // There is an adjacent point, highlight it
  1662. return newPoint.highlight();
  1663. };
  1664. // Highlight first valid point in a series. Returns the point if successfully
  1665. // highlighted, otherwise false. If there is a highlighted point in the series,
  1666. // use that as starting point.
  1667. H.Series.prototype.highlightFirstValidPoint = function () {
  1668. var curPoint = this.chart.highlightedPoint,
  1669. start = (curPoint && curPoint.series) === this ? curPoint.index : 0,
  1670. points = this.points;
  1671. if (points) {
  1672. for (var i = start, len = points.length; i < len; ++i) {
  1673. if (!isSkipPoint(points[i])) {
  1674. return points[i].highlight();
  1675. }
  1676. }
  1677. for (var j = start; j >= 0; --j) {
  1678. if (!isSkipPoint(points[j])) {
  1679. return points[j].highlight();
  1680. }
  1681. }
  1682. }
  1683. return false;
  1684. };
  1685. // Highlight next/previous series in chart. Returns false if no adjacent series
  1686. // in the direction, otherwise returns new highlighted point.
  1687. H.Chart.prototype.highlightAdjacentSeries = function (down) {
  1688. var chart = this,
  1689. newSeries,
  1690. newPoint,
  1691. adjacentNewPoint,
  1692. curPoint = chart.highlightedPoint,
  1693. lastSeries = chart.series && chart.series[chart.series.length - 1],
  1694. lastPoint = lastSeries && lastSeries.points &&
  1695. lastSeries.points[lastSeries.points.length - 1];
  1696. // If no point is highlighted, highlight the first/last point
  1697. if (!chart.highlightedPoint) {
  1698. newSeries = down ? (chart.series && chart.series[0]) : lastSeries;
  1699. newPoint = down ?
  1700. (newSeries && newSeries.points && newSeries.points[0]) : lastPoint;
  1701. return newPoint ? newPoint.highlight() : false;
  1702. }
  1703. newSeries = chart.series[curPoint.series.index + (down ? -1 : 1)];
  1704. if (!newSeries) {
  1705. return false;
  1706. }
  1707. // We have a new series in this direction, find the right point
  1708. // Weigh xDistance as counting much higher than Y distance
  1709. newPoint = getClosestPoint(curPoint, newSeries, 4);
  1710. if (!newPoint) {
  1711. return false;
  1712. }
  1713. // New series and point exists, but we might want to skip it
  1714. if (!newSeries.visible) {
  1715. // Skip the series
  1716. newPoint.highlight();
  1717. adjacentNewPoint = chart.highlightAdjacentSeries(down); // Try recurse
  1718. if (!adjacentNewPoint) {
  1719. // Recurse failed
  1720. curPoint.highlight();
  1721. return false;
  1722. }
  1723. // Recurse succeeded
  1724. return adjacentNewPoint;
  1725. }
  1726. // Highlight the new point or any first valid point back or forwards from it
  1727. newPoint.highlight();
  1728. return newPoint.series.highlightFirstValidPoint();
  1729. };
  1730. // Highlight the closest point vertically
  1731. H.Chart.prototype.highlightAdjacentPointVertical = function (down) {
  1732. var curPoint = this.highlightedPoint,
  1733. minDistance = Infinity,
  1734. bestPoint;
  1735. if (curPoint.plotX === undefined || curPoint.plotY === undefined) {
  1736. return false;
  1737. }
  1738. each(this.series, function (series) {
  1739. each(series.points, function (point) {
  1740. if (point.plotY === undefined || point.plotX === undefined ||
  1741. point === curPoint) {
  1742. return;
  1743. }
  1744. var yDistance = point.plotY - curPoint.plotY,
  1745. width = Math.abs(point.plotX - curPoint.plotX),
  1746. distance = Math.abs(yDistance) * Math.abs(yDistance) +
  1747. width * width * 4; // Weigh horizontal distance highly
  1748. // Reverse distance number if axis is reversed
  1749. if (series.yAxis.reversed) {
  1750. yDistance *= -1;
  1751. }
  1752. if (
  1753. yDistance < 0 && down || yDistance > 0 && !down || // Wrong dir
  1754. distance < 5 || // Points in same spot => infinite loop
  1755. isSkipPoint(point)
  1756. ) {
  1757. return;
  1758. }
  1759. if (distance < minDistance) {
  1760. minDistance = distance;
  1761. bestPoint = point;
  1762. }
  1763. });
  1764. });
  1765. return bestPoint ? bestPoint.highlight() : false;
  1766. };
  1767. // Show the export menu and focus the first item (if exists)
  1768. H.Chart.prototype.showExportMenu = function () {
  1769. if (this.exportSVGElements && this.exportSVGElements[0]) {
  1770. this.exportSVGElements[0].element.onclick();
  1771. this.highlightExportItem(0);
  1772. }
  1773. };
  1774. // Hide export menu
  1775. H.Chart.prototype.hideExportMenu = function () {
  1776. var exportList = this.exportDivElements;
  1777. if (exportList) {
  1778. each(exportList, function (el) {
  1779. fireEvent(el, 'mouseleave');
  1780. });
  1781. if (
  1782. exportList[this.highlightedExportItem] &&
  1783. exportList[this.highlightedExportItem].onmouseout
  1784. ) {
  1785. exportList[this.highlightedExportItem].onmouseout();
  1786. }
  1787. this.highlightedExportItem = 0;
  1788. if (hasSVGFocusSupport) {
  1789. // Only focus if we can set focus back to the elements after
  1790. // destroying the menu (#7422)
  1791. this.renderTo.focus();
  1792. }
  1793. }
  1794. };
  1795. // Highlight export menu item by index
  1796. H.Chart.prototype.highlightExportItem = function (ix) {
  1797. var listItem = this.exportDivElements && this.exportDivElements[ix],
  1798. curHighlighted =
  1799. this.exportDivElements &&
  1800. this.exportDivElements[this.highlightedExportItem];
  1801. if (
  1802. listItem &&
  1803. listItem.tagName === 'DIV' &&
  1804. !(listItem.children && listItem.children.length)
  1805. ) {
  1806. if (listItem.focus && hasSVGFocusSupport) {
  1807. // Only focus if we can set focus back to the elements after
  1808. // destroying the menu (#7422)
  1809. listItem.focus();
  1810. }
  1811. if (curHighlighted && curHighlighted.onmouseout) {
  1812. curHighlighted.onmouseout();
  1813. }
  1814. if (listItem.onmouseover) {
  1815. listItem.onmouseover();
  1816. }
  1817. this.highlightedExportItem = ix;
  1818. return true;
  1819. }
  1820. };
  1821. // Try to highlight the last valid export menu item
  1822. H.Chart.prototype.highlightLastExportItem = function () {
  1823. var chart = this,
  1824. i;
  1825. if (chart.exportDivElements) {
  1826. i = chart.exportDivElements.length;
  1827. while (i--) {
  1828. if (chart.highlightExportItem(i)) {
  1829. break;
  1830. }
  1831. }
  1832. }
  1833. };
  1834. // Highlight range selector button by index
  1835. H.Chart.prototype.highlightRangeSelectorButton = function (ix) {
  1836. var buttons = this.rangeSelector.buttons;
  1837. // Deselect old
  1838. if (buttons[this.highlightedRangeSelectorItemIx]) {
  1839. buttons[this.highlightedRangeSelectorItemIx].setState(
  1840. this.oldRangeSelectorItemState || 0
  1841. );
  1842. }
  1843. // Select new
  1844. this.highlightedRangeSelectorItemIx = ix;
  1845. if (buttons[ix]) {
  1846. this.setFocusToElement(buttons[ix].box, buttons[ix]);
  1847. this.oldRangeSelectorItemState = buttons[ix].state;
  1848. buttons[ix].setState(2);
  1849. return true;
  1850. }
  1851. return false;
  1852. };
  1853. // Highlight legend item by index
  1854. H.Chart.prototype.highlightLegendItem = function (ix) {
  1855. var items = this.legend.allItems,
  1856. oldIx = this.highlightedLegendItemIx;
  1857. if (items[ix]) {
  1858. if (items[oldIx]) {
  1859. fireEvent(
  1860. items[oldIx].legendGroup.element,
  1861. 'mouseout'
  1862. );
  1863. }
  1864. // Scroll if we have to
  1865. if (items[ix].pageIx !== undefined &&
  1866. items[ix].pageIx + 1 !== this.legend.currentPage) {
  1867. this.legend.scroll(1 + items[ix].pageIx - this.legend.currentPage);
  1868. }
  1869. // Focus
  1870. this.highlightedLegendItemIx = ix;
  1871. this.setFocusToElement(items[ix].legendItem, items[ix].legendGroup);
  1872. fireEvent(items[ix].legendGroup.element, 'mouseover');
  1873. return true;
  1874. }
  1875. return false;
  1876. };
  1877. // Add keyboard navigation handling modules to chart
  1878. H.Chart.prototype.addKeyboardNavigationModules = function () {
  1879. var chart = this;
  1880. function navModuleFactory(id, keyMap, options) {
  1881. return new KeyboardNavigationModule(chart, merge({
  1882. keyCodeMap: keyMap
  1883. }, { id: id }, options));
  1884. }
  1885. // List of the different keyboard handling modes we use depending on where
  1886. // we are in the chart. Each mode has a set of handling functions mapped to
  1887. // key codes. Each mode determines when to move to the next/prev mode.
  1888. chart.keyboardNavigationModules = [
  1889. // Entry point catching the first tab, allowing users to tab into points
  1890. // more intuitively.
  1891. navModuleFactory('entry', []),
  1892. // Points
  1893. navModuleFactory('points', [
  1894. // Left/Right
  1895. [[37, 39], function (keyCode) {
  1896. var right = keyCode === 39;
  1897. if (!chart.highlightAdjacentPoint(right)) {
  1898. // Failed to highlight next, wrap to last/first
  1899. return this.init(right ? 1 : -1);
  1900. }
  1901. return true;
  1902. }],
  1903. // Up/Down
  1904. [[38, 40], function (keyCode) {
  1905. var down = keyCode !== 38,
  1906. navOptions = chart.options.accessibility.keyboardNavigation;
  1907. if (navOptions.mode && navOptions.mode === 'serialize') {
  1908. // Act like left/right
  1909. if (!chart.highlightAdjacentPoint(down)) {
  1910. return this.init(down ? 1 : -1);
  1911. }
  1912. return true;
  1913. }
  1914. // Normal mode, move between series
  1915. var highlightMethod = chart.highlightedPoint &&
  1916. chart.highlightedPoint.series.keyboardMoveVertical ?
  1917. 'highlightAdjacentPointVertical' :
  1918. 'highlightAdjacentSeries';
  1919. chart[highlightMethod](down);
  1920. return true;
  1921. }],
  1922. // Enter/Spacebar
  1923. [[13, 32], function () {
  1924. if (chart.highlightedPoint) {
  1925. chart.highlightedPoint.firePointEvent('click');
  1926. }
  1927. }]
  1928. ], {
  1929. // Always start highlighting from scratch when entering this module
  1930. init: function (dir) {
  1931. var numSeries = chart.series.length,
  1932. i = dir > 0 ? 0 : numSeries,
  1933. res;
  1934. if (dir > 0) {
  1935. delete chart.highlightedPoint;
  1936. // Find first valid point to highlight
  1937. while (i < numSeries) {
  1938. res = chart.series[i].highlightFirstValidPoint();
  1939. if (res) {
  1940. return res;
  1941. }
  1942. ++i;
  1943. }
  1944. } else {
  1945. // Find last valid point to highlight
  1946. while (i--) {
  1947. chart.highlightedPoint = chart.series[i].points[
  1948. chart.series[i].points.length - 1
  1949. ];
  1950. // Highlight first valid point in the series will also
  1951. // look backwards. It always starts from currently
  1952. // highlighted point.
  1953. res = chart.series[i].highlightFirstValidPoint();
  1954. if (res) {
  1955. return res;
  1956. }
  1957. }
  1958. }
  1959. },
  1960. // If leaving points, don't show tooltip anymore
  1961. terminate: function () {
  1962. if (chart.tooltip) {
  1963. chart.tooltip.hide(0);
  1964. }
  1965. delete chart.highlightedPoint;
  1966. }
  1967. }),
  1968. // Exporting
  1969. navModuleFactory('exporting', [
  1970. // Left/Up
  1971. [[37, 38], function () {
  1972. var i = chart.highlightedExportItem || 0,
  1973. reachedEnd = true;
  1974. // Try to highlight prev item in list. Highlighting e.g.
  1975. // separators will fail.
  1976. while (i--) {
  1977. if (chart.highlightExportItem(i)) {
  1978. reachedEnd = false;
  1979. break;
  1980. }
  1981. }
  1982. if (reachedEnd) {
  1983. chart.highlightLastExportItem();
  1984. return true;
  1985. }
  1986. }],
  1987. // Right/Down
  1988. [[39, 40], function () {
  1989. var highlightedExportItem = chart.highlightedExportItem || 0,
  1990. reachedEnd = true;
  1991. // Try to highlight next item in list. Highlighting e.g.
  1992. // separators will fail.
  1993. for (
  1994. var i = highlightedExportItem + 1;
  1995. i < chart.exportDivElements.length;
  1996. ++i
  1997. ) {
  1998. if (chart.highlightExportItem(i)) {
  1999. reachedEnd = false;
  2000. break;
  2001. }
  2002. }
  2003. if (reachedEnd) {
  2004. chart.highlightExportItem(0);
  2005. return true;
  2006. }
  2007. }],
  2008. // Enter/Spacebar
  2009. [[13, 32], function () {
  2010. fakeClickEvent(
  2011. chart.exportDivElements[chart.highlightedExportItem]
  2012. );
  2013. }]
  2014. ], {
  2015. // Only run exporting navigation if exporting support exists and is
  2016. // enabled on chart
  2017. validate: function () {
  2018. return (
  2019. chart.exportChart &&
  2020. !(
  2021. chart.options.exporting &&
  2022. chart.options.exporting.enabled === false
  2023. )
  2024. );
  2025. },
  2026. // Show export menu
  2027. init: function (direction) {
  2028. chart.highlightedPoint = null;
  2029. chart.showExportMenu();
  2030. // If coming back to export menu from other module, try to
  2031. // highlight last item in menu
  2032. if (direction < 0) {
  2033. chart.highlightLastExportItem();
  2034. }
  2035. },
  2036. // Hide the menu
  2037. terminate: function () {
  2038. chart.hideExportMenu();
  2039. }
  2040. }),
  2041. // Map zoom
  2042. navModuleFactory('mapZoom', [
  2043. // Up/down/left/right
  2044. [[38, 40, 37, 39], function (keyCode) {
  2045. chart[keyCode === 38 || keyCode === 40 ? 'yAxis' : 'xAxis'][0]
  2046. .panStep(keyCode < 39 ? -1 : 1);
  2047. }],
  2048. // Tabs
  2049. [[9], function (keyCode, e) {
  2050. var button;
  2051. // Deselect old
  2052. chart.mapNavButtons[chart.focusedMapNavButtonIx].setState(0);
  2053. if (
  2054. e.shiftKey && !chart.focusedMapNavButtonIx ||
  2055. !e.shiftKey && chart.focusedMapNavButtonIx
  2056. ) { // trying to go somewhere we can't?
  2057. chart.mapZoom(); // Reset zoom
  2058. // Nowhere to go, go to prev/next module
  2059. return this.move(e.shiftKey ? -1 : 1);
  2060. }
  2061. chart.focusedMapNavButtonIx += e.shiftKey ? -1 : 1;
  2062. button = chart.mapNavButtons[chart.focusedMapNavButtonIx];
  2063. chart.setFocusToElement(button.box, button);
  2064. button.setState(2);
  2065. }],
  2066. // Enter/Spacebar
  2067. [[13, 32], function () {
  2068. fakeClickEvent(
  2069. chart.mapNavButtons[chart.focusedMapNavButtonIx].element
  2070. );
  2071. }]
  2072. ], {
  2073. // Only run this module if we have map zoom on the chart
  2074. validate: function () {
  2075. return (
  2076. chart.mapZoom &&
  2077. chart.mapNavButtons &&
  2078. chart.mapNavButtons.length === 2
  2079. );
  2080. },
  2081. // Make zoom buttons do their magic
  2082. init: function (direction) {
  2083. var zoomIn = chart.mapNavButtons[0],
  2084. zoomOut = chart.mapNavButtons[1],
  2085. initialButton = direction > 0 ? zoomIn : zoomOut;
  2086. each(chart.mapNavButtons, function (button, i) {
  2087. button.element.setAttribute('tabindex', -1);
  2088. button.element.setAttribute('role', 'button');
  2089. button.element.setAttribute(
  2090. 'aria-label',
  2091. chart.langFormat(
  2092. 'accessibility.mapZoom' + (i ? 'Out' : 'In'),
  2093. { chart: chart }
  2094. )
  2095. );
  2096. });
  2097. chart.setFocusToElement(initialButton.box, initialButton);
  2098. initialButton.setState(2);
  2099. chart.focusedMapNavButtonIx = direction > 0 ? 0 : 1;
  2100. }
  2101. }),
  2102. // Highstock range selector (minus input boxes)
  2103. navModuleFactory('rangeSelector', [
  2104. // Left/Right/Up/Down
  2105. [[37, 39, 38, 40], function (keyCode) {
  2106. var direction = (keyCode === 37 || keyCode === 38) ? -1 : 1;
  2107. // Try to highlight next/prev button
  2108. if (
  2109. !chart.highlightRangeSelectorButton(
  2110. chart.highlightedRangeSelectorItemIx + direction
  2111. )
  2112. ) {
  2113. return this.move(direction);
  2114. }
  2115. }],
  2116. // Enter/Spacebar
  2117. [[13, 32], function () {
  2118. // Don't allow click if button used to be disabled
  2119. if (chart.oldRangeSelectorItemState !== 3) {
  2120. fakeClickEvent(
  2121. chart.rangeSelector.buttons[
  2122. chart.highlightedRangeSelectorItemIx
  2123. ].element
  2124. );
  2125. }
  2126. }]
  2127. ], {
  2128. // Only run this module if we have range selector
  2129. validate: function () {
  2130. return (
  2131. chart.rangeSelector &&
  2132. chart.rangeSelector.buttons &&
  2133. chart.rangeSelector.buttons.length
  2134. );
  2135. },
  2136. // Make elements focusable and accessible
  2137. init: function (direction) {
  2138. each(chart.rangeSelector.buttons, function (button) {
  2139. button.element.setAttribute('tabindex', '-1');
  2140. button.element.setAttribute('role', 'button');
  2141. button.element.setAttribute(
  2142. 'aria-label',
  2143. chart.langFormat(
  2144. 'accessibility.rangeSelectorButton',
  2145. {
  2146. chart: chart,
  2147. buttonText: button.text && button.text.textStr
  2148. }
  2149. )
  2150. );
  2151. });
  2152. // Focus first/last button
  2153. chart.highlightRangeSelectorButton(
  2154. direction > 0 ? 0 : chart.rangeSelector.buttons.length - 1
  2155. );
  2156. }
  2157. }),
  2158. // Highstock range selector, input boxes
  2159. navModuleFactory('rangeSelectorInput', [
  2160. // Tab/Up/Down
  2161. [[9, 38, 40], function (keyCode, e) {
  2162. var direction =
  2163. (keyCode === 9 && e.shiftKey || keyCode === 38) ? -1 : 1,
  2164. newIx = chart.highlightedInputRangeIx =
  2165. chart.highlightedInputRangeIx + direction;
  2166. // Try to highlight next/prev item in list.
  2167. if (newIx > 1 || newIx < 0) { // Out of range
  2168. return this.move(direction);
  2169. }
  2170. chart.rangeSelector[newIx ? 'maxInput' : 'minInput'].focus();
  2171. }]
  2172. ], {
  2173. // Only run if we have range selector with input boxes
  2174. validate: function () {
  2175. var inputVisible = (
  2176. chart.rangeSelector &&
  2177. chart.rangeSelector.inputGroup &&
  2178. chart.rangeSelector.inputGroup.element
  2179. .getAttribute('visibility') !== 'hidden'
  2180. );
  2181. return (
  2182. inputVisible &&
  2183. chart.options.rangeSelector.inputEnabled !== false &&
  2184. chart.rangeSelector.minInput &&
  2185. chart.rangeSelector.maxInput
  2186. );
  2187. },
  2188. // Highlight first/last input box
  2189. init: function (direction) {
  2190. chart.highlightedInputRangeIx = direction > 0 ? 0 : 1;
  2191. chart.rangeSelector[
  2192. chart.highlightedInputRangeIx ? 'maxInput' : 'minInput'
  2193. ].focus();
  2194. }
  2195. }),
  2196. // Legend navigation
  2197. navModuleFactory('legend', [
  2198. // Left/Right/Up/Down
  2199. [[37, 39, 38, 40], function (keyCode) {
  2200. var direction = (keyCode === 37 || keyCode === 38) ? -1 : 1;
  2201. // Try to highlight next/prev legend item
  2202. if (!chart.highlightLegendItem(
  2203. chart.highlightedLegendItemIx + direction
  2204. ) && chart.legend.allItems.length > 1) {
  2205. // Wrap around if more than 1 item
  2206. this.init(direction);
  2207. }
  2208. }],
  2209. // Enter/Spacebar
  2210. [[13, 32], function () {
  2211. fakeClickEvent(
  2212. chart.legend.allItems[
  2213. chart.highlightedLegendItemIx
  2214. ].legendItem.element.parentNode
  2215. );
  2216. }]
  2217. ], {
  2218. // Only run this module if we have at least one legend - wait for
  2219. // it - item. Don't run if the legend is populated by a colorAxis.
  2220. // Don't run if legend navigation is disabled.
  2221. validate: function () {
  2222. return chart.legend && chart.legend.allItems &&
  2223. chart.legend.display &&
  2224. !(chart.colorAxis && chart.colorAxis.length) &&
  2225. (chart.options.legend &&
  2226. chart.options.legend.keyboardNavigation &&
  2227. chart.options.legend.keyboardNavigation.enabled) !== false;
  2228. },
  2229. // Make elements focusable and accessible
  2230. init: function (direction) {
  2231. each(chart.legend.allItems, function (item) {
  2232. item.legendGroup.element.setAttribute('tabindex', '-1');
  2233. item.legendGroup.element.setAttribute('role', 'button');
  2234. item.legendGroup.element.setAttribute(
  2235. 'aria-label',
  2236. chart.langFormat(
  2237. 'accessibility.legendItem',
  2238. {
  2239. chart: chart,
  2240. itemName: stripTags(item.name)
  2241. }
  2242. )
  2243. );
  2244. });
  2245. // Focus first/last item
  2246. chart.highlightLegendItem(
  2247. direction > 0 ? 0 : chart.legend.allItems.length - 1
  2248. );
  2249. }
  2250. })
  2251. ];
  2252. };
  2253. // Add exit anchor to the chart
  2254. // We use this to move focus out of chart whenever we want, by setting focus
  2255. // to this div and not preventing the default tab action.
  2256. // We also use this when users come back into the chart by tabbing back, in
  2257. // order to navigate from the end of the chart.
  2258. // Function returns the unbind function for the exit anchor's event handler.
  2259. H.Chart.prototype.addExitAnchor = function () {
  2260. var chart = this;
  2261. chart.tabExitAnchor = doc.createElement('div');
  2262. chart.tabExitAnchor.setAttribute('tabindex', '0');
  2263. // Hide exit anchor
  2264. merge(true, chart.tabExitAnchor.style, {
  2265. position: 'absolute',
  2266. left: '-9999px',
  2267. top: 'auto',
  2268. width: '1px',
  2269. height: '1px',
  2270. overflow: 'hidden'
  2271. });
  2272. chart.renderTo.appendChild(chart.tabExitAnchor);
  2273. return addEvent(chart.tabExitAnchor, 'focus',
  2274. function (ev) {
  2275. var e = ev || win.event,
  2276. curModule;
  2277. // If focusing and we are exiting, do nothing once.
  2278. if (!chart.exiting) {
  2279. // Not exiting, means we are coming in backwards
  2280. chart.renderTo.focus();
  2281. e.preventDefault();
  2282. // Move to last valid keyboard nav module
  2283. // Note the we don't run it, just set the index
  2284. chart.keyboardNavigationModuleIndex =
  2285. chart.keyboardNavigationModules.length - 1;
  2286. curModule = chart.keyboardNavigationModules[
  2287. chart.keyboardNavigationModuleIndex
  2288. ];
  2289. // Validate the module
  2290. if (curModule.validate && !curModule.validate()) {
  2291. // Invalid.
  2292. // Move inits next valid module in direction
  2293. curModule.move(-1);
  2294. } else {
  2295. // We have a valid module, init it
  2296. curModule.init(-1);
  2297. }
  2298. } else {
  2299. // Don't skip the next focus, we only skip once.
  2300. chart.exiting = false;
  2301. }
  2302. }
  2303. );
  2304. };
  2305. // Clear the chart and reset the navigation state
  2306. H.Chart.prototype.resetKeyboardNavigation = function () {
  2307. var chart = this,
  2308. curMod = (
  2309. chart.keyboardNavigationModules &&
  2310. chart.keyboardNavigationModules[
  2311. chart.keyboardNavigationModuleIndex || 0
  2312. ]
  2313. );
  2314. if (curMod && curMod.terminate) {
  2315. curMod.terminate();
  2316. }
  2317. if (chart.focusElement) {
  2318. chart.focusElement.removeFocusBorder();
  2319. }
  2320. chart.keyboardNavigationModuleIndex = 0;
  2321. chart.keyboardReset = true;
  2322. };
  2323. /**
  2324. * On destroy, we need to clean up the focus border and the state
  2325. */
  2326. H.addEvent(H.Series, 'destroy', function () {
  2327. var chart = this.chart;
  2328. if (chart.highlightedPoint && chart.highlightedPoint.series === this) {
  2329. delete chart.highlightedPoint;
  2330. if (chart.focusElement) {
  2331. chart.focusElement.removeFocusBorder();
  2332. }
  2333. }
  2334. });
  2335. // Add keyboard navigation events on chart load
  2336. H.Chart.prototype.callbacks.push(function (chart) {
  2337. var a11yOptions = chart.options.accessibility;
  2338. if (a11yOptions.enabled && a11yOptions.keyboardNavigation.enabled) {
  2339. // Test if we have focus support for SVG elements
  2340. hasSVGFocusSupport = !!chart.renderTo
  2341. .getElementsByTagName('g')[0].focus;
  2342. // Init nav modules. We start at the first module, and as the user
  2343. // navigates through the chart the index will increase to use different
  2344. // handler modules.
  2345. chart.addKeyboardNavigationModules();
  2346. chart.keyboardNavigationModuleIndex = 0;
  2347. // Make chart container reachable by tab
  2348. if (
  2349. chart.container.hasAttribute &&
  2350. !chart.container.hasAttribute('tabIndex')
  2351. ) {
  2352. chart.container.setAttribute('tabindex', '0');
  2353. }
  2354. // Add tab exit anchor
  2355. if (!chart.tabExitAnchor) {
  2356. chart.unbindExitAnchorFocus = chart.addExitAnchor();
  2357. }
  2358. // Handle keyboard events by routing them to active keyboard nav module
  2359. chart.unbindKeydownHandler = addEvent(chart.renderTo, 'keydown',
  2360. function (ev) {
  2361. var e = ev || win.event,
  2362. curNavModule = chart.keyboardNavigationModules[
  2363. chart.keyboardNavigationModuleIndex
  2364. ];
  2365. chart.keyboardReset = false;
  2366. // If there is a nav module for the current index, run it.
  2367. // Otherwise, we are outside of the chart in some direction.
  2368. if (curNavModule) {
  2369. if (curNavModule.run(e)) {
  2370. // Successfully handled this key event, stop default
  2371. e.preventDefault();
  2372. }
  2373. }
  2374. });
  2375. // Reset chart navigation state if we click outside the chart and it's
  2376. // not already reset
  2377. chart.unbindBlurHandler = addEvent(doc, 'mouseup', function () {
  2378. if (
  2379. !chart.keyboardReset &&
  2380. !(chart.pointer && chart.pointer.chartPosition)
  2381. ) {
  2382. chart.resetKeyboardNavigation();
  2383. }
  2384. });
  2385. // Add cleanup handlers
  2386. addEvent(chart, 'destroy', function () {
  2387. chart.resetKeyboardNavigation();
  2388. if (chart.unbindExitAnchorFocus && chart.tabExitAnchor) {
  2389. chart.unbindExitAnchorFocus();
  2390. }
  2391. if (chart.unbindKeydownHandler && chart.renderTo) {
  2392. chart.unbindKeydownHandler();
  2393. }
  2394. if (chart.unbindBlurHandler) {
  2395. chart.unbindBlurHandler();
  2396. }
  2397. });
  2398. }
  2399. });
  2400. }(Highcharts));
  2401. }));