Accessibility.js 9.4 KB

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