Accessibility.js 9.2 KB

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