AccessibilityComponent.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. /* *
  2. *
  3. * (c) 2009-2021 Øystein Moseng
  4. *
  5. * Accessibility component class definition
  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. var unhideChartElementFromAT = ChartUtilities.unhideChartElementFromAT;
  15. import DOMElementProvider from './Utils/DOMElementProvider.js';
  16. import EventProvider from './Utils/EventProvider.js';
  17. import H from '../Core/Globals.js';
  18. var doc = H.doc, win = H.win;
  19. import HTMLUtilities from './Utils/HTMLUtilities.js';
  20. var removeElement = HTMLUtilities.removeElement, getFakeMouseEvent = HTMLUtilities.getFakeMouseEvent;
  21. import U from '../Core/Utilities.js';
  22. var extend = U.extend, fireEvent = U.fireEvent, merge = U.merge;
  23. /* eslint-disable valid-jsdoc */
  24. /** @lends Highcharts.AccessibilityComponent */
  25. var functionsToOverrideByDerivedClasses = {
  26. /**
  27. * Called on component initialization.
  28. */
  29. init: function () { },
  30. /**
  31. * Get keyboard navigation handler for this component.
  32. * @return {Highcharts.KeyboardNavigationHandler}
  33. */
  34. getKeyboardNavigation: function () { },
  35. /**
  36. * Called on updates to the chart, including options changes.
  37. * Note that this is also called on first render of chart.
  38. */
  39. onChartUpdate: function () { },
  40. /**
  41. * Called on every chart render.
  42. */
  43. onChartRender: function () { },
  44. /**
  45. * Called when accessibility is disabled or chart is destroyed.
  46. */
  47. destroy: function () { }
  48. };
  49. /**
  50. * The AccessibilityComponent base class, representing a part of the chart that
  51. * has accessibility logic connected to it. This class can be inherited from to
  52. * create a custom accessibility component for a chart.
  53. *
  54. * Components should take care to destroy added elements and unregister event
  55. * handlers on destroy. This is handled automatically if using this.addEvent and
  56. * this.createElement.
  57. *
  58. * @sample highcharts/accessibility/custom-component
  59. * Custom accessibility component
  60. *
  61. * @requires module:modules/accessibility
  62. * @class
  63. * @name Highcharts.AccessibilityComponent
  64. */
  65. function AccessibilityComponent() { }
  66. /**
  67. * @lends Highcharts.AccessibilityComponent
  68. */
  69. AccessibilityComponent.prototype = {
  70. /**
  71. * Initialize the class
  72. * @private
  73. * @param {Highcharts.Chart} chart
  74. * Chart object
  75. */
  76. initBase: function (chart) {
  77. this.chart = chart;
  78. this.eventProvider = new EventProvider();
  79. this.domElementProvider = new DOMElementProvider();
  80. // Key code enum for common keys
  81. this.keyCodes = {
  82. left: 37,
  83. right: 39,
  84. up: 38,
  85. down: 40,
  86. enter: 13,
  87. space: 32,
  88. esc: 27,
  89. tab: 9
  90. };
  91. },
  92. /**
  93. * Add an event to an element and keep track of it for later removal.
  94. * See EventProvider for details.
  95. * @private
  96. */
  97. addEvent: function () {
  98. return this.eventProvider.addEvent
  99. .apply(this.eventProvider, arguments);
  100. },
  101. /**
  102. * Create an element and keep track of it for later removal.
  103. * See DOMElementProvider for details.
  104. * @private
  105. */
  106. createElement: function () {
  107. return this.domElementProvider.createElement.apply(this.domElementProvider, arguments);
  108. },
  109. /**
  110. * Fire an event on an element that is either wrapped by Highcharts,
  111. * or a DOM element
  112. * @private
  113. * @param {Highcharts.HTMLElement|Highcharts.HTMLDOMElement|
  114. * Highcharts.SVGDOMElement|Highcharts.SVGElement} el
  115. * @param {Event} eventObject
  116. */
  117. fireEventOnWrappedOrUnwrappedElement: function (el, eventObject) {
  118. var type = eventObject.type;
  119. if (doc.createEvent && (el.dispatchEvent || el.fireEvent)) {
  120. if (el.dispatchEvent) {
  121. el.dispatchEvent(eventObject);
  122. }
  123. else {
  124. el.fireEvent(type, eventObject);
  125. }
  126. }
  127. else {
  128. fireEvent(el, type, eventObject);
  129. }
  130. },
  131. /**
  132. * Utility function to attempt to fake a click event on an element.
  133. * @private
  134. * @param {Highcharts.HTMLDOMElement|Highcharts.SVGDOMElement} element
  135. */
  136. fakeClickEvent: function (element) {
  137. if (element) {
  138. var fakeEventObject = getFakeMouseEvent('click');
  139. this.fireEventOnWrappedOrUnwrappedElement(element, fakeEventObject);
  140. }
  141. },
  142. /**
  143. * Add a new proxy group to the proxy container. Creates the proxy container
  144. * if it does not exist.
  145. * @private
  146. * @param {Highcharts.HTMLAttributes} [attrs]
  147. * The attributes to set on the new group div.
  148. * @return {Highcharts.HTMLDOMElement}
  149. * The new proxy group element.
  150. */
  151. addProxyGroup: function (attrs) {
  152. this.createOrUpdateProxyContainer();
  153. var groupDiv = this.createElement('div');
  154. Object.keys(attrs || {}).forEach(function (prop) {
  155. if (attrs[prop] !== null) {
  156. groupDiv.setAttribute(prop, attrs[prop]);
  157. }
  158. });
  159. this.chart.a11yProxyContainer.appendChild(groupDiv);
  160. return groupDiv;
  161. },
  162. /**
  163. * Creates and updates DOM position of proxy container
  164. * @private
  165. */
  166. createOrUpdateProxyContainer: function () {
  167. var chart = this.chart, rendererSVGEl = chart.renderer.box;
  168. chart.a11yProxyContainer = chart.a11yProxyContainer ||
  169. this.createProxyContainerElement();
  170. if (rendererSVGEl.nextSibling !== chart.a11yProxyContainer) {
  171. chart.container.insertBefore(chart.a11yProxyContainer, rendererSVGEl.nextSibling);
  172. }
  173. },
  174. /**
  175. * @private
  176. * @return {Highcharts.HTMLDOMElement} element
  177. */
  178. createProxyContainerElement: function () {
  179. var pc = doc.createElement('div');
  180. pc.className = 'highcharts-a11y-proxy-container';
  181. return pc;
  182. },
  183. /**
  184. * Create an invisible proxy HTML button in the same position as an SVG
  185. * element
  186. * @private
  187. * @param {Highcharts.SVGElement} svgElement
  188. * The wrapped svg el to proxy.
  189. * @param {Highcharts.HTMLDOMElement} parentGroup
  190. * The proxy group element in the proxy container to add this button to.
  191. * @param {Highcharts.SVGAttributes} [attributes]
  192. * Additional attributes to set.
  193. * @param {Highcharts.SVGElement} [posElement]
  194. * Element to use for positioning instead of svgElement.
  195. * @param {Function} [preClickEvent]
  196. * Function to call before click event fires.
  197. *
  198. * @return {Highcharts.HTMLDOMElement} The proxy button.
  199. */
  200. createProxyButton: function (svgElement, parentGroup, attributes, posElement, preClickEvent) {
  201. var svgEl = svgElement.element, proxy = this.createElement('button'), attrs = merge({
  202. 'aria-label': svgEl.getAttribute('aria-label')
  203. }, attributes);
  204. Object.keys(attrs).forEach(function (prop) {
  205. if (attrs[prop] !== null) {
  206. proxy.setAttribute(prop, attrs[prop]);
  207. }
  208. });
  209. proxy.className = 'highcharts-a11y-proxy-button';
  210. if (preClickEvent) {
  211. this.addEvent(proxy, 'click', preClickEvent);
  212. }
  213. this.setProxyButtonStyle(proxy);
  214. this.updateProxyButtonPosition(proxy, posElement || svgElement);
  215. this.proxyMouseEventsForButton(svgEl, proxy);
  216. // Add to chart div and unhide from screen readers
  217. parentGroup.appendChild(proxy);
  218. if (!attrs['aria-hidden']) {
  219. unhideChartElementFromAT(this.chart, proxy);
  220. }
  221. return proxy;
  222. },
  223. /**
  224. * Get the position relative to chart container for a wrapped SVG element.
  225. * @private
  226. * @param {Highcharts.SVGElement} element
  227. * The element to calculate position for.
  228. * @return {Highcharts.BBoxObject}
  229. * Object with x and y props for the position.
  230. */
  231. getElementPosition: function (element) {
  232. var el = element.element, div = this.chart.renderTo;
  233. if (div && el && el.getBoundingClientRect) {
  234. var rectEl = el.getBoundingClientRect(), rectDiv = div.getBoundingClientRect();
  235. return {
  236. x: rectEl.left - rectDiv.left,
  237. y: rectEl.top - rectDiv.top,
  238. width: rectEl.right - rectEl.left,
  239. height: rectEl.bottom - rectEl.top
  240. };
  241. }
  242. return { x: 0, y: 0, width: 1, height: 1 };
  243. },
  244. /**
  245. * @private
  246. * @param {Highcharts.HTMLElement} button The proxy element.
  247. */
  248. setProxyButtonStyle: function (button) {
  249. merge(true, button.style, {
  250. borderWidth: '0',
  251. backgroundColor: 'transparent',
  252. cursor: 'pointer',
  253. outline: 'none',
  254. opacity: '0.001',
  255. filter: 'alpha(opacity=1)',
  256. zIndex: '999',
  257. overflow: 'hidden',
  258. padding: '0',
  259. margin: '0',
  260. display: 'block',
  261. position: 'absolute'
  262. });
  263. button.style['-ms-filter'] =
  264. 'progid:DXImageTransform.Microsoft.Alpha(Opacity=1)';
  265. },
  266. /**
  267. * @private
  268. * @param {Highcharts.HTMLElement} proxy The proxy to update position of.
  269. * @param {Highcharts.SVGElement} posElement The element to overlay and take position from.
  270. */
  271. updateProxyButtonPosition: function (proxy, posElement) {
  272. var bBox = this.getElementPosition(posElement);
  273. merge(true, proxy.style, {
  274. width: (bBox.width || 1) + 'px',
  275. height: (bBox.height || 1) + 'px',
  276. left: (bBox.x || 0) + 'px',
  277. top: (bBox.y || 0) + 'px'
  278. });
  279. },
  280. /**
  281. * @private
  282. * @param {Highcharts.HTMLElement|Highcharts.HTMLDOMElement|
  283. * Highcharts.SVGDOMElement|Highcharts.SVGElement} source
  284. * @param {Highcharts.HTMLElement} button
  285. */
  286. proxyMouseEventsForButton: function (source, button) {
  287. var component = this;
  288. [
  289. 'click', 'touchstart', 'touchend', 'touchcancel', 'touchmove',
  290. 'mouseover', 'mouseenter', 'mouseleave', 'mouseout'
  291. ].forEach(function (evtType) {
  292. var isTouchEvent = evtType.indexOf('touch') === 0;
  293. component.addEvent(button, evtType, function (e) {
  294. var clonedEvent = isTouchEvent ?
  295. component.cloneTouchEvent(e) :
  296. component.cloneMouseEvent(e);
  297. if (source) {
  298. component.fireEventOnWrappedOrUnwrappedElement(source, clonedEvent);
  299. }
  300. e.stopPropagation();
  301. // #9682, #15318: Touch scrolling didnt work when touching a
  302. // component
  303. if (evtType !== 'touchstart' && evtType !== 'touchmove' && evtType !== 'touchend') {
  304. e.preventDefault();
  305. }
  306. }, { passive: false });
  307. });
  308. },
  309. /**
  310. * Utility function to clone a mouse event for re-dispatching.
  311. * @private
  312. * @param {global.MouseEvent} e The event to clone.
  313. * @return {global.MouseEvent} The cloned event
  314. */
  315. cloneMouseEvent: function (e) {
  316. if (typeof win.MouseEvent === 'function') {
  317. return new win.MouseEvent(e.type, e);
  318. }
  319. // No MouseEvent support, try using initMouseEvent
  320. if (doc.createEvent) {
  321. var evt = doc.createEvent('MouseEvent');
  322. if (evt.initMouseEvent) {
  323. evt.initMouseEvent(e.type, e.bubbles, // #10561, #12161
  324. e.cancelable, e.view || win, e.detail, e.screenX, e.screenY, e.clientX, e.clientY, e.ctrlKey, e.altKey, e.shiftKey, e.metaKey, e.button, e.relatedTarget);
  325. return evt;
  326. }
  327. }
  328. return getFakeMouseEvent(e.type);
  329. },
  330. /**
  331. * Utility function to clone a touch event for re-dispatching.
  332. * @private
  333. * @param {global.TouchEvent} e The event to clone.
  334. * @return {global.TouchEvent} The cloned event
  335. */
  336. cloneTouchEvent: function (e) {
  337. var touchListToTouchArray = function (l) {
  338. var touchArray = [];
  339. for (var i = 0; i < l.length; ++i) {
  340. var item = l.item(i);
  341. if (item) {
  342. touchArray.push(item);
  343. }
  344. }
  345. return touchArray;
  346. };
  347. if (typeof win.TouchEvent === 'function') {
  348. var newEvent = new win.TouchEvent(e.type, {
  349. touches: touchListToTouchArray(e.touches),
  350. targetTouches: touchListToTouchArray(e.targetTouches),
  351. changedTouches: touchListToTouchArray(e.changedTouches),
  352. ctrlKey: e.ctrlKey,
  353. shiftKey: e.shiftKey,
  354. altKey: e.altKey,
  355. metaKey: e.metaKey,
  356. bubbles: e.bubbles,
  357. cancelable: e.cancelable,
  358. composed: e.composed,
  359. detail: e.detail,
  360. view: e.view
  361. });
  362. if (e.defaultPrevented) {
  363. newEvent.preventDefault();
  364. }
  365. return newEvent;
  366. }
  367. // Fallback to mouse event
  368. var fakeEvt = this.cloneMouseEvent(e);
  369. fakeEvt.touches = e.touches;
  370. fakeEvt.changedTouches = e.changedTouches;
  371. fakeEvt.targetTouches = e.targetTouches;
  372. return fakeEvt;
  373. },
  374. /**
  375. * Remove traces of the component.
  376. * @private
  377. */
  378. destroyBase: function () {
  379. removeElement(this.chart.a11yProxyContainer);
  380. this.domElementProvider.destroyCreatedElements();
  381. this.eventProvider.removeAddedEvents();
  382. }
  383. };
  384. extend(AccessibilityComponent.prototype, functionsToOverrideByDerivedClasses);
  385. export default AccessibilityComponent;