ZoomComponent.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. /* *
  2. *
  3. * (c) 2009-2020 Øystein Moseng
  4. *
  5. * Accessibility component for chart zoom.
  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 H from '../../Core/Globals.js';
  14. import U from '../../Core/Utilities.js';
  15. var extend = U.extend, pick = U.pick;
  16. import AccessibilityComponent from '../AccessibilityComponent.js';
  17. import KeyboardNavigationHandler from '../KeyboardNavigationHandler.js';
  18. import ChartUtilities from '../Utils/ChartUtilities.js';
  19. var unhideChartElementFromAT = ChartUtilities.unhideChartElementFromAT;
  20. import HTMLUtilities from '../Utils/HTMLUtilities.js';
  21. var setElAttrs = HTMLUtilities.setElAttrs, removeElement = HTMLUtilities.removeElement;
  22. /* eslint-disable no-invalid-this, valid-jsdoc */
  23. /**
  24. * @private
  25. */
  26. function chartHasMapZoom(chart) {
  27. return !!(chart.mapZoom &&
  28. chart.mapNavButtons &&
  29. chart.mapNavButtons.length);
  30. }
  31. /**
  32. * Pan along axis in a direction (1 or -1), optionally with a defined
  33. * granularity (number of steps it takes to walk across current view)
  34. *
  35. * @private
  36. * @function Highcharts.Axis#panStep
  37. *
  38. * @param {number} direction
  39. * @param {number} [granularity]
  40. */
  41. H.Axis.prototype.panStep = function (direction, granularity) {
  42. var gran = granularity || 3, extremes = this.getExtremes(), step = (extremes.max - extremes.min) / gran * direction, newMax = extremes.max + step, newMin = extremes.min + step, size = newMax - newMin;
  43. if (direction < 0 && newMin < extremes.dataMin) {
  44. newMin = extremes.dataMin;
  45. newMax = newMin + size;
  46. }
  47. else if (direction > 0 && newMax > extremes.dataMax) {
  48. newMax = extremes.dataMax;
  49. newMin = newMax - size;
  50. }
  51. this.setExtremes(newMin, newMax);
  52. };
  53. /**
  54. * The ZoomComponent class
  55. *
  56. * @private
  57. * @class
  58. * @name Highcharts.ZoomComponent
  59. */
  60. var ZoomComponent = function () { };
  61. ZoomComponent.prototype = new AccessibilityComponent();
  62. extend(ZoomComponent.prototype, /** @lends Highcharts.ZoomComponent */ {
  63. /**
  64. * Initialize the component
  65. */
  66. init: function () {
  67. var component = this, chart = this.chart;
  68. [
  69. 'afterShowResetZoom', 'afterDrilldown', 'drillupall'
  70. ].forEach(function (eventType) {
  71. component.addEvent(chart, eventType, function () {
  72. component.updateProxyOverlays();
  73. });
  74. });
  75. },
  76. /**
  77. * Called when chart is updated
  78. */
  79. onChartUpdate: function () {
  80. var chart = this.chart, component = this;
  81. // Make map zoom buttons accessible
  82. if (chart.mapNavButtons) {
  83. chart.mapNavButtons.forEach(function (button, i) {
  84. unhideChartElementFromAT(chart, button.element);
  85. component.setMapNavButtonAttrs(button.element, 'accessibility.zoom.mapZoom' + (i ? 'Out' : 'In'));
  86. });
  87. }
  88. },
  89. /**
  90. * @private
  91. * @param {Highcharts.HTMLDOMElement|Highcharts.SVGDOMElement} button
  92. * @param {string} labelFormatKey
  93. */
  94. setMapNavButtonAttrs: function (button, labelFormatKey) {
  95. var chart = this.chart, label = chart.langFormat(labelFormatKey, { chart: chart });
  96. setElAttrs(button, {
  97. tabindex: -1,
  98. role: 'button',
  99. 'aria-label': label
  100. });
  101. },
  102. /**
  103. * Update the proxy overlays on every new render to ensure positions are
  104. * correct.
  105. */
  106. onChartRender: function () {
  107. this.updateProxyOverlays();
  108. },
  109. /**
  110. * Update proxy overlays, recreating the buttons.
  111. */
  112. updateProxyOverlays: function () {
  113. var chart = this.chart;
  114. // Always start with a clean slate
  115. removeElement(this.drillUpProxyGroup);
  116. removeElement(this.resetZoomProxyGroup);
  117. if (chart.resetZoomButton) {
  118. this.recreateProxyButtonAndGroup(chart.resetZoomButton, 'resetZoomProxyButton', 'resetZoomProxyGroup', chart.langFormat('accessibility.zoom.resetZoomButton', { chart: chart }));
  119. }
  120. if (chart.drillUpButton) {
  121. this.recreateProxyButtonAndGroup(chart.drillUpButton, 'drillUpProxyButton', 'drillUpProxyGroup', chart.langFormat('accessibility.drillUpButton', {
  122. chart: chart,
  123. buttonText: chart.getDrilldownBackText()
  124. }));
  125. }
  126. },
  127. /**
  128. * @private
  129. * @param {Highcharts.SVGElement} buttonEl
  130. * @param {string} buttonProp
  131. * @param {string} groupProp
  132. * @param {string} label
  133. */
  134. recreateProxyButtonAndGroup: function (buttonEl, buttonProp, groupProp, label) {
  135. removeElement(this[groupProp]);
  136. this[groupProp] = this.addProxyGroup();
  137. this[buttonProp] = this.createProxyButton(buttonEl, this[groupProp], { 'aria-label': label, tabindex: -1 });
  138. },
  139. /**
  140. * Get keyboard navigation handler for map zoom.
  141. * @private
  142. * @return {Highcharts.KeyboardNavigationHandler} The module object
  143. */
  144. getMapZoomNavigation: function () {
  145. var keys = this.keyCodes, chart = this.chart, component = this;
  146. return new KeyboardNavigationHandler(chart, {
  147. keyCodeMap: [
  148. [
  149. [keys.up, keys.down, keys.left, keys.right],
  150. function (keyCode) {
  151. return component.onMapKbdArrow(this, keyCode);
  152. }
  153. ],
  154. [
  155. [keys.tab],
  156. function (_keyCode, e) {
  157. return component.onMapKbdTab(this, e);
  158. }
  159. ],
  160. [
  161. [keys.space, keys.enter],
  162. function () {
  163. return component.onMapKbdClick(this);
  164. }
  165. ]
  166. ],
  167. validate: function () {
  168. return chartHasMapZoom(chart);
  169. },
  170. init: function (direction) {
  171. return component.onMapNavInit(direction);
  172. }
  173. });
  174. },
  175. /**
  176. * @private
  177. * @param {Highcharts.KeyboardNavigationHandler} keyboardNavigationHandler
  178. * @param {number} keyCode
  179. * @return {number} Response code
  180. */
  181. onMapKbdArrow: function (keyboardNavigationHandler, keyCode) {
  182. var keys = this.keyCodes, panAxis = (keyCode === keys.up || keyCode === keys.down) ?
  183. 'yAxis' : 'xAxis', stepDirection = (keyCode === keys.left || keyCode === keys.up) ?
  184. -1 : 1;
  185. this.chart[panAxis][0].panStep(stepDirection);
  186. return keyboardNavigationHandler.response.success;
  187. },
  188. /**
  189. * @private
  190. * @param {Highcharts.KeyboardNavigationHandler} keyboardNavigationHandler
  191. * @param {global.KeyboardEvent} event
  192. * @return {number} Response code
  193. */
  194. onMapKbdTab: function (keyboardNavigationHandler, event) {
  195. var button, chart = this.chart, response = keyboardNavigationHandler.response, isBackwards = event.shiftKey, isMoveOutOfRange = isBackwards && !this.focusedMapNavButtonIx ||
  196. !isBackwards && this.focusedMapNavButtonIx;
  197. // Deselect old
  198. chart.mapNavButtons[this.focusedMapNavButtonIx].setState(0);
  199. if (isMoveOutOfRange) {
  200. chart.mapZoom(); // Reset zoom
  201. return response[isBackwards ? 'prev' : 'next'];
  202. }
  203. // Select other button
  204. this.focusedMapNavButtonIx += isBackwards ? -1 : 1;
  205. button = chart.mapNavButtons[this.focusedMapNavButtonIx];
  206. chart.setFocusToElement(button.box, button.element);
  207. button.setState(2);
  208. return response.success;
  209. },
  210. /**
  211. * @private
  212. * @param {Highcharts.KeyboardNavigationHandler} keyboardNavigationHandler
  213. * @return {number} Response code
  214. */
  215. onMapKbdClick: function (keyboardNavigationHandler) {
  216. this.fakeClickEvent(this.chart.mapNavButtons[this.focusedMapNavButtonIx]
  217. .element);
  218. return keyboardNavigationHandler.response.success;
  219. },
  220. /**
  221. * @private
  222. * @param {number} direction
  223. */
  224. onMapNavInit: function (direction) {
  225. var chart = this.chart, zoomIn = chart.mapNavButtons[0], zoomOut = chart.mapNavButtons[1], initialButton = direction > 0 ? zoomIn : zoomOut;
  226. chart.setFocusToElement(initialButton.box, initialButton.element);
  227. initialButton.setState(2);
  228. this.focusedMapNavButtonIx = direction > 0 ? 0 : 1;
  229. },
  230. /**
  231. * Get keyboard navigation handler for a simple chart button. Provide the
  232. * button reference for the chart, and a function to call on click.
  233. *
  234. * @private
  235. * @param {string} buttonProp The property on chart referencing the button.
  236. * @return {Highcharts.KeyboardNavigationHandler} The module object
  237. */
  238. simpleButtonNavigation: function (buttonProp, proxyProp, onClick) {
  239. var keys = this.keyCodes, component = this, chart = this.chart;
  240. return new KeyboardNavigationHandler(chart, {
  241. keyCodeMap: [
  242. [
  243. [keys.tab, keys.up, keys.down, keys.left, keys.right],
  244. function (keyCode, e) {
  245. var isBackwards = keyCode === keys.tab && e.shiftKey ||
  246. keyCode === keys.left || keyCode === keys.up;
  247. // Arrow/tab => just move
  248. return this.response[isBackwards ? 'prev' : 'next'];
  249. }
  250. ],
  251. [
  252. [keys.space, keys.enter],
  253. function () {
  254. var res = onClick(this, chart);
  255. return pick(res, this.response.success);
  256. }
  257. ]
  258. ],
  259. validate: function () {
  260. var hasButton = (chart[buttonProp] &&
  261. chart[buttonProp].box &&
  262. component[proxyProp]);
  263. return hasButton;
  264. },
  265. init: function () {
  266. chart.setFocusToElement(chart[buttonProp].box, component[proxyProp]);
  267. }
  268. });
  269. },
  270. /**
  271. * Get keyboard navigation handlers for this component.
  272. * @return {Array<Highcharts.KeyboardNavigationHandler>}
  273. * List of module objects
  274. */
  275. getKeyboardNavigation: function () {
  276. return [
  277. this.simpleButtonNavigation('resetZoomButton', 'resetZoomProxyButton', function (_handler, chart) {
  278. chart.zoomOut();
  279. }),
  280. this.simpleButtonNavigation('drillUpButton', 'drillUpProxyButton', function (handler, chart) {
  281. chart.drillUp();
  282. return handler.response.prev;
  283. }),
  284. this.getMapZoomNavigation()
  285. ];
  286. }
  287. });
  288. export default ZoomComponent;