RangeSelectorComponent.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  1. /* *
  2. *
  3. * (c) 2009-2021 Øystein Moseng
  4. *
  5. * Accessibility component for the range selector.
  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 AccessibilityComponent from '../AccessibilityComponent.js';
  14. import ChartUtilities from '../Utils/ChartUtilities.js';
  15. var unhideChartElementFromAT = ChartUtilities.unhideChartElementFromAT, getAxisRangeDescription = ChartUtilities.getAxisRangeDescription;
  16. import Announcer from '../Utils/Announcer.js';
  17. import H from '../../Core/Globals.js';
  18. import HTMLUtilities from '../Utils/HTMLUtilities.js';
  19. var setElAttrs = HTMLUtilities.setElAttrs;
  20. import KeyboardNavigationHandler from '../KeyboardNavigationHandler.js';
  21. import U from '../../Core/Utilities.js';
  22. import RangeSelector from '../../Extensions/RangeSelector.js';
  23. var addEvent = U.addEvent, extend = U.extend;
  24. /* eslint-disable no-invalid-this, valid-jsdoc */
  25. /**
  26. * @private
  27. */
  28. function shouldRunInputNavigation(chart) {
  29. return Boolean(chart.rangeSelector &&
  30. chart.rangeSelector.inputGroup &&
  31. chart.rangeSelector.inputGroup.element
  32. .getAttribute('visibility') !== 'hidden' &&
  33. chart.options.rangeSelector.inputEnabled !== false &&
  34. chart.rangeSelector.minInput &&
  35. chart.rangeSelector.maxInput);
  36. }
  37. /**
  38. * Highlight range selector button by index.
  39. *
  40. * @private
  41. * @function Highcharts.Chart#highlightRangeSelectorButton
  42. *
  43. * @param {number} ix
  44. *
  45. * @return {boolean}
  46. */
  47. H.Chart.prototype.highlightRangeSelectorButton = function (ix) {
  48. var buttons = (this.rangeSelector &&
  49. this.rangeSelector.buttons ||
  50. []);
  51. var curHighlightedIx = this.highlightedRangeSelectorItemIx;
  52. var curSelectedIx = (this.rangeSelector &&
  53. this.rangeSelector.selected);
  54. // Deselect old
  55. if (typeof curHighlightedIx !== 'undefined' &&
  56. buttons[curHighlightedIx] &&
  57. curHighlightedIx !== curSelectedIx) {
  58. buttons[curHighlightedIx].setState(this.oldRangeSelectorItemState || 0);
  59. }
  60. // Select new
  61. this.highlightedRangeSelectorItemIx = ix;
  62. if (buttons[ix]) {
  63. this.setFocusToElement(buttons[ix].box, buttons[ix].element);
  64. if (ix !== curSelectedIx) {
  65. this.oldRangeSelectorItemState = buttons[ix].state;
  66. buttons[ix].setState(1);
  67. }
  68. return true;
  69. }
  70. return false;
  71. };
  72. // Range selector does not have destroy-setup for class instance events - so
  73. // we set it on the class and call the component from here.
  74. addEvent(RangeSelector, 'afterBtnClick', function () {
  75. if (this.chart.accessibility &&
  76. this.chart.accessibility.components.rangeSelector) {
  77. return this.chart.accessibility.components.rangeSelector.onAfterBtnClick();
  78. }
  79. });
  80. /**
  81. * The RangeSelectorComponent class
  82. *
  83. * @private
  84. * @class
  85. * @name Highcharts.RangeSelectorComponent
  86. */
  87. var RangeSelectorComponent = function () { };
  88. RangeSelectorComponent.prototype = new AccessibilityComponent();
  89. extend(RangeSelectorComponent.prototype, /** @lends Highcharts.RangeSelectorComponent */ {
  90. /**
  91. * Init the component
  92. * @private
  93. */
  94. init: function () {
  95. var chart = this.chart;
  96. this.announcer = new Announcer(chart, 'polite');
  97. },
  98. /**
  99. * Called on first render/updates to the chart, including options changes.
  100. */
  101. onChartUpdate: function () {
  102. var chart = this.chart, component = this, rangeSelector = chart.rangeSelector;
  103. if (!rangeSelector) {
  104. return;
  105. }
  106. this.updateSelectorVisibility();
  107. this.setDropdownAttrs();
  108. if (rangeSelector.buttons &&
  109. rangeSelector.buttons.length) {
  110. rangeSelector.buttons.forEach(function (button) {
  111. component.setRangeButtonAttrs(button);
  112. });
  113. }
  114. // Make sure input boxes are accessible and focusable
  115. if (rangeSelector.maxInput && rangeSelector.minInput) {
  116. ['minInput', 'maxInput'].forEach(function (key, i) {
  117. var input = rangeSelector[key];
  118. if (input) {
  119. unhideChartElementFromAT(chart, input);
  120. component.setRangeInputAttrs(input, 'accessibility.rangeSelector.' + (i ? 'max' : 'min') +
  121. 'InputLabel');
  122. }
  123. });
  124. }
  125. },
  126. /**
  127. * Hide buttons from AT when showing dropdown, and vice versa.
  128. * @private
  129. */
  130. updateSelectorVisibility: function () {
  131. var chart = this.chart;
  132. var rangeSelector = chart.rangeSelector;
  133. var dropdown = (rangeSelector &&
  134. rangeSelector.dropdown);
  135. var buttons = (rangeSelector &&
  136. rangeSelector.buttons ||
  137. []);
  138. var hideFromAT = function (el) { return el.setAttribute('aria-hidden', true); };
  139. if (rangeSelector &&
  140. rangeSelector.hasVisibleDropdown &&
  141. dropdown) {
  142. unhideChartElementFromAT(chart, dropdown);
  143. buttons.forEach(function (btn) { return hideFromAT(btn.element); });
  144. }
  145. else {
  146. if (dropdown) {
  147. hideFromAT(dropdown);
  148. }
  149. buttons.forEach(function (btn) { return unhideChartElementFromAT(chart, btn.element); });
  150. }
  151. },
  152. /**
  153. * Set accessibility related attributes on dropdown element.
  154. * @private
  155. */
  156. setDropdownAttrs: function () {
  157. var chart = this.chart;
  158. var dropdown = (chart.rangeSelector &&
  159. chart.rangeSelector.dropdown);
  160. if (dropdown) {
  161. var label = chart.langFormat('accessibility.rangeSelector.dropdownLabel', { rangeTitle: chart.options.lang.rangeSelectorZoom });
  162. dropdown.setAttribute('aria-label', label);
  163. dropdown.setAttribute('tabindex', -1);
  164. }
  165. },
  166. /**
  167. * @private
  168. * @param {Highcharts.SVGElement} button
  169. */
  170. setRangeButtonAttrs: function (button) {
  171. setElAttrs(button.element, {
  172. tabindex: -1,
  173. role: 'button'
  174. });
  175. },
  176. /**
  177. * @private
  178. */
  179. setRangeInputAttrs: function (input, langKey) {
  180. var chart = this.chart;
  181. setElAttrs(input, {
  182. tabindex: -1,
  183. 'aria-label': chart.langFormat(langKey, { chart: chart })
  184. });
  185. },
  186. /**
  187. * @private
  188. * @param {Highcharts.KeyboardNavigationHandler} keyboardNavigationHandler
  189. * @param {number} keyCode
  190. * @return {number} Response code
  191. */
  192. onButtonNavKbdArrowKey: function (keyboardNavigationHandler, keyCode) {
  193. var response = keyboardNavigationHandler.response, keys = this.keyCodes, chart = this.chart, wrapAround = chart.options.accessibility
  194. .keyboardNavigation.wrapAround, direction = (keyCode === keys.left || keyCode === keys.up) ? -1 : 1, didHighlight = chart.highlightRangeSelectorButton(chart.highlightedRangeSelectorItemIx + direction);
  195. if (!didHighlight) {
  196. if (wrapAround) {
  197. keyboardNavigationHandler.init(direction);
  198. return response.success;
  199. }
  200. return response[direction > 0 ? 'next' : 'prev'];
  201. }
  202. return response.success;
  203. },
  204. /**
  205. * @private
  206. */
  207. onButtonNavKbdClick: function (keyboardNavigationHandler) {
  208. var response = keyboardNavigationHandler.response, chart = this.chart, wasDisabled = chart.oldRangeSelectorItemState === 3;
  209. if (!wasDisabled) {
  210. this.fakeClickEvent(chart.rangeSelector.buttons[chart.highlightedRangeSelectorItemIx].element);
  211. }
  212. return response.success;
  213. },
  214. /**
  215. * Called whenever a range selector button has been clicked, either by
  216. * mouse, touch, or kbd/voice/other.
  217. * @private
  218. */
  219. onAfterBtnClick: function () {
  220. var chart = this.chart;
  221. var axisRangeDescription = getAxisRangeDescription(chart.xAxis[0]);
  222. var announcement = chart.langFormat('accessibility.rangeSelector.clickButtonAnnouncement', { chart: chart, axisRangeDescription: axisRangeDescription });
  223. if (announcement) {
  224. this.announcer.announce(announcement);
  225. }
  226. },
  227. /**
  228. * @private
  229. */
  230. onInputKbdMove: function (direction) {
  231. var chart = this.chart;
  232. var rangeSel = chart.rangeSelector;
  233. var newIx = chart.highlightedInputRangeIx = (chart.highlightedInputRangeIx || 0) + direction;
  234. var newIxOutOfRange = newIx > 1 || newIx < 0;
  235. if (newIxOutOfRange) {
  236. if (chart.accessibility) {
  237. chart.accessibility.keyboardNavigation.tabindexContainer.focus();
  238. chart.accessibility.keyboardNavigation[direction < 0 ? 'prev' : 'next']();
  239. }
  240. }
  241. else if (rangeSel) {
  242. var svgEl = rangeSel[newIx ? 'maxDateBox' : 'minDateBox'];
  243. var inputEl = rangeSel[newIx ? 'maxInput' : 'minInput'];
  244. if (svgEl && inputEl) {
  245. chart.setFocusToElement(svgEl, inputEl);
  246. }
  247. }
  248. },
  249. /**
  250. * @private
  251. * @param {number} direction
  252. */
  253. onInputNavInit: function (direction) {
  254. var _this = this;
  255. var component = this;
  256. var chart = this.chart;
  257. var buttonIxToHighlight = direction > 0 ? 0 : 1;
  258. var rangeSel = chart.rangeSelector;
  259. var svgEl = (rangeSel &&
  260. rangeSel[buttonIxToHighlight ? 'maxDateBox' : 'minDateBox']);
  261. var minInput = (rangeSel && rangeSel.minInput);
  262. var maxInput = (rangeSel && rangeSel.maxInput);
  263. var inputEl = buttonIxToHighlight ? maxInput : minInput;
  264. chart.highlightedInputRangeIx = buttonIxToHighlight;
  265. if (svgEl && minInput && maxInput) {
  266. chart.setFocusToElement(svgEl, inputEl);
  267. // Tab-press with the input focused does not propagate to chart
  268. // automatically, so we manually catch and handle it when relevant.
  269. if (this.removeInputKeydownHandler) {
  270. this.removeInputKeydownHandler();
  271. }
  272. var keydownHandler = function (e) {
  273. var isTab = (e.which || e.keyCode) === _this.keyCodes.tab;
  274. if (isTab) {
  275. e.preventDefault();
  276. e.stopPropagation();
  277. component.onInputKbdMove(e.shiftKey ? -1 : 1);
  278. }
  279. };
  280. var minRemover_1 = addEvent(minInput, 'keydown', keydownHandler);
  281. var maxRemover_1 = addEvent(maxInput, 'keydown', keydownHandler);
  282. this.removeInputKeydownHandler = function () {
  283. minRemover_1();
  284. maxRemover_1();
  285. };
  286. }
  287. },
  288. /**
  289. * @private
  290. */
  291. onInputNavTerminate: function () {
  292. var rangeSel = (this.chart.rangeSelector || {});
  293. if (rangeSel.maxInput) {
  294. rangeSel.hideInput('max');
  295. }
  296. if (rangeSel.minInput) {
  297. rangeSel.hideInput('min');
  298. }
  299. if (this.removeInputKeydownHandler) {
  300. this.removeInputKeydownHandler();
  301. delete this.removeInputKeydownHandler;
  302. }
  303. },
  304. /**
  305. * @private
  306. */
  307. initDropdownNav: function () {
  308. var _this = this;
  309. var chart = this.chart;
  310. var rangeSelector = chart.rangeSelector;
  311. var dropdown = (rangeSelector && rangeSelector.dropdown);
  312. if (rangeSelector && dropdown) {
  313. chart.setFocusToElement(rangeSelector.buttonGroup, dropdown);
  314. if (this.removeDropdownKeydownHandler) {
  315. this.removeDropdownKeydownHandler();
  316. }
  317. // Tab-press with dropdown focused does not propagate to chart
  318. // automatically, so we manually catch and handle it when relevant.
  319. this.removeDropdownKeydownHandler = addEvent(dropdown, 'keydown', function (e) {
  320. var isTab = (e.which || e.keyCode) === _this.keyCodes.tab;
  321. if (isTab) {
  322. e.preventDefault();
  323. e.stopPropagation();
  324. if (chart.accessibility) {
  325. chart.accessibility.keyboardNavigation.tabindexContainer.focus();
  326. chart.accessibility.keyboardNavigation[e.shiftKey ? 'prev' : 'next']();
  327. }
  328. }
  329. });
  330. }
  331. },
  332. /**
  333. * Get navigation for the range selector buttons.
  334. * @private
  335. * @return {Highcharts.KeyboardNavigationHandler} The module object.
  336. */
  337. getRangeSelectorButtonNavigation: function () {
  338. var chart = this.chart;
  339. var keys = this.keyCodes;
  340. var component = this;
  341. return new KeyboardNavigationHandler(chart, {
  342. keyCodeMap: [
  343. [
  344. [keys.left, keys.right, keys.up, keys.down],
  345. function (keyCode) {
  346. return component.onButtonNavKbdArrowKey(this, keyCode);
  347. }
  348. ],
  349. [
  350. [keys.enter, keys.space],
  351. function () {
  352. return component.onButtonNavKbdClick(this);
  353. }
  354. ]
  355. ],
  356. validate: function () {
  357. return !!(chart.rangeSelector &&
  358. chart.rangeSelector.buttons &&
  359. chart.rangeSelector.buttons.length);
  360. },
  361. init: function (direction) {
  362. var rangeSelector = chart.rangeSelector;
  363. if (rangeSelector && rangeSelector.hasVisibleDropdown) {
  364. component.initDropdownNav();
  365. }
  366. else if (rangeSelector) {
  367. var lastButtonIx = rangeSelector.buttons.length - 1;
  368. chart.highlightRangeSelectorButton(direction > 0 ? 0 : lastButtonIx);
  369. }
  370. },
  371. terminate: function () {
  372. if (component.removeDropdownKeydownHandler) {
  373. component.removeDropdownKeydownHandler();
  374. delete component.removeDropdownKeydownHandler;
  375. }
  376. }
  377. });
  378. },
  379. /**
  380. * Get navigation for the range selector input boxes.
  381. * @private
  382. * @return {Highcharts.KeyboardNavigationHandler}
  383. * The module object.
  384. */
  385. getRangeSelectorInputNavigation: function () {
  386. var chart = this.chart;
  387. var component = this;
  388. return new KeyboardNavigationHandler(chart, {
  389. keyCodeMap: [],
  390. validate: function () {
  391. return shouldRunInputNavigation(chart);
  392. },
  393. init: function (direction) {
  394. component.onInputNavInit(direction);
  395. },
  396. terminate: function () {
  397. component.onInputNavTerminate();
  398. }
  399. });
  400. },
  401. /**
  402. * Get keyboard navigation handlers for this component.
  403. * @return {Array<Highcharts.KeyboardNavigationHandler>}
  404. * List of module objects.
  405. */
  406. getKeyboardNavigation: function () {
  407. return [
  408. this.getRangeSelectorButtonNavigation(),
  409. this.getRangeSelectorInputNavigation()
  410. ];
  411. },
  412. /**
  413. * Remove component traces
  414. */
  415. destroy: function () {
  416. if (this.removeDropdownKeydownHandler) {
  417. this.removeDropdownKeydownHandler();
  418. }
  419. if (this.removeInputKeydownHandler) {
  420. this.removeInputKeydownHandler();
  421. }
  422. if (this.announcer) {
  423. this.announcer.destroy();
  424. }
  425. }
  426. });
  427. export default RangeSelectorComponent;