UNPKG

highcharts

Version:
1,230 lines (1,223 loc) 307 kB
/** * @license Highcharts JS v8.0.0 (2019-12-10) * * 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, 'modules/accessibility/utils/htmlUtilities.js', [_modules['parts/Globals.js']], function (H) { /* * * * (c) 2009-2019 Øystein Moseng * * Utility functions for accessibility module. * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ var merge = H.merge, 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', '-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, 'modules/accessibility/utils/chartUtilities.js', [_modules['modules/accessibility/utils/htmlUtilities.js'], _modules['parts/Globals.js']], function (HTMLUtilities, H) { /* * * * (c) 2009-2019 Ø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 find = H.find; /* 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.isDatetimeAxis && '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 * @return {void} */ 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; } } } var ChartUtilities = { getChartTitle: getChartTitle, getAxisDescription: getAxisDescription, getPointFromXY: getPointFromXY, getSeriesFirstPointElement: getSeriesFirstPointElement, getSeriesFromName: getSeriesFromName, getSeriesA11yElement: getSeriesA11yElement, unhideChartElementFromAT: unhideChartElementFromAT, hideSeriesFromAT: hideSeriesFromAT }; return ChartUtilities; }); _registerModule(_modules, 'modules/accessibility/KeyboardNavigationHandler.js', [_modules['parts/Globals.js']], function (H) { /* * * * (c) 2009-2019 Øystein Moseng * * Keyboard navigation handler base class definition * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ var find = H.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']; } else if (keyCode === 27) { // Default esc handler, hide tooltip if (this.chart && this.chart.tooltip) { this.chart.tooltip.hide(0); } response = this.response.success; } return response; } }; return KeyboardNavigationHandler; }); _registerModule(_modules, 'modules/accessibility/utils/EventProvider.js', [_modules['parts/Globals.js'], _modules['parts/Utilities.js']], function (H, U) { /* * * * (c) 2009-2019 Ø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 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 = H.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, 'modules/accessibility/utils/DOMElementProvider.js', [_modules['parts/Globals.js'], _modules['parts/Utilities.js'], _modules['modules/accessibility/utils/htmlUtilities.js']], function (H, U, HTMLUtilities) { /* * * * (c) 2009-2019 Ø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, 'modules/accessibility/AccessibilityComponent.js', [_modules['parts/Globals.js'], _modules['parts/Utilities.js'], _modules['modules/accessibility/utils/htmlUtilities.js'], _modules['modules/accessibility/utils/chartUtilities.js'], _modules['modules/accessibility/utils/EventProvider.js'], _modules['modules/accessibility/utils/DOMElementProvider.js']], function (H, U, HTMLUtilities, ChartUtilities, EventProvider, DOMElementProvider) { /* * * * (c) 2009-2019 Ø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, merge = H.merge, fireEvent = H.fireEvent; var extend = U.extend; 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), bBox = this.getElementPosition(posElement || svgElement); 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, bBox); 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 * @param {Highcharts.BBoxObject} bBox */ setProxyButtonStyle: function (button, bBox) { 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', 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) { component.addEvent(button, evtType, function (e) { var clonedEvent = 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); }, /** * 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, 'modules/accessibility/KeyboardNavigation.js', [_modules['parts/Globals.js'], _modules['modules/accessibility/utils/htmlUtilities.js'], _modules['modules/accessibility/KeyboardNavigationHandler.js'], _modules['modules/accessibility/utils/EventProvider.js']], function (H, HTMLUtilities, KeyboardNavigationHandler, EventProvider) { /* * * * (c) 2009-2019 Øystein Moseng * * Main keyboard navigation handling. * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ var merge = H.merge, win = H.win, doc = win.document; var getElement = HTMLUtilities.getElement; /* eslint-disable valid-jsdoc */ /** * 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 keyboardNavigation = this, e = this.eventProvider = new EventProvider(); this.chart = chart; this.components = components; this.modules = []; this.currentModuleIx = 0; // Add keydown event e.addEvent(chart.renderTo, 'keydown', function (e) { keyboardNavigation.onKeydown(e); }); // Add mouseup event on doc e.addEvent(doc, 'mouseup', function () { keyboardNavigation.onMouseUp(); }); // Run an update to get all modules this.update(); // 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); }, [ // Add an empty module at the start of list, to allow users to // tab into the chart. new KeyboardNavigationHandler(this.chart, { init: function () { } }) ]); this.updateExitAnchor(); } else { this.modules = []; this.currentModuleIx = 0; this.removeExitAnchor(); } }, /** * Reset chart navigation state if we click outside the chart and it's * not already reset. * @private */ onMouseUp: function () { if (!this.keyboardReset && !(this.chart.pointer && this.chart.pointer.chartPosition)) { 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.chart.renderTo.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 chart by tabbing back, in * order to navigate from the end of the chart. * @private */ updateExitAnchor: function () { var endMarkerId = 'highcharts-end-of-chart-marker-' + this.chart.index, endMarker = getElement(endMarkerId); this.removeExitAnchor(); if (endMarker) { this.makeElementAnExitAnchor(endMarker); this.exitAnchor = endMarker; } else { this.createExitAnchor(); } }, /** * Chart container should have tabindex if navigation is enabled. * @private */ updateContainerTabindex: function () { var a11yOptions = this.chart.options.accessibility, keyboardOptions = a11yOptions && a11yOptions.keyboardNavigation, shouldHaveTabindex = !(keyboardOptions && keyboardOptions.enabled === false), container = this.chart.container, curTabindex = container.getAttribute('tabIndex'); if (shouldHaveTabindex && !curTabindex) { container.setAttribute('tabindex', '0'); } else if (!shouldHaveTabindex && curTabindex === '0') { container.removeAttribute('tabindex'); } }, /** * @private */ makeElementAnExitAnchor: function (el) { el.setAttribute('class', 'highcharts-exit-anchor'); el.setAttribute('tabindex', '0'); el.setAttribute('aria-hidden', false); // Handle focus this.addExitAnchorEventsToEl(el); }, /** * Add new exit anchor to the chart. * * @private */ createExitAnchor: function () { var chart = this.chart, exitAnchor = this.exitAnchor = doc.createElement('div'); // Hide exit anchor merge(true, exitAnchor.style, { position: 'absolute', width: '1px', height: '1px', zIndex: 0, overflow: 'hidden', outline: 'none' }); chart.renderTo.appendChild(exitAnchor); this.makeElementAnExitAnchor(exitAnchor); }, /** * @private */ removeExitAnchor: function () { if (this.exitAnchor && this.exitAnchor.parentNode) { this.exitAnchor.parentNode .removeChild(this.exitAnchor); delete this.exitAnchor; } }, /** * @private */ addExitAnchorEventsToEl: function (element) { var chart = this.chart, keyboardNavigation = this; this.eventProvider.addEvent(element, 'focus', function (ev) { var e = ev || win.event, curModule, focusComesFromChart = (e.relatedTarget && chart.container.contains(e.relatedTarget)), comingInBackwards = !(focusComesFromChart || keyboardNavigation.exiting); if (comingInBackwards) { chart.renderTo.focus(); e.preventDefault(); // Move to last valid keyboard nav module // Note the we don't run it, just set the index if (keyboardNavigation.modules && keyboardNavigation.modules.length) { keyboardNavigation.currentModuleIx = keyboardNavigation.modules.length - 1; curModule = keyboardNavigation.modules[keyboardNavigation.currentModuleIx]; // Validate the module if (curModule && curModule.validate && !curModule.validate()) { // Invalid. Try moving backwards to find next valid. keyboardNavigation.prev(); } else if (curModule) { // We have a valid module, init it curModule.init(-1); } } } else { // Don't skip the next focus, we only skip once. keyboardNavigation.exiting = false; } }); }, /** * Remove all traces of keyboard navigation. * @private */ destroy: function () { this.removeExitAnchor(); this.eventProvider.removeAddedEvents(); if (this.chart.container.getAttribute('tabindex') === '0') { this.chart.container.removeAttribute('tabindex'); } } }; return KeyboardNavigation; }); _registerModule(_modules, 'modules/accessibility/components/LegendComponent.js', [_modules['parts/Globals.js'], _modules['parts/Utilities.js'], _modules['modules/accessibility/AccessibilityComponent.js'], _modules['modules/accessibility/KeyboardNavigationHandler.js'], _modules['modules/accessibility/utils/htmlUtilities.js']], function (H, U, AccessibilityComponent, KeyboardNavigationHandler, HTMLUtilities) { /* * * *