Accessibility.js 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. /* *
  2. *
  3. * (c) 2009-2021 Øystein Moseng
  4. *
  5. * Accessibility module for Highcharts
  6. *
  7. * License: www.highcharts.com/license
  8. *
  9. * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
  10. *
  11. * */
  12. 'use strict';
  13. import Chart from '../Core/Chart/Chart.js';
  14. import ChartUtilities from './Utils/ChartUtilities.js';
  15. import H from '../Core/Globals.js';
  16. var doc = H.doc;
  17. import KeyboardNavigationHandler from './KeyboardNavigationHandler.js';
  18. import D from '../Core/DefaultOptions.js';
  19. var defaultOptions = D.defaultOptions;
  20. import Point from '../Core/Series/Point.js';
  21. import Series from '../Core/Series/Series.js';
  22. import U from '../Core/Utilities.js';
  23. var addEvent = U.addEvent, extend = U.extend, fireEvent = U.fireEvent, merge = U.merge;
  24. import AccessibilityComponent from './AccessibilityComponent.js';
  25. import KeyboardNavigation from './KeyboardNavigation.js';
  26. import LegendComponent from './Components/LegendComponent.js';
  27. import MenuComponent from './Components/MenuComponent.js';
  28. import SeriesComponent from './Components/SeriesComponent/SeriesComponent.js';
  29. import ZoomComponent from './Components/ZoomComponent.js';
  30. import RangeSelectorComponent from './Components/RangeSelectorComponent.js';
  31. import InfoRegionsComponent from './Components/InfoRegionsComponent.js';
  32. import ContainerComponent from './Components/ContainerComponent.js';
  33. import whcm from './HighContrastMode.js';
  34. import highContrastTheme from './HighContrastTheme.js';
  35. import defaultOptionsA11Y from './Options/Options.js';
  36. import defaultLangOptions from './Options/LangOptions.js';
  37. import copyDeprecatedOptions from './Options/DeprecatedOptions.js';
  38. import HTMLUtilities from './Utils/HTMLUtilities.js';
  39. import './A11yI18n.js';
  40. import './FocusBorder.js';
  41. // Add default options
  42. merge(true, defaultOptions, defaultOptionsA11Y, {
  43. accessibility: {
  44. highContrastTheme: highContrastTheme
  45. },
  46. lang: defaultLangOptions
  47. });
  48. // Expose functionality on Highcharts namespace
  49. H.A11yChartUtilities = ChartUtilities;
  50. H.A11yHTMLUtilities = HTMLUtilities;
  51. H.KeyboardNavigationHandler = KeyboardNavigationHandler;
  52. H.AccessibilityComponent = AccessibilityComponent;
  53. /* eslint-disable no-invalid-this, valid-jsdoc */
  54. /**
  55. * The Accessibility class
  56. *
  57. * @private
  58. * @requires module:modules/accessibility
  59. *
  60. * @class
  61. * @name Highcharts.Accessibility
  62. *
  63. * @param {Highcharts.Chart} chart
  64. * Chart object
  65. */
  66. function Accessibility(chart) {
  67. this.init(chart);
  68. }
  69. Accessibility.prototype = {
  70. /**
  71. * Initialize the accessibility class
  72. * @private
  73. * @param {Highcharts.Chart} chart
  74. * Chart object
  75. */
  76. init: function (chart) {
  77. this.chart = chart;
  78. // Abort on old browsers
  79. if (!doc.addEventListener || !chart.renderer.isSVG) {
  80. chart.renderTo.setAttribute('aria-hidden', true);
  81. return;
  82. }
  83. // Copy over any deprecated options that are used. We could do this on
  84. // every update, but it is probably not needed.
  85. copyDeprecatedOptions(chart);
  86. this.initComponents();
  87. this.keyboardNavigation = new KeyboardNavigation(chart, this.components);
  88. this.update();
  89. },
  90. /**
  91. * @private
  92. */
  93. initComponents: function () {
  94. var chart = this.chart, a11yOptions = chart.options.accessibility;
  95. this.components = {
  96. container: new ContainerComponent(),
  97. infoRegions: new InfoRegionsComponent(),
  98. legend: new LegendComponent(),
  99. chartMenu: new MenuComponent(),
  100. rangeSelector: new RangeSelectorComponent(),
  101. series: new SeriesComponent(),
  102. zoom: new ZoomComponent()
  103. };
  104. if (a11yOptions.customComponents) {
  105. extend(this.components, a11yOptions.customComponents);
  106. }
  107. var components = this.components;
  108. this.getComponentOrder().forEach(function (componentName) {
  109. components[componentName].initBase(chart);
  110. components[componentName].init();
  111. });
  112. },
  113. /**
  114. * Get order to update components in.
  115. * @private
  116. */
  117. getComponentOrder: function () {
  118. if (!this.components) {
  119. return []; // For zombie accessibility object on old browsers
  120. }
  121. if (!this.components.series) {
  122. return Object.keys(this.components);
  123. }
  124. var componentsExceptSeries = Object.keys(this.components)
  125. .filter(function (c) { return c !== 'series'; });
  126. // Update series first, so that other components can read accessibility
  127. // info on points.
  128. return ['series'].concat(componentsExceptSeries);
  129. },
  130. /**
  131. * Update all components.
  132. */
  133. update: function () {
  134. var components = this.components, chart = this.chart, a11yOptions = chart.options.accessibility;
  135. fireEvent(chart, 'beforeA11yUpdate');
  136. // Update the chart type list as this is used by multiple modules
  137. chart.types = this.getChartTypes();
  138. // Update markup
  139. this.getComponentOrder().forEach(function (componentName) {
  140. components[componentName].onChartUpdate();
  141. fireEvent(chart, 'afterA11yComponentUpdate', {
  142. name: componentName,
  143. component: components[componentName]
  144. });
  145. });
  146. // Update keyboard navigation
  147. this.keyboardNavigation.update(a11yOptions.keyboardNavigation.order);
  148. // Handle high contrast mode
  149. if (!chart.highContrastModeActive && // Only do this once
  150. whcm.isHighContrastModeActive()) {
  151. whcm.setHighContrastTheme(chart);
  152. }
  153. fireEvent(chart, 'afterA11yUpdate', {
  154. accessibility: this
  155. });
  156. },
  157. /**
  158. * Destroy all elements.
  159. */
  160. destroy: function () {
  161. var chart = this.chart || {};
  162. // Destroy components
  163. var components = this.components;
  164. Object.keys(components).forEach(function (componentName) {
  165. components[componentName].destroy();
  166. components[componentName].destroyBase();
  167. });
  168. // Kill keyboard nav
  169. if (this.keyboardNavigation) {
  170. this.keyboardNavigation.destroy();
  171. }
  172. // Hide container from screen readers if it exists
  173. if (chart.renderTo) {
  174. chart.renderTo.setAttribute('aria-hidden', true);
  175. }
  176. // Remove focus border if it exists
  177. if (chart.focusElement) {
  178. chart.focusElement.removeFocusBorder();
  179. }
  180. },
  181. /**
  182. * Return a list of the types of series we have in the chart.
  183. * @private
  184. */
  185. getChartTypes: function () {
  186. var types = {};
  187. this.chart.series.forEach(function (series) {
  188. types[series.type] = 1;
  189. });
  190. return Object.keys(types);
  191. }
  192. };
  193. /**
  194. * @private
  195. */
  196. Chart.prototype.updateA11yEnabled = function () {
  197. var a11y = this.accessibility, accessibilityOptions = this.options.accessibility;
  198. if (accessibilityOptions && accessibilityOptions.enabled) {
  199. if (a11y) {
  200. a11y.update();
  201. }
  202. else {
  203. this.accessibility = a11y = new Accessibility(this);
  204. }
  205. }
  206. else if (a11y) {
  207. // Destroy if after update we have a11y and it is disabled
  208. if (a11y.destroy) {
  209. a11y.destroy();
  210. }
  211. delete this.accessibility;
  212. }
  213. else {
  214. // Just hide container
  215. this.renderTo.setAttribute('aria-hidden', true);
  216. }
  217. };
  218. // Handle updates to the module and send render updates to components
  219. addEvent(Chart, 'render', function (e) {
  220. // Update/destroy
  221. if (this.a11yDirty && this.renderTo) {
  222. delete this.a11yDirty;
  223. this.updateA11yEnabled();
  224. }
  225. var a11y = this.accessibility;
  226. if (a11y) {
  227. a11y.getComponentOrder().forEach(function (componentName) {
  228. a11y.components[componentName].onChartRender();
  229. });
  230. }
  231. });
  232. // Update with chart/series/point updates
  233. addEvent(Chart, 'update', function (e) {
  234. // Merge new options
  235. var newOptions = e.options.accessibility;
  236. if (newOptions) {
  237. // Handle custom component updating specifically
  238. if (newOptions.customComponents) {
  239. this.options.accessibility.customComponents =
  240. newOptions.customComponents;
  241. delete newOptions.customComponents;
  242. }
  243. merge(true, this.options.accessibility, newOptions);
  244. // Recreate from scratch
  245. if (this.accessibility && this.accessibility.destroy) {
  246. this.accessibility.destroy();
  247. delete this.accessibility;
  248. }
  249. }
  250. // Mark dirty for update
  251. this.a11yDirty = true;
  252. });
  253. // Mark dirty for update
  254. addEvent(Point, 'update', function () {
  255. if (this.series.chart.accessibility) {
  256. this.series.chart.a11yDirty = true;
  257. }
  258. });
  259. ['addSeries', 'init'].forEach(function (event) {
  260. addEvent(Chart, event, function () {
  261. this.a11yDirty = true;
  262. });
  263. });
  264. ['update', 'updatedData', 'remove'].forEach(function (event) {
  265. addEvent(Series, event, function () {
  266. if (this.chart.accessibility) {
  267. this.chart.a11yDirty = true;
  268. }
  269. });
  270. });
  271. // Direct updates (events happen after render)
  272. [
  273. 'afterDrilldown', 'drillupall'
  274. ].forEach(function (event) {
  275. addEvent(Chart, event, function () {
  276. if (this.accessibility) {
  277. this.accessibility.update();
  278. }
  279. });
  280. });
  281. // Destroy with chart
  282. addEvent(Chart, 'destroy', function () {
  283. if (this.accessibility) {
  284. this.accessibility.destroy();
  285. }
  286. });