InfoRegionsComponent.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  1. /* *
  2. *
  3. * (c) 2009-2021 Øystein Moseng
  4. *
  5. * Accessibility component for chart info region and table.
  6. *
  7. * License: www.highcharts.com/license
  8. *
  9. * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
  10. *
  11. * */
  12. import AST from '../../Core/Renderer/HTML/AST.js';
  13. import F from '../../Core/FormatUtilities.js';
  14. var format = F.format;
  15. import H from '../../Core/Globals.js';
  16. var doc = H.doc;
  17. import U from '../../Core/Utilities.js';
  18. var extend = U.extend, pick = U.pick;
  19. import AccessibilityComponent from '../AccessibilityComponent.js';
  20. import Announcer from '../Utils/Announcer.js';
  21. import AnnotationsA11y from './AnnotationsA11y.js';
  22. var getAnnotationsInfoHTML = AnnotationsA11y.getAnnotationsInfoHTML;
  23. import ChartUtilities from '../Utils/ChartUtilities.js';
  24. var getAxisDescription = ChartUtilities.getAxisDescription, getAxisRangeDescription = ChartUtilities.getAxisRangeDescription, getChartTitle = ChartUtilities.getChartTitle, unhideChartElementFromAT = ChartUtilities.unhideChartElementFromAT;
  25. import HTMLUtilities from '../Utils/HTMLUtilities.js';
  26. var addClass = HTMLUtilities.addClass, escapeStringForHTML = HTMLUtilities.escapeStringForHTML, getElement = HTMLUtilities.getElement, getHeadingTagNameForElement = HTMLUtilities.getHeadingTagNameForElement, setElAttrs = HTMLUtilities.setElAttrs, stripHTMLTagsFromString = HTMLUtilities.stripHTMLTagsFromString, visuallyHideElement = HTMLUtilities.visuallyHideElement;
  27. /* eslint-disable no-invalid-this, valid-jsdoc */
  28. /**
  29. * @private
  30. */
  31. function stripEmptyHTMLTags(str) {
  32. return str.replace(/<(\w+)[^>]*?>\s*<\/\1>/g, '');
  33. }
  34. /**
  35. * @private
  36. */
  37. function getTypeDescForMapChart(chart, formatContext) {
  38. return formatContext.mapTitle ?
  39. chart.langFormat('accessibility.chartTypes.mapTypeDescription', formatContext) :
  40. chart.langFormat('accessibility.chartTypes.unknownMap', formatContext);
  41. }
  42. /**
  43. * @private
  44. */
  45. function getTypeDescForCombinationChart(chart, formatContext) {
  46. return chart.langFormat('accessibility.chartTypes.combinationChart', formatContext);
  47. }
  48. /**
  49. * @private
  50. */
  51. function getTypeDescForEmptyChart(chart, formatContext) {
  52. return chart.langFormat('accessibility.chartTypes.emptyChart', formatContext);
  53. }
  54. /**
  55. * @private
  56. */
  57. function buildTypeDescriptionFromSeries(chart, types, context) {
  58. var firstType = types[0], typeExplaination = chart.langFormat('accessibility.seriesTypeDescriptions.' + firstType, context), multi = chart.series && chart.series.length < 2 ? 'Single' : 'Multiple';
  59. return (chart.langFormat('accessibility.chartTypes.' + firstType + multi, context) ||
  60. chart.langFormat('accessibility.chartTypes.default' + multi, context)) + (typeExplaination ? ' ' + typeExplaination : '');
  61. }
  62. /**
  63. * @private
  64. */
  65. function getTableSummary(chart) {
  66. return chart.langFormat('accessibility.table.tableSummary', { chart: chart });
  67. }
  68. /**
  69. * Return simplified explaination of chart type. Some types will not be familiar
  70. * to most users, but in those cases we try to add an explaination of the type.
  71. *
  72. * @private
  73. * @function Highcharts.Chart#getTypeDescription
  74. * @param {Array<string>} types The series types in this chart.
  75. * @return {string} The text description of the chart type.
  76. */
  77. H.Chart.prototype.getTypeDescription = function (types) {
  78. var firstType = types[0], firstSeries = this.series && this.series[0] || {}, formatContext = {
  79. numSeries: this.series.length,
  80. numPoints: firstSeries.points && firstSeries.points.length,
  81. chart: this,
  82. mapTitle: firstSeries.mapTitle
  83. };
  84. if (!firstType) {
  85. return getTypeDescForEmptyChart(this, formatContext);
  86. }
  87. if (firstType === 'map') {
  88. return getTypeDescForMapChart(this, formatContext);
  89. }
  90. if (this.types.length > 1) {
  91. return getTypeDescForCombinationChart(this, formatContext);
  92. }
  93. return buildTypeDescriptionFromSeries(this, types, formatContext);
  94. };
  95. /**
  96. * The InfoRegionsComponent class
  97. *
  98. * @private
  99. * @class
  100. * @name Highcharts.InfoRegionsComponent
  101. */
  102. var InfoRegionsComponent = function () { };
  103. InfoRegionsComponent.prototype = new AccessibilityComponent();
  104. extend(InfoRegionsComponent.prototype, /** @lends Highcharts.InfoRegionsComponent */ {
  105. /**
  106. * Init the component
  107. * @private
  108. */
  109. init: function () {
  110. var chart = this.chart;
  111. var component = this;
  112. this.initRegionsDefinitions();
  113. this.addEvent(chart, 'aftergetTableAST', function (e) {
  114. component.onDataTableCreated(e);
  115. });
  116. this.addEvent(chart, 'afterViewData', function (tableDiv) {
  117. component.dataTableDiv = tableDiv;
  118. // Use small delay to give browsers & AT time to register new table
  119. setTimeout(function () {
  120. component.focusDataTable();
  121. }, 300);
  122. });
  123. this.announcer = new Announcer(chart, 'assertive');
  124. },
  125. /**
  126. * @private
  127. */
  128. initRegionsDefinitions: function () {
  129. var component = this;
  130. this.screenReaderSections = {
  131. before: {
  132. element: null,
  133. buildContent: function (chart) {
  134. var formatter = chart.options.accessibility
  135. .screenReaderSection.beforeChartFormatter;
  136. return formatter ? formatter(chart) :
  137. component.defaultBeforeChartFormatter(chart);
  138. },
  139. insertIntoDOM: function (el, chart) {
  140. chart.renderTo.insertBefore(el, chart.renderTo.firstChild);
  141. },
  142. afterInserted: function () {
  143. if (typeof component.sonifyButtonId !== 'undefined') {
  144. component.initSonifyButton(component.sonifyButtonId);
  145. }
  146. if (typeof component.dataTableButtonId !== 'undefined') {
  147. component.initDataTableButton(component.dataTableButtonId);
  148. }
  149. }
  150. },
  151. after: {
  152. element: null,
  153. buildContent: function (chart) {
  154. var formatter = chart.options.accessibility.screenReaderSection
  155. .afterChartFormatter;
  156. return formatter ? formatter(chart) :
  157. component.defaultAfterChartFormatter();
  158. },
  159. insertIntoDOM: function (el, chart) {
  160. chart.renderTo.insertBefore(el, chart.container.nextSibling);
  161. }
  162. }
  163. };
  164. },
  165. /**
  166. * Called on chart render. Have to update the sections on render, in order
  167. * to get a11y info from series.
  168. */
  169. onChartRender: function () {
  170. var component = this;
  171. this.linkedDescriptionElement = this.getLinkedDescriptionElement();
  172. this.setLinkedDescriptionAttrs();
  173. Object.keys(this.screenReaderSections).forEach(function (regionKey) {
  174. component.updateScreenReaderSection(regionKey);
  175. });
  176. },
  177. /**
  178. * @private
  179. */
  180. getLinkedDescriptionElement: function () {
  181. var chartOptions = this.chart.options, linkedDescOption = chartOptions.accessibility.linkedDescription;
  182. if (!linkedDescOption) {
  183. return;
  184. }
  185. if (typeof linkedDescOption !== 'string') {
  186. return linkedDescOption;
  187. }
  188. var query = format(linkedDescOption, this.chart), queryMatch = doc.querySelectorAll(query);
  189. if (queryMatch.length === 1) {
  190. return queryMatch[0];
  191. }
  192. },
  193. /**
  194. * @private
  195. */
  196. setLinkedDescriptionAttrs: function () {
  197. var el = this.linkedDescriptionElement;
  198. if (el) {
  199. el.setAttribute('aria-hidden', 'true');
  200. addClass(el, 'highcharts-linked-description');
  201. }
  202. },
  203. /**
  204. * @private
  205. * @param {string} regionKey The name/key of the region to update
  206. */
  207. updateScreenReaderSection: function (regionKey) {
  208. var chart = this.chart, region = this.screenReaderSections[regionKey], content = region.buildContent(chart), sectionDiv = region.element = (region.element || this.createElement('div')), hiddenDiv = (sectionDiv.firstChild || this.createElement('div'));
  209. this.setScreenReaderSectionAttribs(sectionDiv, regionKey);
  210. AST.setElementHTML(hiddenDiv, content);
  211. sectionDiv.appendChild(hiddenDiv);
  212. region.insertIntoDOM(sectionDiv, chart);
  213. visuallyHideElement(hiddenDiv);
  214. unhideChartElementFromAT(chart, hiddenDiv);
  215. if (region.afterInserted) {
  216. region.afterInserted();
  217. }
  218. },
  219. /**
  220. * @private
  221. * @param {Highcharts.HTMLDOMElement} sectionDiv The section element
  222. * @param {string} regionKey Name/key of the region we are setting attrs for
  223. */
  224. setScreenReaderSectionAttribs: function (sectionDiv, regionKey) {
  225. var labelLangKey = ('accessibility.screenReaderSection.' + regionKey + 'RegionLabel'), chart = this.chart, labelText = chart.langFormat(labelLangKey, { chart: chart }), sectionId = 'highcharts-screen-reader-region-' + regionKey + '-' +
  226. chart.index;
  227. setElAttrs(sectionDiv, {
  228. id: sectionId,
  229. 'aria-label': labelText
  230. });
  231. // Sections are wrapped to be positioned relatively to chart in case
  232. // elements inside are tabbed to.
  233. sectionDiv.style.position = 'relative';
  234. if (chart.options.accessibility.landmarkVerbosity === 'all' &&
  235. labelText) {
  236. sectionDiv.setAttribute('role', 'region');
  237. }
  238. },
  239. /**
  240. * @private
  241. * @return {string}
  242. */
  243. defaultBeforeChartFormatter: function () {
  244. var chart = this.chart, format = chart.options.accessibility
  245. .screenReaderSection.beforeChartFormat, axesDesc = this.getAxesDescription(), shouldHaveSonifyBtn = (chart.sonify &&
  246. chart.options.sonification &&
  247. chart.options.sonification.enabled), sonifyButtonId = 'highcharts-a11y-sonify-data-btn-' +
  248. chart.index, dataTableButtonId = 'hc-linkto-highcharts-data-table-' +
  249. chart.index, annotationsList = getAnnotationsInfoHTML(chart), annotationsTitleStr = chart.langFormat('accessibility.screenReaderSection.annotations.heading', { chart: chart }), context = {
  250. headingTagName: getHeadingTagNameForElement(chart.renderTo),
  251. chartTitle: getChartTitle(chart),
  252. typeDescription: this.getTypeDescriptionText(),
  253. chartSubtitle: this.getSubtitleText(),
  254. chartLongdesc: this.getLongdescText(),
  255. xAxisDescription: axesDesc.xAxis,
  256. yAxisDescription: axesDesc.yAxis,
  257. playAsSoundButton: shouldHaveSonifyBtn ?
  258. this.getSonifyButtonText(sonifyButtonId) : '',
  259. viewTableButton: chart.getCSV ?
  260. this.getDataTableButtonText(dataTableButtonId) : '',
  261. annotationsTitle: annotationsList ? annotationsTitleStr : '',
  262. annotationsList: annotationsList
  263. }, formattedString = H.i18nFormat(format, context, chart);
  264. this.dataTableButtonId = dataTableButtonId;
  265. this.sonifyButtonId = sonifyButtonId;
  266. return stripEmptyHTMLTags(formattedString);
  267. },
  268. /**
  269. * @private
  270. * @return {string}
  271. */
  272. defaultAfterChartFormatter: function () {
  273. var chart = this.chart, format = chart.options.accessibility
  274. .screenReaderSection.afterChartFormat, context = {
  275. endOfChartMarker: this.getEndOfChartMarkerText()
  276. }, formattedString = H.i18nFormat(format, context, chart);
  277. return stripEmptyHTMLTags(formattedString);
  278. },
  279. /**
  280. * @private
  281. * @return {string}
  282. */
  283. getLinkedDescription: function () {
  284. var el = this.linkedDescriptionElement, content = el && el.innerHTML || '';
  285. return stripHTMLTagsFromString(content);
  286. },
  287. /**
  288. * @private
  289. * @return {string}
  290. */
  291. getLongdescText: function () {
  292. var chartOptions = this.chart.options, captionOptions = chartOptions.caption, captionText = captionOptions && captionOptions.text, linkedDescription = this.getLinkedDescription();
  293. return (chartOptions.accessibility.description ||
  294. linkedDescription ||
  295. captionText ||
  296. '');
  297. },
  298. /**
  299. * @private
  300. * @return {string}
  301. */
  302. getTypeDescriptionText: function () {
  303. var chart = this.chart;
  304. return chart.types ?
  305. chart.options.accessibility.typeDescription ||
  306. chart.getTypeDescription(chart.types) : '';
  307. },
  308. /**
  309. * @private
  310. * @param {string} buttonId
  311. * @return {string}
  312. */
  313. getDataTableButtonText: function (buttonId) {
  314. var chart = this.chart, buttonText = chart.langFormat('accessibility.table.viewAsDataTableButtonText', { chart: chart, chartTitle: getChartTitle(chart) });
  315. return '<button id="' + buttonId + '">' + buttonText + '</button>';
  316. },
  317. /**
  318. * @private
  319. * @param {string} buttonId
  320. * @return {string}
  321. */
  322. getSonifyButtonText: function (buttonId) {
  323. var chart = this.chart;
  324. if (chart.options.sonification &&
  325. chart.options.sonification.enabled === false) {
  326. return '';
  327. }
  328. var buttonText = chart.langFormat('accessibility.sonification.playAsSoundButtonText', { chart: chart, chartTitle: getChartTitle(chart) });
  329. return '<button id="' + buttonId + '">' + buttonText + '</button>';
  330. },
  331. /**
  332. * @private
  333. * @return {string}
  334. */
  335. getSubtitleText: function () {
  336. var subtitle = (this.chart.options.subtitle);
  337. return stripHTMLTagsFromString(subtitle && subtitle.text || '');
  338. },
  339. /**
  340. * @private
  341. * @return {string}
  342. */
  343. getEndOfChartMarkerText: function () {
  344. var chart = this.chart, markerText = chart.langFormat('accessibility.screenReaderSection.endOfChartMarker', { chart: chart }), id = 'highcharts-end-of-chart-marker-' + chart.index;
  345. return '<div id="' + id + '">' + markerText + '</div>';
  346. },
  347. /**
  348. * @private
  349. * @param {Highcharts.Dictionary<string>} e
  350. */
  351. onDataTableCreated: function (e) {
  352. var chart = this.chart;
  353. if (chart.options.accessibility.enabled) {
  354. if (this.viewDataTableButton) {
  355. this.viewDataTableButton.setAttribute('aria-expanded', 'true');
  356. }
  357. var attributes = e.tree.attributes || {};
  358. attributes.tabindex = -1;
  359. attributes.summary = getTableSummary(chart);
  360. e.tree.attributes = attributes;
  361. }
  362. },
  363. /**
  364. * @private
  365. */
  366. focusDataTable: function () {
  367. var tableDiv = this.dataTableDiv, table = tableDiv && tableDiv.getElementsByTagName('table')[0];
  368. if (table && table.focus) {
  369. table.focus();
  370. }
  371. },
  372. /**
  373. * @private
  374. * @param {string} sonifyButtonId
  375. */
  376. initSonifyButton: function (sonifyButtonId) {
  377. var _this = this;
  378. var el = this.sonifyButton = getElement(sonifyButtonId);
  379. var chart = this.chart;
  380. var defaultHandler = function (e) {
  381. if (el) {
  382. el.setAttribute('aria-hidden', 'true');
  383. el.setAttribute('aria-label', '');
  384. }
  385. e.preventDefault();
  386. e.stopPropagation();
  387. var announceMsg = chart.langFormat('accessibility.sonification.playAsSoundClickAnnouncement', { chart: chart });
  388. _this.announcer.announce(announceMsg);
  389. setTimeout(function () {
  390. if (el) {
  391. el.removeAttribute('aria-hidden');
  392. el.removeAttribute('aria-label');
  393. }
  394. if (chart.sonify) {
  395. chart.sonify();
  396. }
  397. }, 1000); // Delay to let screen reader speak the button press
  398. };
  399. if (el && chart) {
  400. setElAttrs(el, {
  401. tabindex: -1
  402. });
  403. el.onclick = function (e) {
  404. var onPlayAsSoundClick = (chart.options.accessibility &&
  405. chart.options.accessibility.screenReaderSection.onPlayAsSoundClick);
  406. (onPlayAsSoundClick || defaultHandler).call(this, e, chart);
  407. };
  408. }
  409. },
  410. /**
  411. * Set attribs and handlers for default viewAsDataTable button if exists.
  412. * @private
  413. * @param {string} tableButtonId
  414. */
  415. initDataTableButton: function (tableButtonId) {
  416. var el = this.viewDataTableButton = getElement(tableButtonId), chart = this.chart, tableId = tableButtonId.replace('hc-linkto-', '');
  417. if (el) {
  418. setElAttrs(el, {
  419. tabindex: -1,
  420. 'aria-expanded': !!getElement(tableId)
  421. });
  422. el.onclick = chart.options.accessibility
  423. .screenReaderSection.onViewDataTableClick ||
  424. function () {
  425. chart.viewData();
  426. };
  427. }
  428. },
  429. /**
  430. * Return object with text description of each of the chart's axes.
  431. * @private
  432. * @return {Highcharts.Dictionary<string>}
  433. */
  434. getAxesDescription: function () {
  435. var chart = this.chart, shouldDescribeColl = function (collectionKey, defaultCondition) {
  436. var axes = chart[collectionKey];
  437. return axes.length > 1 || axes[0] &&
  438. pick(axes[0].options.accessibility &&
  439. axes[0].options.accessibility.enabled, defaultCondition);
  440. }, hasNoMap = !!chart.types && chart.types.indexOf('map') < 0, hasCartesian = !!chart.hasCartesianSeries, showXAxes = shouldDescribeColl('xAxis', !chart.angular && hasCartesian && hasNoMap), showYAxes = shouldDescribeColl('yAxis', hasCartesian && hasNoMap), desc = {};
  441. if (showXAxes) {
  442. desc.xAxis = this.getAxisDescriptionText('xAxis');
  443. }
  444. if (showYAxes) {
  445. desc.yAxis = this.getAxisDescriptionText('yAxis');
  446. }
  447. return desc;
  448. },
  449. /**
  450. * @private
  451. * @param {string} collectionKey
  452. * @return {string}
  453. */
  454. getAxisDescriptionText: function (collectionKey) {
  455. var chart = this.chart;
  456. var axes = chart[collectionKey];
  457. return chart.langFormat('accessibility.axis.' + collectionKey + 'Description' + (axes.length > 1 ? 'Plural' : 'Singular'), {
  458. chart: chart,
  459. names: axes.map(function (axis) {
  460. return getAxisDescription(axis);
  461. }),
  462. ranges: axes.map(function (axis) {
  463. return getAxisRangeDescription(axis);
  464. }),
  465. numAxes: axes.length
  466. });
  467. },
  468. /**
  469. * Remove component traces
  470. */
  471. destroy: function () {
  472. if (this.announcer) {
  473. this.announcer.destroy();
  474. }
  475. }
  476. });
  477. export default InfoRegionsComponent;