screen-reader.src.js 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897
  1. /**
  2. * Accessibility module - Screen Reader support
  3. *
  4. * (c) 2010-2017 Highsoft AS
  5. * Author: Oystein Moseng
  6. *
  7. * License: www.highcharts.com/license
  8. */
  9. 'use strict';
  10. import H from '../parts/Globals.js';
  11. import '../parts/Utilities.js';
  12. import '../parts/Chart.js';
  13. import '../parts/Series.js';
  14. import '../parts/Point.js';
  15. var win = H.win,
  16. doc = win.document,
  17. erase = H.erase,
  18. addEvent = H.addEvent,
  19. merge = H.merge,
  20. // CSS style to hide element from visual users while still exposing it to
  21. // screen readers
  22. hiddenStyle = {
  23. position: 'absolute',
  24. left: '-9999px',
  25. top: 'auto',
  26. width: '1px',
  27. height: '1px',
  28. overflow: 'hidden'
  29. };
  30. // If a point has one of the special keys defined, we expose all keys to the
  31. // screen reader.
  32. H.Series.prototype.commonKeys = ['name', 'id', 'category', 'x', 'value', 'y'];
  33. H.Series.prototype.specialKeys = [
  34. 'z', 'open', 'high', 'q3', 'median', 'q1', 'low', 'close'
  35. ];
  36. if (H.seriesTypes.pie) {
  37. // A pie is always simple. Don't quote me on that.
  38. H.seriesTypes.pie.prototype.specialKeys = [];
  39. }
  40. /**
  41. * HTML encode some characters vulnerable for XSS.
  42. *
  43. * @private
  44. * @function htmlencode
  45. *
  46. * @param {string} html
  47. * The input string.
  48. *
  49. * @return {string}
  50. * The excaped string.
  51. */
  52. function htmlencode(html) {
  53. return html
  54. .replace(/&/g, '&')
  55. .replace(/</g, '&lt;')
  56. .replace(/>/g, '&gt;')
  57. .replace(/"/g, '&quot;')
  58. .replace(/'/g, '&#x27;')
  59. .replace(/\//g, '&#x2F;');
  60. }
  61. /**
  62. * Strip HTML tags away from a string. Used for aria-label attributes, painting
  63. * on a canvas will fail if the text contains tags.
  64. *
  65. * @private
  66. * @function stripTags
  67. *
  68. * @param {string} s
  69. * The input string.
  70. *
  71. * @return {string}
  72. * The filtered string.
  73. */
  74. function stripTags(s) {
  75. return typeof s === 'string' ? s.replace(/<\/?[^>]+(>|$)/g, '') : s;
  76. }
  77. // Accessibility options
  78. H.setOptions({
  79. /**
  80. * Options for configuring accessibility for the chart. Requires the
  81. * [accessibility module](https://code.highcharts.com/modules/accessibility.js)
  82. * to be loaded. For a description of the module and information
  83. * on its features, see
  84. * [Highcharts Accessibility](http://www.highcharts.com/docs/chart-concepts/accessibility).
  85. *
  86. * @since 5.0.0
  87. * @optionparent accessibility
  88. */
  89. accessibility: {
  90. /**
  91. * Whether or not to add series descriptions to charts with a single
  92. * series.
  93. *
  94. * @type {boolean}
  95. * @default false
  96. * @since 5.0.0
  97. * @apioption accessibility.describeSingleSeries
  98. */
  99. /**
  100. * Function to run upon clicking the "View as Data Table" link in the
  101. * screen reader region.
  102. *
  103. * By default Highcharts will insert and set focus to a data table
  104. * representation of the chart.
  105. *
  106. * @type {Function}
  107. * @since 5.0.0
  108. * @apioption accessibility.onTableAnchorClick
  109. */
  110. /**
  111. * Date format to use for points on datetime axes when describing them
  112. * to screen reader users.
  113. *
  114. * Defaults to the same format as in tooltip.
  115. *
  116. * For an overview of the replacement codes, see
  117. * [dateFormat](/class-reference/Highcharts#dateFormat).
  118. *
  119. * @see [pointDateFormatter](#accessibility.pointDateFormatter)
  120. *
  121. * @type {string}
  122. * @since 5.0.0
  123. * @apioption accessibility.pointDateFormat
  124. */
  125. /**
  126. * Formatter function to determine the date/time format used with
  127. * points on datetime axes when describing them to screen reader users.
  128. * Receives one argument, `point`, referring to the point to describe.
  129. * Should return a date format string compatible with
  130. * [dateFormat](/class-reference/Highcharts#dateFormat).
  131. *
  132. * @see [pointDateFormat](#accessibility.pointDateFormat)
  133. *
  134. * @type {Function}
  135. * @since 5.0.0
  136. * @apioption accessibility.pointDateFormatter
  137. */
  138. /**
  139. * Formatter function to use instead of the default for point
  140. * descriptions.
  141. * Receives one argument, `point`, referring to the point to describe.
  142. * Should return a String with the description of the point for a screen
  143. * reader user.
  144. *
  145. * @see [point.description](#series.line.data.description)
  146. *
  147. * @type {Function}
  148. * @since 5.0.0
  149. * @apioption accessibility.pointDescriptionFormatter
  150. */
  151. /**
  152. * Formatter function to use instead of the default for series
  153. * descriptions. Receives one argument, `series`, referring to the
  154. * series to describe. Should return a String with the description of
  155. * the series for a screen reader user.
  156. *
  157. * @see [series.description](#plotOptions.series.description)
  158. *
  159. * @type {Function}
  160. * @since 5.0.0
  161. * @apioption accessibility.seriesDescriptionFormatter
  162. */
  163. /**
  164. * Enable accessibility features for the chart.
  165. *
  166. * @since 5.0.0
  167. */
  168. enabled: true,
  169. /**
  170. * When a series contains more points than this, we no longer expose
  171. * information about individual points to screen readers.
  172. *
  173. * Set to `false` to disable.
  174. *
  175. * @type {false|number}
  176. * @since 5.0.0
  177. */
  178. pointDescriptionThreshold: false, // set to false to disable
  179. /**
  180. * A formatter function to create the HTML contents of the hidden screen
  181. * reader information region. Receives one argument, `chart`, referring
  182. * to the chart object. Should return a String with the HTML content
  183. * of the region.
  184. *
  185. * The link to view the chart as a data table will be added
  186. * automatically after the custom HTML content.
  187. *
  188. * @type {Function}
  189. * @default undefined
  190. * @since 5.0.0
  191. */
  192. screenReaderSectionFormatter: function (chart) {
  193. var options = chart.options,
  194. chartTypes = chart.types || [],
  195. formatContext = {
  196. chart: chart,
  197. numSeries: chart.series && chart.series.length
  198. },
  199. // Build axis info - but not for pies and maps. Consider not
  200. // adding for certain other types as well (funnel, pyramid?)
  201. axesDesc = (
  202. chartTypes.length === 1 && chartTypes[0] === 'pie' ||
  203. chartTypes[0] === 'map'
  204. ) && {} || chart.getAxesDescription();
  205. return '<div>' + chart.langFormat(
  206. 'accessibility.navigationHint', formatContext
  207. ) + '</div><h3>' +
  208. (
  209. options.title.text ?
  210. htmlencode(options.title.text) :
  211. chart.langFormat(
  212. 'accessibility.defaultChartTitle', formatContext
  213. )
  214. ) +
  215. (
  216. options.subtitle && options.subtitle.text ?
  217. '. ' + htmlencode(options.subtitle.text) :
  218. ''
  219. ) +
  220. '</h3><h4>' + chart.langFormat(
  221. 'accessibility.longDescriptionHeading', formatContext
  222. ) + '</h4><div>' +
  223. (
  224. options.chart.description || chart.langFormat(
  225. 'accessibility.noDescription', formatContext
  226. )
  227. ) +
  228. '</div><h4>' + chart.langFormat(
  229. 'accessibility.structureHeading', formatContext
  230. ) + '</h4><div>' +
  231. (
  232. options.chart.typeDescription ||
  233. chart.getTypeDescription()
  234. ) + '</div>' +
  235. (axesDesc.xAxis ? (
  236. '<div>' + axesDesc.xAxis + '</div>'
  237. ) : '') +
  238. (axesDesc.yAxis ? (
  239. '<div>' + axesDesc.yAxis + '</div>'
  240. ) : '');
  241. }
  242. }
  243. });
  244. /**
  245. * A text description of the chart.
  246. *
  247. * If the Accessibility module is loaded, this is included by default
  248. * as a long description of the chart and its contents in the hidden
  249. * screen reader information region.
  250. *
  251. * @see [typeDescription](#chart.typeDescription)
  252. *
  253. * @type {string}
  254. * @since 5.0.0
  255. * @apioption chart.description
  256. */
  257. /**
  258. * A text description of the chart type.
  259. *
  260. * If the Accessibility module is loaded, this will be included in the
  261. * description of the chart in the screen reader information region.
  262. *
  263. *
  264. * Highcharts will by default attempt to guess the chart type, but for
  265. * more complex charts it is recommended to specify this property for
  266. * clarity.
  267. *
  268. * @type {string}
  269. * @since 5.0.0
  270. * @apioption chart.typeDescription
  271. */
  272. /**
  273. * Utility function. Reverses child nodes of a DOM element.
  274. *
  275. * @private
  276. * @function reverseChildNodes
  277. *
  278. * @param {Highcharts.HTMLDOMElement|Highcharts.SVGDOMElement} node
  279. */
  280. function reverseChildNodes(node) {
  281. var i = node.childNodes.length;
  282. while (i--) {
  283. node.appendChild(node.childNodes[i]);
  284. }
  285. }
  286. // Whenever drawing series, put info on DOM elements
  287. H.addEvent(H.Series, 'afterRender', function () {
  288. if (this.chart.options.accessibility.enabled) {
  289. this.setA11yDescription();
  290. }
  291. });
  292. /**
  293. * Put accessible info on series and points of a series.
  294. *
  295. * @private
  296. * @function Highcharts.Series#setA11yDescription
  297. */
  298. H.Series.prototype.setA11yDescription = function () {
  299. var a11yOptions = this.chart.options.accessibility,
  300. firstPointEl = (
  301. this.points &&
  302. this.points.length &&
  303. this.points[0].graphic &&
  304. this.points[0].graphic.element
  305. ),
  306. seriesEl = (
  307. firstPointEl &&
  308. firstPointEl.parentNode || this.graph &&
  309. this.graph.element || this.group &&
  310. this.group.element
  311. ); // Could be tracker series depending on series type
  312. if (seriesEl) {
  313. // For some series types the order of elements do not match the order of
  314. // points in series. In that case we have to reverse them in order for
  315. // AT to read them out in an understandable order
  316. if (seriesEl.lastChild === firstPointEl) {
  317. reverseChildNodes(seriesEl);
  318. }
  319. // Make individual point elements accessible if possible. Note: If
  320. // markers are disabled there might not be any elements there to make
  321. // accessible.
  322. if (
  323. this.points && (
  324. this.points.length < a11yOptions.pointDescriptionThreshold ||
  325. a11yOptions.pointDescriptionThreshold === false
  326. )
  327. ) {
  328. this.points.forEach(function (point) {
  329. if (point.graphic) {
  330. point.graphic.element.setAttribute('role', 'img');
  331. point.graphic.element.setAttribute('tabindex', '-1');
  332. point.graphic.element.setAttribute('aria-label', stripTags(
  333. point.series.options.pointDescriptionFormatter &&
  334. point.series.options.pointDescriptionFormatter(point) ||
  335. a11yOptions.pointDescriptionFormatter &&
  336. a11yOptions.pointDescriptionFormatter(point) ||
  337. point.buildPointInfoString()
  338. ));
  339. }
  340. });
  341. }
  342. // Make series element accessible
  343. if (this.chart.series.length > 1 || a11yOptions.describeSingleSeries) {
  344. seriesEl.setAttribute(
  345. 'role',
  346. this.options.exposeElementToA11y ? 'img' : 'region'
  347. );
  348. seriesEl.setAttribute('tabindex', '-1');
  349. seriesEl.setAttribute(
  350. 'aria-label',
  351. stripTags(
  352. a11yOptions.seriesDescriptionFormatter &&
  353. a11yOptions.seriesDescriptionFormatter(this) ||
  354. this.buildSeriesInfoString()
  355. )
  356. );
  357. }
  358. }
  359. };
  360. /**
  361. * Return string with information about series.
  362. *
  363. * @private
  364. * @function Highcharts.Series#buildSeriesInfoString
  365. *
  366. * @return {string}
  367. */
  368. H.Series.prototype.buildSeriesInfoString = function () {
  369. var chart = this.chart,
  370. desc = this.description || this.options.description,
  371. description = desc && chart.langFormat(
  372. 'accessibility.series.description', {
  373. description: desc,
  374. series: this
  375. }
  376. ),
  377. xAxisInfo = chart.langFormat(
  378. 'accessibility.series.xAxisDescription',
  379. {
  380. name: this.xAxis && this.xAxis.getDescription(),
  381. series: this
  382. }
  383. ),
  384. yAxisInfo = chart.langFormat(
  385. 'accessibility.series.yAxisDescription',
  386. {
  387. name: this.yAxis && this.yAxis.getDescription(),
  388. series: this
  389. }
  390. ),
  391. summaryContext = {
  392. name: this.name || '',
  393. ix: this.index + 1,
  394. numSeries: chart.series.length,
  395. numPoints: this.points.length,
  396. series: this
  397. },
  398. combination = chart.types.length === 1 ? '' : 'Combination',
  399. summary = chart.langFormat(
  400. 'accessibility.series.summary.' + this.type + combination,
  401. summaryContext
  402. ) || chart.langFormat(
  403. 'accessibility.series.summary.default' + combination,
  404. summaryContext
  405. );
  406. return summary + (description ? ' ' + description : '') + (
  407. chart.yAxis.length > 1 && this.yAxis ?
  408. ' ' + yAxisInfo : ''
  409. ) + (
  410. chart.xAxis.length > 1 && this.xAxis ?
  411. ' ' + xAxisInfo : ''
  412. );
  413. };
  414. /**
  415. * Return string with information about point.
  416. *
  417. * @private
  418. * @function Highcharts.Point#buildPointInfoString
  419. *
  420. * @return {string}
  421. */
  422. H.Point.prototype.buildPointInfoString = function () {
  423. var point = this,
  424. series = point.series,
  425. a11yOptions = series.chart.options.accessibility,
  426. infoString = '',
  427. dateTimePoint = series.xAxis && series.xAxis.isDatetimeAxis,
  428. timeDesc =
  429. dateTimePoint &&
  430. series.chart.time.dateFormat(
  431. a11yOptions.pointDateFormatter &&
  432. a11yOptions.pointDateFormatter(point) ||
  433. a11yOptions.pointDateFormat ||
  434. H.Tooltip.prototype.getXDateFormat.call(
  435. {
  436. getDateFormat: H.Tooltip.prototype.getDateFormat,
  437. chart: series.chart
  438. },
  439. point,
  440. series.chart.options.tooltip,
  441. series.xAxis
  442. ),
  443. point.x
  444. ),
  445. hasSpecialKey = H.find(series.specialKeys, function (key) {
  446. return point[key] !== undefined;
  447. });
  448. // If the point has one of the less common properties defined, display all
  449. // that are defined
  450. if (hasSpecialKey) {
  451. if (dateTimePoint) {
  452. infoString = timeDesc;
  453. }
  454. series.commonKeys.concat(series.specialKeys).forEach(function (key) {
  455. if (point[key] !== undefined && !(dateTimePoint && key === 'x')) {
  456. infoString += (infoString ? '. ' : '') +
  457. key + ', ' +
  458. point[key];
  459. }
  460. });
  461. } else {
  462. // Pick and choose properties for a succint label
  463. infoString =
  464. (
  465. this.name ||
  466. timeDesc ||
  467. this.category ||
  468. this.id ||
  469. 'x, ' + this.x
  470. ) + ', ' +
  471. (this.value !== undefined ? this.value : this.y);
  472. }
  473. return (this.index + 1) + '. ' + infoString + '.' +
  474. (this.description ? ' ' + this.description : '');
  475. };
  476. /**
  477. * Get descriptive label for axis.
  478. *
  479. * @private
  480. * @function Highcharts.Axis#getDescription
  481. *
  482. * @return {string}
  483. */
  484. H.Axis.prototype.getDescription = function () {
  485. return (
  486. this.userOptions && this.userOptions.description ||
  487. this.axisTitle && this.axisTitle.textStr ||
  488. this.options.id ||
  489. this.categories && 'categories' ||
  490. this.isDatetimeAxis && 'Time' ||
  491. 'values'
  492. );
  493. };
  494. // Whenever adding or removing series, keep track of types present in chart
  495. addEvent(H.Series, 'afterInit', function () {
  496. var chart = this.chart;
  497. if (chart.options.accessibility.enabled) {
  498. chart.types = chart.types || [];
  499. // Add type to list if does not exist
  500. if (chart.types.indexOf(this.type) < 0) {
  501. chart.types.push(this.type);
  502. }
  503. }
  504. });
  505. addEvent(H.Series, 'remove', function () {
  506. var chart = this.chart,
  507. removedSeries = this,
  508. hasType = false;
  509. // Check if any of the other series have the same type as this one.
  510. // Otherwise remove it from the list.
  511. chart.series.forEach(function (s) {
  512. if (
  513. s !== removedSeries &&
  514. chart.types.indexOf(removedSeries.type) < 0
  515. ) {
  516. hasType = true;
  517. }
  518. });
  519. if (!hasType) {
  520. erase(chart.types, removedSeries.type);
  521. }
  522. });
  523. /**
  524. * Return simplified description of chart type. Some types will not be familiar
  525. * to most screen reader users, but in those cases we try to add a description
  526. * of the type.
  527. *
  528. * @private
  529. * @function Highcharts.Chart#getTypeDescription
  530. *
  531. * @return {string}
  532. */
  533. H.Chart.prototype.getTypeDescription = function () {
  534. var firstType = this.types && this.types[0],
  535. firstSeries = this.series && this.series[0] || {},
  536. mapTitle = firstSeries.mapTitle,
  537. typeDesc = this.langFormat(
  538. 'accessibility.seriesTypeDescriptions.' + firstType,
  539. { chart: this }
  540. ),
  541. formatContext = {
  542. numSeries: this.series.length,
  543. numPoints: firstSeries.points && firstSeries.points.length,
  544. chart: this,
  545. mapTitle: mapTitle
  546. },
  547. multi = this.series && this.series.length === 1 ? 'Single' : 'Multiple';
  548. if (!firstType) {
  549. return this.langFormat(
  550. 'accessibility.chartTypes.emptyChart', formatContext
  551. );
  552. }
  553. if (firstType === 'map') {
  554. return mapTitle ?
  555. this.langFormat(
  556. 'accessibility.chartTypes.mapTypeDescription',
  557. formatContext
  558. ) :
  559. this.langFormat(
  560. 'accessibility.chartTypes.unknownMap',
  561. formatContext
  562. );
  563. }
  564. if (this.types.length > 1) {
  565. return this.langFormat(
  566. 'accessibility.chartTypes.combinationChart', formatContext
  567. );
  568. }
  569. return (
  570. this.langFormat(
  571. 'accessibility.chartTypes.' + firstType + multi,
  572. formatContext
  573. ) ||
  574. this.langFormat(
  575. 'accessibility.chartTypes.default' + multi,
  576. formatContext
  577. )
  578. ) +
  579. (typeDesc ? ' ' + typeDesc : '');
  580. };
  581. /**
  582. * Return object with text description of each of the chart's axes.
  583. *
  584. * @private
  585. * @function Highcharts.Chart#getAxesDescription
  586. *
  587. * @return {*}
  588. */
  589. H.Chart.prototype.getAxesDescription = function () {
  590. var numXAxes = this.xAxis.length,
  591. numYAxes = this.yAxis.length,
  592. desc = {};
  593. if (numXAxes) {
  594. desc.xAxis = this.langFormat(
  595. 'accessibility.axis.xAxisDescription' + (
  596. numXAxes > 1 ? 'Plural' : 'Singular'
  597. ),
  598. {
  599. chart: this,
  600. names: this.xAxis.map(function (axis) {
  601. return axis.getDescription();
  602. }),
  603. numAxes: numXAxes
  604. }
  605. );
  606. }
  607. if (numYAxes) {
  608. desc.yAxis = this.langFormat(
  609. 'accessibility.axis.yAxisDescription' + (
  610. numYAxes > 1 ? 'Plural' : 'Singular'
  611. ),
  612. {
  613. chart: this,
  614. names: this.yAxis.map(function (axis) {
  615. return axis.getDescription();
  616. }),
  617. numAxes: numYAxes
  618. }
  619. );
  620. }
  621. return desc;
  622. };
  623. /**
  624. * Set a11y attribs on exporting menu.
  625. *
  626. * @private
  627. * @function Highcharts.Chart#addAccessibleContextMenuAttribs
  628. */
  629. H.Chart.prototype.addAccessibleContextMenuAttribs = function () {
  630. var exportList = this.exportDivElements;
  631. if (exportList) {
  632. // Set tabindex on the menu items to allow focusing by script
  633. // Set role to give screen readers a chance to pick up the contents
  634. exportList.forEach(function (item) {
  635. if (item.tagName === 'DIV' &&
  636. !(item.children && item.children.length)) {
  637. item.setAttribute('role', 'menuitem');
  638. item.setAttribute('tabindex', -1);
  639. }
  640. });
  641. // Set accessibility properties on parent div
  642. exportList[0].parentNode.setAttribute('role', 'menu');
  643. exportList[0].parentNode.setAttribute(
  644. 'aria-label',
  645. this.langFormat(
  646. 'accessibility.exporting.chartMenuLabel', { chart: this }
  647. )
  648. );
  649. }
  650. };
  651. /**
  652. * Add screen reader region to chart. tableId is the HTML id of the table to
  653. * focus when clicking the table anchor in the screen reader region.
  654. *
  655. * @private
  656. * @function Highcharts.Chart#addScreenReaderRegion
  657. *
  658. * @param {string} id
  659. *
  660. * @param {string} tableId
  661. */
  662. H.Chart.prototype.addScreenReaderRegion = function (id, tableId) {
  663. var chart = this,
  664. hiddenSection = chart.screenReaderRegion = doc.createElement('div'),
  665. tableShortcut = doc.createElement('h4'),
  666. tableShortcutAnchor = doc.createElement('a'),
  667. chartHeading = doc.createElement('h4');
  668. hiddenSection.setAttribute('id', id);
  669. hiddenSection.setAttribute('role', 'region');
  670. hiddenSection.setAttribute(
  671. 'aria-label',
  672. chart.langFormat(
  673. 'accessibility.screenReaderRegionLabel', { chart: this }
  674. )
  675. );
  676. hiddenSection.innerHTML = chart.options.accessibility
  677. .screenReaderSectionFormatter(chart);
  678. // Add shortcut to data table if export-data is loaded
  679. if (chart.getCSV) {
  680. tableShortcutAnchor.innerHTML = chart.langFormat(
  681. 'accessibility.viewAsDataTable', { chart: chart }
  682. );
  683. tableShortcutAnchor.href = '#' + tableId;
  684. // Make this unreachable by user tabbing
  685. tableShortcutAnchor.setAttribute('tabindex', '-1');
  686. tableShortcutAnchor.onclick =
  687. chart.options.accessibility.onTableAnchorClick || function () {
  688. chart.viewData();
  689. doc.getElementById(tableId).focus();
  690. };
  691. tableShortcut.appendChild(tableShortcutAnchor);
  692. hiddenSection.appendChild(tableShortcut);
  693. }
  694. // Note: JAWS seems to refuse to read aria-label on the container, so add an
  695. // h4 element as title for the chart.
  696. chartHeading.innerHTML = chart.langFormat(
  697. 'accessibility.chartHeading', { chart: chart }
  698. );
  699. chart.renderTo.insertBefore(chartHeading, chart.renderTo.firstChild);
  700. chart.renderTo.insertBefore(hiddenSection, chart.renderTo.firstChild);
  701. // Hide the section and the chart heading
  702. merge(true, chartHeading.style, hiddenStyle);
  703. merge(true, hiddenSection.style, hiddenStyle);
  704. };
  705. // Make chart container accessible, and wrap table functionality.
  706. H.Chart.prototype.callbacks.push(function (chart) {
  707. var options = chart.options,
  708. a11yOptions = options.accessibility;
  709. if (!a11yOptions.enabled) {
  710. return;
  711. }
  712. var titleElement,
  713. descElement = chart.container.getElementsByTagName('desc')[0],
  714. textElements = chart.container.getElementsByTagName('text'),
  715. titleId = 'highcharts-title-' + chart.index,
  716. tableId = 'highcharts-data-table-' + chart.index,
  717. hiddenSectionId = 'highcharts-information-region-' + chart.index,
  718. chartTitle = options.title.text || chart.langFormat(
  719. 'accessibility.defaultChartTitle', { chart: chart }
  720. ),
  721. svgContainerTitle = stripTags(chart.langFormat(
  722. 'accessibility.svgContainerTitle', {
  723. chartTitle: chartTitle
  724. }
  725. ));
  726. // Add SVG title tag if it is set
  727. if (svgContainerTitle.length) {
  728. titleElement = doc.createElementNS(
  729. 'http://www.w3.org/2000/svg',
  730. 'title'
  731. );
  732. titleElement.textContent = svgContainerTitle;
  733. titleElement.id = titleId;
  734. descElement.parentNode.insertBefore(titleElement, descElement);
  735. }
  736. chart.renderTo.setAttribute('role', 'region');
  737. chart.renderTo.setAttribute(
  738. 'aria-label',
  739. chart.langFormat(
  740. 'accessibility.chartContainerLabel',
  741. {
  742. title: stripTags(chartTitle),
  743. chart: chart
  744. }
  745. )
  746. );
  747. // Set screen reader properties on export menu
  748. if (
  749. chart.exportSVGElements &&
  750. chart.exportSVGElements[0] &&
  751. chart.exportSVGElements[0].element
  752. ) {
  753. // Set event handler on button
  754. var button = chart.exportSVGElements[0].element,
  755. oldExportCallback = button.onclick;
  756. button.onclick = function () {
  757. oldExportCallback.apply(
  758. this,
  759. Array.prototype.slice.call(arguments)
  760. );
  761. chart.addAccessibleContextMenuAttribs();
  762. chart.highlightExportItem(0);
  763. };
  764. // Set props on button
  765. button.setAttribute('role', 'button');
  766. button.setAttribute(
  767. 'aria-label',
  768. chart.langFormat(
  769. 'accessibility.exporting.menuButtonLabel', { chart: chart }
  770. )
  771. );
  772. // Set props on group
  773. chart.exportingGroup.element.setAttribute('role', 'region');
  774. chart.exportingGroup.element.setAttribute(
  775. 'aria-label',
  776. chart.langFormat(
  777. 'accessibility.exporting.exportRegionLabel', { chart: chart }
  778. )
  779. );
  780. }
  781. // Set screen reader properties on input boxes for range selector. We need
  782. // to do this regardless of whether or not these are visible, as they are
  783. // by default part of the page's tabindex unless we set them to -1.
  784. if (chart.rangeSelector) {
  785. ['minInput', 'maxInput'].forEach(function (key, i) {
  786. if (chart.rangeSelector[key]) {
  787. chart.rangeSelector[key].setAttribute('tabindex', '-1');
  788. chart.rangeSelector[key].setAttribute('role', 'textbox');
  789. chart.rangeSelector[key].setAttribute(
  790. 'aria-label',
  791. chart.langFormat(
  792. 'accessibility.rangeSelector' +
  793. (i ? 'MaxInput' : 'MinInput'), { chart: chart }
  794. )
  795. );
  796. }
  797. });
  798. }
  799. // Hide text elements from screen readers
  800. [].forEach.call(textElements, function (el) {
  801. el.setAttribute('aria-hidden', 'true');
  802. });
  803. // Add top-secret screen reader region
  804. chart.addScreenReaderRegion(hiddenSectionId, tableId);
  805. // Add ID and summary attr to table HTML
  806. addEvent(chart, 'afterGetTable', function (e) {
  807. e.html = e.html
  808. .replace(
  809. '<table ',
  810. '<table summary="' + chart.langFormat(
  811. 'accessibility.tableSummary', { chart: chart }
  812. ) + '"'
  813. );
  814. });
  815. });