UNPKG

highcharts

Version:
1,233 lines (1,227 loc) 342 kB
/** * @license Highcharts JS v8.2.0 (2020-08-20) * * Accessibility module * * (c) 2010-2019 Highsoft AS * Author: Oystein Moseng * * License: www.highcharts.com/license */ 'use strict'; (function (factory) { if (typeof module === 'object' && module.exports) { factory['default'] = factory; module.exports = factory; } else if (typeof define === 'function' && define.amd) { define('highcharts/modules/accessibility', ['highcharts'], function (Highcharts) { factory(Highcharts); factory.Highcharts = Highcharts; return factory; }); } else { factory(typeof Highcharts !== 'undefined' ? Highcharts : undefined); } }(function (Highcharts) { var _modules = Highcharts ? Highcharts._modules : {}; function _registerModule(obj, path, args, fn) { if (!obj.hasOwnProperty(path)) { obj[path] = fn.apply(null, args); } } _registerModule(_modules, 'Accessibility/Utils/HTMLUtilities.js', [_modules['Core/Utilities.js'], _modules['Core/Globals.js']], function (U, H) { /* * * * (c) 2009-2020 Øystein Moseng * * Utility functions for accessibility module. * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ var merge = U.merge; var win = H.win, doc = win.document; /* eslint-disable valid-jsdoc */ /** * @private * @param {Highcharts.HTMLDOMElement} el * @param {string} className * @return {void} */ function addClass(el, className) { if (el.classList) { el.classList.add(className); } else if (el.className.indexOf(className) < 0) { // Note: Dumb check for class name exists, should be fine for practical // use cases, but will return false positives if the element has a class // that contains the className. el.className += className; } } /** * @private * @param {string} str * @return {string} */ function escapeStringForHTML(str) { return str .replace(/&/g, '&amp;') .replace(/</g, '&lt;') .replace(/>/g, '&gt;') .replace(/"/g, '&quot;') .replace(/'/g, '&#x27;') .replace(/\//g, '&#x2F;'); } /** * Get an element by ID * @param {string} id * @private * @return {Highcharts.HTMLDOMElement|Highcharts.SVGDOMElement|null} */ function getElement(id) { return doc.getElementById(id); } /** * Get a fake mouse event of a given type * @param {string} type * @private * @return {global.MouseEvent} */ function getFakeMouseEvent(type) { if (typeof win.MouseEvent === 'function') { return new win.MouseEvent(type); } // No MouseEvent support, try using initMouseEvent if (doc.createEvent) { var evt = doc.createEvent('MouseEvent'); if (evt.initMouseEvent) { evt.initMouseEvent(type, true, // Bubble true, // Cancel win, // View type === 'click' ? 1 : 0, // Detail // Coords 0, 0, 0, 0, // Pressed keys false, false, false, false, 0, // button null // related target ); return evt; } } return { type: type }; } /** * Remove an element from the DOM. * @private * @param {Highcharts.HTMLDOMElement|Highcharts.SVGDOMElement} [element] * @return {void} */ function removeElement(element) { if (element && element.parentNode) { element.parentNode.removeChild(element); } } /** * Utility function. Reverses child nodes of a DOM element. * @private * @param {Highcharts.HTMLDOMElement|Highcharts.SVGDOMElement} node * @return {void} */ function reverseChildNodes(node) { var i = node.childNodes.length; while (i--) { node.appendChild(node.childNodes[i]); } } /** * Set attributes on element. Set to null to remove attribute. * @private * @param {Highcharts.HTMLDOMElement|Highcharts.SVGDOMElement} el * @param {Highcharts.HTMLAttributes|Highcharts.SVGAttributes} attrs * @return {void} */ function setElAttrs(el, attrs) { Object.keys(attrs).forEach(function (attr) { var val = attrs[attr]; if (val === null) { el.removeAttribute(attr); } else { var cleanedVal = escapeStringForHTML('' + val); el.setAttribute(attr, cleanedVal); } }); } /** * Used for aria-label attributes, painting on a canvas will fail if the * text contains tags. * @private * @param {string} str * @return {string} */ function stripHTMLTagsFromString(str) { return typeof str === 'string' ? str.replace(/<\/?[^>]+(>|$)/g, '') : str; } /** * Utility function for hiding an element visually, but still keeping it * available to screen reader users. * @private * @param {Highcharts.HTMLDOMElement} element * @return {void} */ function visuallyHideElement(element) { var hiddenStyle = { position: 'absolute', width: '1px', height: '1px', overflow: 'hidden', whiteSpace: 'nowrap', clip: 'rect(1px, 1px, 1px, 1px)', marginTop: '-3px', '-ms-filter': 'progid:DXImageTransform.Microsoft.Alpha(Opacity=1)', filter: 'alpha(opacity=1)', opacity: '0.01' }; merge(true, element.style, hiddenStyle); } var HTMLUtilities = { addClass: addClass, escapeStringForHTML: escapeStringForHTML, getElement: getElement, getFakeMouseEvent: getFakeMouseEvent, removeElement: removeElement, reverseChildNodes: reverseChildNodes, setElAttrs: setElAttrs, stripHTMLTagsFromString: stripHTMLTagsFromString, visuallyHideElement: visuallyHideElement }; return HTMLUtilities; }); _registerModule(_modules, 'Accessibility/Utils/ChartUtilities.js', [_modules['Accessibility/Utils/HTMLUtilities.js'], _modules['Core/Utilities.js']], function (HTMLUtilities, U) { /* * * * (c) 2009-2020 Øystein Moseng * * Utils for dealing with charts. * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ var stripHTMLTags = HTMLUtilities.stripHTMLTagsFromString; var defined = U.defined, find = U.find, fireEvent = U.fireEvent; /* eslint-disable valid-jsdoc */ /** * @return {string} */ function getChartTitle(chart) { return stripHTMLTags(chart.options.title.text || chart.langFormat('accessibility.defaultChartTitle', { chart: chart })); } /** * @param {Highcharts.Axis} axis * @return {string} */ function getAxisDescription(axis) { return stripHTMLTags(axis && (axis.userOptions && axis.userOptions.accessibility && axis.userOptions.accessibility.description || axis.axisTitle && axis.axisTitle.textStr || axis.options.id || axis.categories && 'categories' || axis.dateTime && 'Time' || 'values')); } /** * Get the DOM element for the first point in the series. * @private * @param {Highcharts.Series} series * The series to get element for. * @return {Highcharts.HTMLDOMElement|Highcharts.SVGDOMElement|undefined} * The DOM element for the point. */ function getSeriesFirstPointElement(series) { if (series.points && series.points.length && series.points[0].graphic) { return series.points[0].graphic.element; } } /** * Get the DOM element for the series that we put accessibility info on. * @private * @param {Highcharts.Series} series * The series to get element for. * @return {Highcharts.HTMLDOMElement|Highcharts.SVGDOMElement|undefined} * The DOM element for the series */ function getSeriesA11yElement(series) { var firstPointEl = getSeriesFirstPointElement(series); return (firstPointEl && firstPointEl.parentNode || series.graph && series.graph.element || series.group && series.group.element); // Could be tracker series depending on series type } /** * Remove aria-hidden from element. Also unhides parents of the element, and * hides siblings that are not explicitly unhidden. * @private * @param {Highcharts.Chart} chart * @param {Highcharts.HTMLDOMElement|Highcharts.SVGDOMElement} element */ function unhideChartElementFromAT(chart, element) { element.setAttribute('aria-hidden', false); if (element === chart.renderTo || !element.parentNode) { return; } // Hide siblings unless their hidden state is already explicitly set Array.prototype.forEach.call(element.parentNode.childNodes, function (node) { if (!node.hasAttribute('aria-hidden')) { node.setAttribute('aria-hidden', true); } }); // Repeat for parent unhideChartElementFromAT(chart, element.parentNode); } /** * Hide series from screen readers. * @private * @param {Highcharts.Series} series * The series to hide * @return {void} */ function hideSeriesFromAT(series) { var seriesEl = getSeriesA11yElement(series); if (seriesEl) { seriesEl.setAttribute('aria-hidden', true); } } /** * Get series objects by series name. * @private * @param {Highcharts.Chart} chart * @param {string} name * @return {Array<Highcharts.Series>} */ function getSeriesFromName(chart, name) { if (!name) { return chart.series; } return (chart.series || []).filter(function (s) { return s.name === name; }); } /** * Get point in a series from x/y values. * @private * @param {Array<Highcharts.Series>} series * @param {number} x * @param {number} y * @return {Highcharts.Point|undefined} */ function getPointFromXY(series, x, y) { var i = series.length, res; while (i--) { res = find(series[i].points || [], function (p) { return p.x === x && p.y === y; }); if (res) { return res; } } } /** * Get relative position of point on an x/y axis from 0 to 1. * @private * @param {Highcharts.Axis} axis * @param {Highcharts.Point} point * @return {number} */ function getRelativePointAxisPosition(axis, point) { if (!defined(axis.dataMin) || !defined(axis.dataMax)) { return 0; } var axisStart = axis.toPixels(axis.dataMin); var axisEnd = axis.toPixels(axis.dataMax); // We have to use pixel position because of axis breaks, log axis etc. var positionProp = axis.coll === 'xAxis' ? 'x' : 'y'; var pointPos = axis.toPixels(point[positionProp] || 0); return (pointPos - axisStart) / (axisEnd - axisStart); } /** * Get relative position of point on an x/y axis from 0 to 1. * @private * @param {Highcharts.Point} point */ function scrollToPoint(point) { var xAxis = point.series.xAxis; var yAxis = point.series.yAxis; var axis = (xAxis === null || xAxis === void 0 ? void 0 : xAxis.scrollbar) ? xAxis : yAxis; var scrollbar = axis === null || axis === void 0 ? void 0 : axis.scrollbar; if (scrollbar && defined(scrollbar.to) && defined(scrollbar.from)) { var range = scrollbar.to - scrollbar.from; var pos = getRelativePointAxisPosition(axis, point); scrollbar.updatePosition(pos - range / 2, pos + range / 2); fireEvent(scrollbar, 'changed', { from: scrollbar.from, to: scrollbar.to, trigger: 'scrollbar', DOMEvent: null }); } } var ChartUtilities = { getChartTitle: getChartTitle, getAxisDescription: getAxisDescription, getPointFromXY: getPointFromXY, getSeriesFirstPointElement: getSeriesFirstPointElement, getSeriesFromName: getSeriesFromName, getSeriesA11yElement: getSeriesA11yElement, unhideChartElementFromAT: unhideChartElementFromAT, hideSeriesFromAT: hideSeriesFromAT, scrollToPoint: scrollToPoint }; return ChartUtilities; }); _registerModule(_modules, 'Accessibility/KeyboardNavigationHandler.js', [_modules['Core/Utilities.js']], function (U) { /* * * * (c) 2009-2020 Øystein Moseng * * Keyboard navigation handler base class definition * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ var find = U.find; /** * Options for the keyboard navigation handler. * * @interface Highcharts.KeyboardNavigationHandlerOptionsObject */ /** * An array containing pairs of an array of keycodes, mapped to a handler * function. When the keycode is received, the handler is called with the * keycode as parameter. * @name Highcharts.KeyboardNavigationHandlerOptionsObject#keyCodeMap * @type {Array<Array<Array<number>, Function>>} */ /** * Function to run on initialization of module. * @name Highcharts.KeyboardNavigationHandlerOptionsObject#init * @type {Function} */ /** * Function to run before moving to next/prev module. Receives moving direction * as parameter: +1 for next, -1 for previous. * @name Highcharts.KeyboardNavigationHandlerOptionsObject#terminate * @type {Function|undefined} */ /** * Function to run to validate module. Should return false if module should not * run, true otherwise. Receives chart as parameter. * @name Highcharts.KeyboardNavigationHandlerOptionsObject#validate * @type {Function|undefined} */ /* eslint-disable no-invalid-this, valid-jsdoc */ /** * Define a keyboard navigation handler for use with a * Highcharts.AccessibilityComponent instance. This functions as an abstraction * layer for keyboard navigation, and defines a map of keyCodes to handler * functions. * * @requires module:modules/accessibility * * @sample highcharts/accessibility/custom-component * Custom accessibility component * * @class * @name Highcharts.KeyboardNavigationHandler * * @param {Highcharts.Chart} chart * The chart this module should act on. * * @param {Highcharts.KeyboardNavigationHandlerOptionsObject} options * Options for the keyboard navigation handler. */ function KeyboardNavigationHandler(chart, options) { this.chart = chart; this.keyCodeMap = options.keyCodeMap || []; this.validate = options.validate; this.init = options.init; this.terminate = options.terminate; // Response enum this.response = { success: 1, prev: 2, next: 3, noHandler: 4, fail: 5 // Handler failed }; } KeyboardNavigationHandler.prototype = { /** * Find handler function(s) for key code in the keyCodeMap and run it. * * @function KeyboardNavigationHandler#run * @param {global.KeyboardEvent} e * @return {number} Returns a response code indicating whether the run was * a success/fail/unhandled, or if we should move to next/prev module. */ run: function (e) { var keyCode = e.which || e.keyCode, response = this.response.noHandler, handlerCodeSet = find(this.keyCodeMap, function (codeSet) { return codeSet[0].indexOf(keyCode) > -1; }); if (handlerCodeSet) { response = handlerCodeSet[1].call(this, keyCode, e); } else if (keyCode === 9) { // Default tab handler, move to next/prev module response = this.response[e.shiftKey ? 'prev' : 'next']; } return response; } }; return KeyboardNavigationHandler; }); _registerModule(_modules, 'Accessibility/Utils/EventProvider.js', [_modules['Core/Globals.js'], _modules['Core/Utilities.js']], function (H, U) { /* * * * (c) 2009-2020 Øystein Moseng * * Class that can keep track of events added, and clean them up on destroy. * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ var addEvent = U.addEvent, extend = U.extend; /* eslint-disable no-invalid-this, valid-jsdoc */ /** * @private * @class */ var EventProvider = function () { this.eventRemovers = []; }; extend(EventProvider.prototype, { /** * Add an event to an element and keep track of it for later removal. * Same args as Highcharts.addEvent. * @private * @return {Function} */ addEvent: function () { var remover = addEvent.apply(H, arguments); this.eventRemovers.push(remover); return remover; }, /** * Remove all added events. * @private * @return {void} */ removeAddedEvents: function () { this.eventRemovers.forEach(function (remover) { remover(); }); this.eventRemovers = []; } }); return EventProvider; }); _registerModule(_modules, 'Accessibility/Utils/DOMElementProvider.js', [_modules['Core/Globals.js'], _modules['Core/Utilities.js'], _modules['Accessibility/Utils/HTMLUtilities.js']], function (H, U, HTMLUtilities) { /* * * * (c) 2009-2020 Øystein Moseng * * Class that can keep track of elements added to DOM and clean them up on * destroy. * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ var doc = H.win.document; var extend = U.extend; var removeElement = HTMLUtilities.removeElement; /* eslint-disable no-invalid-this, valid-jsdoc */ /** * @private * @class */ var DOMElementProvider = function () { this.elements = []; }; extend(DOMElementProvider.prototype, { /** * Create an element and keep track of it for later removal. * Same args as document.createElement * @private */ createElement: function () { var el = doc.createElement.apply(doc, arguments); this.elements.push(el); return el; }, /** * Destroy all created elements, removing them from the DOM. * @private */ destroyCreatedElements: function () { this.elements.forEach(function (element) { removeElement(element); }); this.elements = []; } }); return DOMElementProvider; }); _registerModule(_modules, 'Accessibility/AccessibilityComponent.js', [_modules['Core/Globals.js'], _modules['Core/Utilities.js'], _modules['Accessibility/Utils/HTMLUtilities.js'], _modules['Accessibility/Utils/ChartUtilities.js'], _modules['Accessibility/Utils/EventProvider.js'], _modules['Accessibility/Utils/DOMElementProvider.js']], function (H, U, HTMLUtilities, ChartUtilities, EventProvider, DOMElementProvider) { /* * * * (c) 2009-2020 Øystein Moseng * * Accessibility component class definition * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ var win = H.win, doc = win.document; var extend = U.extend, fireEvent = U.fireEvent, merge = U.merge; var removeElement = HTMLUtilities.removeElement, getFakeMouseEvent = HTMLUtilities.getFakeMouseEvent; var unhideChartElementFromAT = ChartUtilities.unhideChartElementFromAT; /* eslint-disable valid-jsdoc */ /** @lends Highcharts.AccessibilityComponent */ var functionsToOverrideByDerivedClasses = { /** * Called on component initialization. */ init: function () { }, /** * Get keyboard navigation handler for this component. * @return {Highcharts.KeyboardNavigationHandler} */ getKeyboardNavigation: function () { }, /** * Called on updates to the chart, including options changes. * Note that this is also called on first render of chart. */ onChartUpdate: function () { }, /** * Called on every chart render. */ onChartRender: function () { }, /** * Called when accessibility is disabled or chart is destroyed. */ destroy: function () { } }; /** * The AccessibilityComponent base class, representing a part of the chart that * has accessibility logic connected to it. This class can be inherited from to * create a custom accessibility component for a chart. * * Components should take care to destroy added elements and unregister event * handlers on destroy. This is handled automatically if using this.addEvent and * this.createElement. * * @sample highcharts/accessibility/custom-component * Custom accessibility component * * @requires module:modules/accessibility * @class * @name Highcharts.AccessibilityComponent */ function AccessibilityComponent() { } /** * @lends Highcharts.AccessibilityComponent */ AccessibilityComponent.prototype = { /** * Initialize the class * @private * @param {Highcharts.Chart} chart * Chart object */ initBase: function (chart) { this.chart = chart; this.eventProvider = new EventProvider(); this.domElementProvider = new DOMElementProvider(); // Key code enum for common keys this.keyCodes = { left: 37, right: 39, up: 38, down: 40, enter: 13, space: 32, esc: 27, tab: 9 }; }, /** * Add an event to an element and keep track of it for later removal. * See EventProvider for details. * @private */ addEvent: function () { return this.eventProvider.addEvent .apply(this.eventProvider, arguments); }, /** * Create an element and keep track of it for later removal. * See DOMElementProvider for details. * @private */ createElement: function () { return this.domElementProvider.createElement.apply(this.domElementProvider, arguments); }, /** * Fire an event on an element that is either wrapped by Highcharts, * or a DOM element * @private * @param {Highcharts.HTMLElement|Highcharts.HTMLDOMElement| * Highcharts.SVGDOMElement|Highcharts.SVGElement} el * @param {Event} eventObject */ fireEventOnWrappedOrUnwrappedElement: function (el, eventObject) { var type = eventObject.type; if (doc.createEvent && (el.dispatchEvent || el.fireEvent)) { if (el.dispatchEvent) { el.dispatchEvent(eventObject); } else { el.fireEvent(type, eventObject); } } else { fireEvent(el, type, eventObject); } }, /** * Utility function to attempt to fake a click event on an element. * @private * @param {Highcharts.HTMLDOMElement|Highcharts.SVGDOMElement} element */ fakeClickEvent: function (element) { if (element) { var fakeEventObject = getFakeMouseEvent('click'); this.fireEventOnWrappedOrUnwrappedElement(element, fakeEventObject); } }, /** * Add a new proxy group to the proxy container. Creates the proxy container * if it does not exist. * @private * @param {Highcharts.HTMLAttributes} [attrs] * The attributes to set on the new group div. * @return {Highcharts.HTMLDOMElement} * The new proxy group element. */ addProxyGroup: function (attrs) { this.createOrUpdateProxyContainer(); var groupDiv = this.createElement('div'); Object.keys(attrs || {}).forEach(function (prop) { if (attrs[prop] !== null) { groupDiv.setAttribute(prop, attrs[prop]); } }); this.chart.a11yProxyContainer.appendChild(groupDiv); return groupDiv; }, /** * Creates and updates DOM position of proxy container * @private */ createOrUpdateProxyContainer: function () { var chart = this.chart, rendererSVGEl = chart.renderer.box; chart.a11yProxyContainer = chart.a11yProxyContainer || this.createProxyContainerElement(); if (rendererSVGEl.nextSibling !== chart.a11yProxyContainer) { chart.container.insertBefore(chart.a11yProxyContainer, rendererSVGEl.nextSibling); } }, /** * @private * @return {Highcharts.HTMLDOMElement} element */ createProxyContainerElement: function () { var pc = doc.createElement('div'); pc.className = 'highcharts-a11y-proxy-container'; return pc; }, /** * Create an invisible proxy HTML button in the same position as an SVG * element * @private * @param {Highcharts.SVGElement} svgElement * The wrapped svg el to proxy. * @param {Highcharts.HTMLDOMElement} parentGroup * The proxy group element in the proxy container to add this button to. * @param {Highcharts.SVGAttributes} [attributes] * Additional attributes to set. * @param {Highcharts.SVGElement} [posElement] * Element to use for positioning instead of svgElement. * @param {Function} [preClickEvent] * Function to call before click event fires. * * @return {Highcharts.HTMLDOMElement} The proxy button. */ createProxyButton: function (svgElement, parentGroup, attributes, posElement, preClickEvent) { var svgEl = svgElement.element, proxy = this.createElement('button'), attrs = merge({ 'aria-label': svgEl.getAttribute('aria-label') }, attributes); Object.keys(attrs).forEach(function (prop) { if (attrs[prop] !== null) { proxy.setAttribute(prop, attrs[prop]); } }); proxy.className = 'highcharts-a11y-proxy-button'; if (preClickEvent) { this.addEvent(proxy, 'click', preClickEvent); } this.setProxyButtonStyle(proxy); this.updateProxyButtonPosition(proxy, posElement || svgElement); this.proxyMouseEventsForButton(svgEl, proxy); // Add to chart div and unhide from screen readers parentGroup.appendChild(proxy); if (!attrs['aria-hidden']) { unhideChartElementFromAT(this.chart, proxy); } return proxy; }, /** * Get the position relative to chart container for a wrapped SVG element. * @private * @param {Highcharts.SVGElement} element * The element to calculate position for. * @return {Highcharts.BBoxObject} * Object with x and y props for the position. */ getElementPosition: function (element) { var el = element.element, div = this.chart.renderTo; if (div && el && el.getBoundingClientRect) { var rectEl = el.getBoundingClientRect(), rectDiv = div.getBoundingClientRect(); return { x: rectEl.left - rectDiv.left, y: rectEl.top - rectDiv.top, width: rectEl.right - rectEl.left, height: rectEl.bottom - rectEl.top }; } return { x: 0, y: 0, width: 1, height: 1 }; }, /** * @private * @param {Highcharts.HTMLElement} button The proxy element. */ setProxyButtonStyle: function (button) { merge(true, button.style, { 'border-width': 0, 'background-color': 'transparent', cursor: 'pointer', outline: 'none', opacity: 0.001, filter: 'alpha(opacity=1)', '-ms-filter': 'progid:DXImageTransform.Microsoft.Alpha(Opacity=1)', zIndex: 999, overflow: 'hidden', padding: 0, margin: 0, display: 'block', position: 'absolute' }); }, /** * @private * @param {Highcharts.HTMLElement} proxy The proxy to update position of. * @param {Highcharts.SVGElement} posElement The element to overlay and take position from. */ updateProxyButtonPosition: function (proxy, posElement) { var bBox = this.getElementPosition(posElement); merge(true, proxy.style, { width: (bBox.width || 1) + 'px', height: (bBox.height || 1) + 'px', left: (bBox.x || 0) + 'px', top: (bBox.y || 0) + 'px' }); }, /** * @private * @param {Highcharts.HTMLElement|Highcharts.HTMLDOMElement| * Highcharts.SVGDOMElement|Highcharts.SVGElement} source * @param {Highcharts.HTMLElement} button */ proxyMouseEventsForButton: function (source, button) { var component = this; [ 'click', 'touchstart', 'touchend', 'touchcancel', 'touchmove', 'mouseover', 'mouseenter', 'mouseleave', 'mouseout' ].forEach(function (evtType) { var isTouchEvent = evtType.indexOf('touch') === 0; component.addEvent(button, evtType, function (e) { var clonedEvent = isTouchEvent ? component.cloneTouchEvent(e) : component.cloneMouseEvent(e); if (source) { component.fireEventOnWrappedOrUnwrappedElement(source, clonedEvent); } e.stopPropagation(); e.preventDefault(); }); }); }, /** * Utility function to clone a mouse event for re-dispatching. * @private * @param {global.MouseEvent} e The event to clone. * @return {global.MouseEvent} The cloned event */ cloneMouseEvent: function (e) { if (typeof win.MouseEvent === 'function') { return new win.MouseEvent(e.type, e); } // No MouseEvent support, try using initMouseEvent if (doc.createEvent) { var evt = doc.createEvent('MouseEvent'); if (evt.initMouseEvent) { evt.initMouseEvent(e.type, e.bubbles, // #10561, #12161 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); return evt; } } return getFakeMouseEvent(e.type); }, /** * Utility function to clone a touch event for re-dispatching. * @private * @param {global.TouchEvent} e The event to clone. * @return {global.TouchEvent} The cloned event */ cloneTouchEvent: function (e) { var touchListToTouchArray = function (l) { var touchArray = []; for (var i = 0; i < l.length; ++i) { var item = l.item(i); if (item) { touchArray.push(item); } } return touchArray; }; if (typeof win.TouchEvent === 'function') { var newEvent = new win.TouchEvent(e.type, { touches: touchListToTouchArray(e.touches), targetTouches: touchListToTouchArray(e.targetTouches), changedTouches: touchListToTouchArray(e.changedTouches), ctrlKey: e.ctrlKey, shiftKey: e.shiftKey, altKey: e.altKey, metaKey: e.metaKey, bubbles: e.bubbles, cancelable: e.cancelable, composed: e.composed, detail: e.detail, view: e.view }); if (e.defaultPrevented) { newEvent.preventDefault(); } return newEvent; } // Fallback to mouse event var fakeEvt = this.cloneMouseEvent(e); fakeEvt.touches = e.touches; fakeEvt.changedTouches = e.changedTouches; fakeEvt.targetTouches = e.targetTouches; return fakeEvt; }, /** * Remove traces of the component. * @private */ destroyBase: function () { removeElement(this.chart.a11yProxyContainer); this.domElementProvider.destroyCreatedElements(); this.eventProvider.removeAddedEvents(); } }; extend(AccessibilityComponent.prototype, functionsToOverrideByDerivedClasses); return AccessibilityComponent; }); _registerModule(_modules, 'Accessibility/KeyboardNavigation.js', [_modules['Core/Globals.js'], _modules['Core/Utilities.js'], _modules['Accessibility/Utils/HTMLUtilities.js'], _modules['Accessibility/Utils/EventProvider.js']], function (H, U, HTMLUtilities, EventProvider) { /* * * * (c) 2009-2020 Øystein Moseng * * Main keyboard navigation handling. * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ var doc = H.doc, win = H.win; var addEvent = U.addEvent, fireEvent = U.fireEvent; var getElement = HTMLUtilities.getElement; /* eslint-disable valid-jsdoc */ // Add event listener to document to detect ESC key press and dismiss // hover/popup content. addEvent(doc, 'keydown', function (e) { var keycode = e.which || e.keyCode; var esc = 27; if (keycode === esc && H.charts) { H.charts.forEach(function (chart) { if (chart && chart.dismissPopupContent) { chart.dismissPopupContent(); } }); } }); /** * Dismiss popup content in chart, including export menu and tooltip. */ H.Chart.prototype.dismissPopupContent = function () { var chart = this; fireEvent(this, 'dismissPopupContent', {}, function () { if (chart.tooltip) { chart.tooltip.hide(0); } chart.hideExportMenu(); }); }; /** * The KeyboardNavigation class, containing the overall keyboard navigation * logic for the chart. * * @requires module:modules/accessibility * * @private * @class * @param {Highcharts.Chart} chart * Chart object * @param {object} components * Map of component names to AccessibilityComponent objects. * @name Highcharts.KeyboardNavigation */ function KeyboardNavigation(chart, components) { this.init(chart, components); } KeyboardNavigation.prototype = { /** * Initialize the class * @private * @param {Highcharts.Chart} chart * Chart object * @param {object} components * Map of component names to AccessibilityComponent objects. */ init: function (chart, components) { var _this = this; var ep = this.eventProvider = new EventProvider(); this.chart = chart; this.components = components; this.modules = []; this.currentModuleIx = 0; // Run an update to get all modules this.update(); ep.addEvent(this.tabindexContainer, 'keydown', function (e) { return _this.onKeydown(e); }); ep.addEvent(this.tabindexContainer, 'focus', function (e) { return _this.onFocus(e); }); ep.addEvent(doc, 'mouseup', function () { return _this.onMouseUp(); }); ep.addEvent(chart.renderTo, 'mousedown', function () { _this.isClickingChart = true; }); ep.addEvent(chart.renderTo, 'mouseover', function () { _this.pointerIsOverChart = true; }); ep.addEvent(chart.renderTo, 'mouseout', function () { _this.pointerIsOverChart = false; }); // Init first module if (this.modules.length) { this.modules[0].init(1); } }, /** * Update the modules for the keyboard navigation. * @param {Array<string>} [order] * Array specifying the tab order of the components. */ update: function (order) { var a11yOptions = this.chart.options.accessibility, keyboardOptions = a11yOptions && a11yOptions.keyboardNavigation, components = this.components; this.updateContainerTabindex(); if (keyboardOptions && keyboardOptions.enabled && order && order.length) { // We (still) have keyboard navigation. Update module list this.modules = order.reduce(function (modules, componentName) { var navModules = components[componentName].getKeyboardNavigation(); return modules.concat(navModules); }, []); this.updateExitAnchor(); } else { this.modules = []; this.currentModuleIx = 0; this.removeExitAnchor(); } }, /** * Function to run on container focus * @private * @param {global.FocusEvent} e Browser focus event. */ onFocus: function (e) { var _a; var chart = this.chart; var focusComesFromChart = (e.relatedTarget && chart.container.contains(e.relatedTarget)); // Init keyboard nav if tabbing into chart if (!this.isClickingChart && !focusComesFromChart) { (_a = this.modules[0]) === null || _a === void 0 ? void 0 : _a.init(1); } }, /** * Reset chart navigation state if we click outside the chart and it's * not already reset. * @private */ onMouseUp: function () { delete this.isClickingChart; if (!this.keyboardReset && !this.pointerIsOverChart) { var chart = this.chart, curMod = this.modules && this.modules[this.currentModuleIx || 0]; if (curMod && curMod.terminate) { curMod.terminate(); } if (chart.focusElement) { chart.focusElement.removeFocusBorder(); } this.currentModuleIx = 0; this.keyboardReset = true; } }, /** * Function to run on keydown * @private * @param {global.KeyboardEvent} ev Browser keydown event. */ onKeydown: function (ev) { var e = ev || win.event, preventDefault, curNavModule = this.modules && this.modules.length && this.modules[this.currentModuleIx]; // Used for resetting nav state when clicking outside chart this.keyboardReset = false; // If there is a nav module for the current index, run it. // Otherwise, we are outside of the chart in some direction. if (curNavModule) { var response = curNavModule.run(e); if (response === curNavModule.response.success) { preventDefault = true; } else if (response === curNavModule.response.prev) { preventDefault = this.prev(); } else if (response === curNavModule.response.next) { preventDefault = this.next(); } if (preventDefault) { e.preventDefault(); e.stopPropagation(); } } }, /** * Go to previous module. * @private */ prev: function () { return this.move(-1); }, /** * Go to next module. * @private */ next: function () { return this.move(1); }, /** * Move to prev/next module. * @private * @param {number} direction * Direction to move. +1 for next, -1 for prev. * @return {boolean} * True if there was a valid module in direction. */ move: function (direction) { var curModule = this.modules && this.modules[this.currentModuleIx]; if (curModule && curModule.terminate) { curModule.terminate(direction); } // Remove existing focus border if any if (this.chart.focusElement) { this.chart.focusElement.removeFocusBorder(); } this.currentModuleIx += direction; var newModule = this.modules && this.modules[this.currentModuleIx]; if (newModule) { if (newModule.validate && !newModule.validate()) { return this.move(direction); // Invalid module, recurse } if (newModule.init) { newModule.init(direction); // Valid module, init it return true; } } // No module this.currentModuleIx = 0; // Reset counter // Set focus to chart or exit anchor depending on direction if (direction > 0) { this.exiting = true; this.exitAnchor.focus(); } else { this.tabindexContainer.focus(); } return false; }, /** * We use an exit anchor to move focus out of chart whenever we want, by * setting focus to this div and not preventing the default tab action. We * also use this when users come back into the char