AccessibilityComponent.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  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 (svgElement.hasClass('highcharts-no-tooltip')) {
  211. proxy.className += ' highcharts-no-tooltip';
  212. }
  213. if (preClickEvent) {
  214. this.addEvent(proxy, 'click', preClickEvent);
  215. }
  216. this.setProxyButtonStyle(proxy);
  217. this.updateProxyButtonPosition(proxy, posElement || svgElement);
  218. this.proxyMouseEventsForButton(svgEl, proxy);
  219. // Add to chart div and unhide from screen readers
  220. parentGroup.appendChild(proxy);
  221. if (!attrs['aria-hidden']) {
  222. unhideChartElementFromAT(this.chart, proxy);
  223. }
  224. return proxy;
  225. },
  226. /**
  227. * Get the position relative to chart container for a wrapped SVG element.
  228. * @private
  229. * @param {Highcharts.SVGElement} element
  230. * The element to calculate position for.
  231. * @return {Highcharts.BBoxObject}
  232. * Object with x and y props for the position.
  233. */
  234. getElementPosition: function (element) {
  235. var el = element.element, div = this.chart.renderTo;
  236. if (div && el && el.getBoundingClientRect) {
  237. var rectEl = el.getBoundingClientRect(), rectDiv = div.getBoundingClientRect();
  238. return {
  239. x: rectEl.left - rectDiv.left,
  240. y: rectEl.top - rectDiv.top,
  241. width: rectEl.right - rectEl.left,
  242. height: rectEl.bottom - rectEl.top
  243. };
  244. }
  245. return { x: 0, y: 0, width: 1, height: 1 };
  246. },
  247. /**
  248. * @private
  249. * @param {Highcharts.HTMLElement} button The proxy element.
  250. */
  251. setProxyButtonStyle: function (button) {
  252. merge(true, button.style, {
  253. borderWidth: '0',
  254. backgroundColor: 'transparent',
  255. cursor: 'pointer',
  256. outline: 'none',
  257. opacity: '0.001',
  258. filter: 'alpha(opacity=1)',
  259. zIndex: '999',
  260. overflow: 'hidden',
  261. padding: '0',
  262. margin: '0',
  263. display: 'block',
  264. position: 'absolute'
  265. });
  266. button.style['-ms-filter'] =
  267. 'progid:DXImageTransform.Microsoft.Alpha(Opacity=1)';
  268. },
  269. /**
  270. * @private
  271. * @param {Highcharts.HTMLElement} proxy The proxy to update position of.
  272. * @param {Highcharts.SVGElement} posElement The element to overlay and take position from.
  273. */
  274. updateProxyButtonPosition: function (proxy, posElement) {
  275. var bBox = this.getElementPosition(posElement);
  276. merge(true, proxy.style, {
  277. width: (bBox.width || 1) + 'px',
  278. height: (bBox.height || 1) + 'px',
  279. left: (Math.round(bBox.x) || 0) + 'px',
  280. top: (Math.round(bBox.y) || 0) + 'px'
  281. });
  282. },
  283. /**
  284. * @private
  285. * @param {Highcharts.HTMLElement|Highcharts.HTMLDOMElement|
  286. * Highcharts.SVGDOMElement|Highcharts.SVGElement} source
  287. * @param {Highcharts.HTMLElement} button
  288. */
  289. proxyMouseEventsForButton: function (source, button) {
  290. var component = this;
  291. [
  292. 'click', 'touchstart', 'touchend', 'touchcancel', 'touchmove',
  293. 'mouseover', 'mouseenter', 'mouseleave', 'mouseout'
  294. ].forEach(function (evtType) {
  295. var isTouchEvent = evtType.indexOf('touch') === 0;
  296. component.addEvent(button, evtType, function (e) {
  297. var clonedEvent = isTouchEvent ?
  298. component.cloneTouchEvent(e) :
  299. component.cloneMouseEvent(e);
  300. if (source) {
  301. component.fireEventOnWrappedOrUnwrappedElement(source, clonedEvent);
  302. }
  303. e.stopPropagation();
  304. // #9682, #15318: Touch scrolling didnt work when touching a
  305. // component
  306. if (evtType !== 'touchstart' && evtType !== 'touchmove' && evtType !== 'touchend') {
  307. e.preventDefault();
  308. }
  309. }, { passive: false });
  310. });
  311. },
  312. /**
  313. * Utility function to clone a mouse event for re-dispatching.
  314. * @private
  315. * @param {global.MouseEvent} e The event to clone.
  316. * @return {global.MouseEvent} The cloned event
  317. */
  318. cloneMouseEvent: function (e) {
  319. if (typeof win.MouseEvent === 'function') {
  320. return new win.MouseEvent(e.type, e);
  321. }
  322. // No MouseEvent support, try using initMouseEvent
  323. if (doc.createEvent) {
  324. var evt = doc.createEvent('MouseEvent');
  325. if (evt.initMouseEvent) {
  326. evt.initMouseEvent(e.type, e.bubbles, // #10561, #12161
  327. 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);
  328. return evt;
  329. }
  330. }
  331. return getFakeMouseEvent(e.type);
  332. },
  333. /**
  334. * Utility function to clone a touch event for re-dispatching.
  335. * @private
  336. * @param {global.TouchEvent} e The event to clone.
  337. * @return {global.TouchEvent} The cloned event
  338. */
  339. cloneTouchEvent: function (e) {
  340. var touchListToTouchArray = function (l) {
  341. var touchArray = [];
  342. for (var i = 0; i < l.length; ++i) {
  343. var item = l.item(i);
  344. if (item) {
  345. touchArray.push(item);
  346. }
  347. }
  348. return touchArray;
  349. };
  350. if (typeof win.TouchEvent === 'function') {
  351. var newEvent = new win.TouchEvent(e.type, {
  352. touches: touchListToTouchArray(e.touches),
  353. targetTouches: touchListToTouchArray(e.targetTouches),
  354. changedTouches: touchListToTouchArray(e.changedTouches),
  355. ctrlKey: e.ctrlKey,
  356. shiftKey: e.shiftKey,
  357. altKey: e.altKey,
  358. metaKey: e.metaKey,
  359. bubbles: e.bubbles,
  360. cancelable: e.cancelable,
  361. composed: e.composed,
  362. detail: e.detail,
  363. view: e.view
  364. });
  365. if (e.defaultPrevented) {
  366. newEvent.preventDefault();
  367. }
  368. return newEvent;
  369. }
  370. // Fallback to mouse event
  371. var fakeEvt = this.cloneMouseEvent(e);
  372. fakeEvt.touches = e.touches;
  373. fakeEvt.changedTouches = e.changedTouches;
  374. fakeEvt.targetTouches = e.targetTouches;
  375. return fakeEvt;
  376. },
  377. /**
  378. * Remove traces of the component.
  379. * @private
  380. */
  381. destroyBase: function () {
  382. removeElement(this.chart.a11yProxyContainer);
  383. this.domElementProvider.destroyCreatedElements();
  384. this.eventProvider.removeAddedEvents();
  385. }
  386. };
  387. extend(AccessibilityComponent.prototype, functionsToOverrideByDerivedClasses);
  388. export default AccessibilityComponent;