accessibility.js 8.5 KB

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