InfoRegionsComponent.js 19 KB

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