RangeSelectorComponent.js 16 KB

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