UNPKG

highcharts

Version:
1,315 lines (1,162 loc) 229 kB
/** * @license Highcharts JS v7.1.2 (2019-06-04) * * 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/KeyboardNavigationHandler.js', [_modules['parts/Globals.js']], function (H) { /* * * * (c) 2009-2019 Øystein Moseng * * Keyboard navigation handler base class definition * * License: www.highcharts.com/license * * */ var find = H.find; /** * 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 {object} options * @param {Array<Array<Number>, Function>} options.keyCodeMap * 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. * @param {Function} [options.init] * Function to run on initialization of module * @param {Function} [options.validate] * Function to run to validate module. Should return false if module should * not run, true otherwise. Receives chart as parameter. * @param {Function} [options.terminate] * Function to run before moving to next/prev module. Receives moving * direction as parameter: +1 for next, -1 for previous. * @param {Function} [options.init] * Function to run on initialization of module */ 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, // Keycode was handled prev: 2, // Move to prev module next: 3, // Move to next module noHandler: 4, // There is no handler for this keycode fail: 5 // Handler failed }; } KeyboardNavigationHandler.prototype = { /** * Find handler function(s) for key code in the keyCodeMap and run it. * * @function KeyboardNavigationHandler#run * @param {global.Event} 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/AccessibilityComponent.js', [_modules['parts/Globals.js']], function (Highcharts) { /* * * * (c) 2009-2019 Øystein Moseng * * Accessibility component class definition * * License: www.highcharts.com/license * * */ var win = Highcharts.win, doc = win.document, merge = Highcharts.merge, addEvent = Highcharts.addEvent; /** * 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. * * A component: * - Must call initBase after inheriting. * - Can override any of the following functions: init(), destroy(), * getKeyboardNavigation(), onChartUpdate(). * - Should take care to destroy added elements and unregister event handlers * on destroy. * * @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.eventRemovers = []; this.domElements = []; // Key code enum for common keys this.keyCodes = { left: 37, right: 39, up: 38, down: 40, enter: 13, space: 32, esc: 27, tab: 9 }; // CSS Styles for hiding elements visually but keeping them visible to // AT. this.hiddenStyle = { position: 'absolute', width: '1px', height: '1px', overflow: 'hidden' }; }, /** * Add an event to an element and keep track of it for destroy(). * Same args as Highcharts.addEvent * @private */ addEvent: function () { var remover = Highcharts.addEvent.apply(Highcharts, arguments); this.eventRemovers.push(remover); return remover; }, /** * Create an element and keep track of it for destroy(). * Same args as document.createElement * @private */ createElement: function () { var el = Highcharts.win.document.createElement.apply( Highcharts.win.document, arguments ); this.domElements.push(el); return el; }, /** * Utility function to clone a mouse event for re-dispatching. * @private * @param {global.Event} event The event to clone. * @return {global.Event} The cloned event */ cloneMouseEvent: function (event) { if (typeof win.MouseEvent === 'function') { return new win.MouseEvent(event.type, event); } // No MouseEvent support, try using initMouseEvent if (doc.createEvent) { var evt = doc.createEvent('MouseEvent'); if (evt.initMouseEvent) { evt.initMouseEvent( event.type, event.canBubble, event.cancelable, event.view, event.detail, event.screenX, event.screenY, event.clientX, event.clientY, event.ctrlKey, event.altKey, event.shiftKey, event.metaKey, event.button, event.relatedTarget ); return evt; } // Fallback to basic Event evt = doc.createEvent('Event'); if (evt.initEvent) { evt.initEvent(event.type, true, true); return evt; } } }, /** * Utility function to attempt to fake a click event on an element. * @private * @param {Highcharts.HTMLDOMElement|Highcharts.SVGDOMElement} element */ fakeClickEvent: function (element) { if (element && element.onclick && doc.createEvent) { var fakeEvent = doc.createEvent('Event'); fakeEvent.initEvent('click', true, false); element.onclick(fakeEvent); } }, /** * 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.HTMLElement} parentGroup The proxy group element in * the proxy container to add this button to. * @param {object} [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.HTMLElement} The proxy button. */ createProxyButton: function ( svgElement, parentGroup, attributes, posElement, preClickEvent ) { var svgEl = svgElement.element, component = this, proxy = this.createElement('button'), attrs = merge({ 'aria-label': svgEl.getAttribute('aria-label') }, attributes), positioningElement = posElement || svgElement, bBox = this.getElementPosition(positioningElement); // If we don't support getBoundingClientRect, no button is made if (!bBox) { return; } Object.keys(attrs).forEach(function (prop) { if (attrs[prop] !== null) { proxy.setAttribute(prop, attrs[prop]); } }); merge(true, proxy.style, { 'border-width': 0, 'background-color': 'transparent', position: 'absolute', width: (bBox.width || 1) + 'px', height: (bBox.height || 1) + 'px', display: 'block', cursor: 'pointer', overflow: 'hidden', outline: 'none', opacity: 0.001, filter: 'alpha(opacity=1)', '-ms-filter': 'progid:DXImageTransform.Microsoft.Alpha(Opacity=1)', zIndex: 999, padding: 0, margin: 0, left: bBox.x + 'px', top: bBox.y - this.chart.containerHeight + 'px' }); // Handle pre-click if (preClickEvent) { addEvent(proxy, 'click', preClickEvent); } // Proxy mouse events [ 'click', 'mouseover', 'mouseenter', 'mouseleave', 'mouseout' ].forEach(function (evtType) { addEvent(proxy, evtType, function (e) { var clonedEvent = component.cloneMouseEvent(e); if (svgEl) { if (clonedEvent) { if (svgEl.fireEvent) { svgEl.fireEvent(clonedEvent); } else if (svgEl.dispatchEvent) { svgEl.dispatchEvent(clonedEvent); } } else if (svgEl['on' + evtType]) { svgEl['on' + evtType](e); } } }); }); // Add to chart div and unhide from screen readers parentGroup.appendChild(proxy); if (!attrs['aria-hidden']) { this.unhideElementFromScreenReaders(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 {object} 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 }; } }, /** * Add a new proxy group to the proxy container. Creates the proxy container * if it does not exist. * @private * @param {object} attrs The attributes to set on the new group div. * * @return {Highcharts.HTMLDOMElement} The new proxy group element. */ addProxyGroup: function (attrs) { var chart = this.chart, proxyContainer = chart.a11yProxyContainer; // Add root proxy container if it does not exist if (!proxyContainer) { chart.a11yProxyContainer = doc.createElement('div'); chart.a11yProxyContainer.style.position = 'relative'; } // Add it if it is new, else make sure we move it to the end if (chart.container.nextSibling !== chart.a11yProxyContainer) { chart.renderTo.insertBefore( chart.a11yProxyContainer, chart.container.nextSibling ); } // Create the group and add it var groupDiv = this.createElement('div'); Object.keys(attrs || {}).forEach(function (prop) { if (attrs[prop] !== null) { groupDiv.setAttribute(prop, attrs[prop]); } }); chart.a11yProxyContainer.appendChild(groupDiv); return groupDiv; }, /** * Utility function for removing an element from the DOM. * @private * @param {Highcharts.HTMLDOMElement} element The element to remove. */ removeElement: function (element) { if (element && element.parentNode) { element.parentNode.removeChild(element); } }, /** * Unhide an element from screen readers. Also unhides parents, and hides * siblings that are not explicitly unhidden. * @private * @param {Highcharts.HTMLDOMElement|Highcharts.SVGDOMElement} element * The element to unhide */ unhideElementFromScreenReaders: function (element) { element.setAttribute('aria-hidden', false); if (element === this.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 this.unhideElementFromScreenReaders(element.parentNode); }, /** * Should remove any event handlers added, as well as any DOM elements. * @private */ destroyBase: function () { // Destroy proxy container var chart = this.chart || {}, component = this; this.removeElement(chart.a11yProxyContainer); // Remove event callbacks and dom elements this.eventRemovers.forEach(function (remover) { remover(); }); this.domElements.forEach(function (element) { component.removeElement(element); }); this.eventRemovers = []; this.domElements = []; }, /** * Utility function to strip tags from a string. Used for aria-label * attributes, painting on a canvas will fail if the text contains tags. * @private * @param {string} s The string to strip tags from * @return {string} The new string. */ stripTags: function (s) { return typeof s === 'string' ? s.replace(/<\/?[^>]+(>|$)/g, '') : s; }, /** * HTML encode some characters vulnerable for XSS. * @private * @param {string} html The input string. * @return {string} The escaped string. */ htmlencode: function (html) { return html .replace(/&/g, '&amp;') .replace(/</g, '&lt;') .replace(/>/g, '&gt;') .replace(/"/g, '&quot;') .replace(/'/g, '&#x27;') .replace(/\//g, '&#x2F;'); }, // Functions to be overridden by derived classes /** * Initialize component. */ 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. * Should call destroyBase to make sure events/elements added are removed. */ destroy: function () { this.destroyBase(); } }; return AccessibilityComponent; }); _registerModule(_modules, 'modules/accessibility/KeyboardNavigation.js', [_modules['parts/Globals.js'], _modules['modules/accessibility/KeyboardNavigationHandler.js']], function (H, KeyboardNavigationHandler) { /* * * * (c) 2009-2019 Øystein Moseng * * Main keyboard navigation handling. * * License: www.highcharts.com/license * * */ var merge = H.merge, addEvent = H.addEvent, win = H.win, doc = win.document; /** * 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, order) { this.init(chart, components, order); } 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; this.chart = chart; this.components = components; this.modules = []; this.currentModuleIx = 0; // Make chart container reachable by tab if (!chart.container.hasAttribute('tabIndex')) { chart.container.setAttribute('tabindex', '0'); } // Add exit anchor for focus this.addExitAnchor(); // Add keydown event this.unbindKeydownHandler = addEvent( chart.renderTo, 'keydown', function (e) { keyboardNavigation.onKeydown(e); } ); // Add mouseup event on doc this.unbindMouseUpHandler = 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; 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(); // If we didn't get back a list of modules, just push the one if (!navModules.length) { modules.push(navModules); return modules; } // Add all of the modules 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, {}) ]); } else { // Clear module list and reset this.modules = []; this.currentModuleIx = 0; } }, /** * 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.Event} 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(); } } }, /** * 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; }, /** * Add exit anchor to the chart. We use this 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 */ addExitAnchor: function () { var chart = this.chart, exitAnchorWrapper = this.exitAnchorWrapper = doc.createElement('div'), exitAnchor = this.exitAnchor = doc.createElement('h6'), keyboardNavigation = this, exitAnchorLabel = chart.langFormat( 'accessibility.svgContainerEnd', { chart: chart } ); exitAnchorWrapper.setAttribute('aria-hidden', 'false'); exitAnchorWrapper.setAttribute( 'class', 'highcharts-exit-anchor-wrapper' ); exitAnchorWrapper.style.position = 'relative'; exitAnchorWrapper.style.outline = 'none'; exitAnchor.setAttribute('tabindex', '0'); exitAnchor.setAttribute('aria-label', exitAnchorLabel); exitAnchor.setAttribute('aria-hidden', false); // Hide exit anchor merge(true, exitAnchor.style, { position: 'absolute', width: '1px', height: '1px', bottom: '5px', // Avoid scrollbars (#10637) zIndex: 0, overflow: 'hidden', outline: 'none' }); exitAnchorWrapper.appendChild(exitAnchor); chart.renderTo.appendChild(exitAnchorWrapper); // Update position on render this.unbindExitAnchorUpdate = addEvent(chart, 'render', function () { this.renderTo.appendChild(exitAnchorWrapper); }); // Handle focus this.unbindExitAnchorFocus = addEvent( exitAnchor, 'focus', function (ev) { var e = ev || win.event, curModule; // If focusing and we are exiting, do nothing once. if (!keyboardNavigation.exiting) { // Not exiting, means we are coming in backwards 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 () { // Remove exit anchor if (this.unbindExitAnchorFocus) { this.unbindExitAnchorFocus(); delete this.unbindExitAnchorFocus; } if (this.unbindExitAnchorUpdate) { this.unbindExitAnchorUpdate(); delete this.unbindExitAnchorUpdate; } if (this.exitAnchorWrapper && this.exitAnchorWrapper.parentNode) { this.exitAnchorWrapper.parentNode .removeChild(this.exitAnchorWrapper); delete this.exitAnchor; delete this.exitAnchorWrapper; } // Remove keydown handler if (this.unbindKeydownHandler) { this.unbindKeydownHandler(); } // Remove mouseup handler if (this.unbindMouseUpHandler) { this.unbindMouseUpHandler(); } } }; return KeyboardNavigation; }); _registerModule(_modules, 'modules/accessibility/components/LegendComponent.js', [_modules['parts/Globals.js'], _modules['modules/accessibility/AccessibilityComponent.js'], _modules['modules/accessibility/KeyboardNavigationHandler.js']], function (H, AccessibilityComponent, KeyboardNavigationHandler) { /* * * * (c) 2009-2019 Øystein Moseng * * Accessibility component for chart legend. * * License: www.highcharts.com/license * * */ /** * Highlight legend item by index. * * @private * @function Highcharts.Chart#highlightLegendItem * * @param {number} ix * * @return {boolean} */ H.Chart.prototype.highlightLegendItem = function (ix) { var items = this.legend.allItems, oldIx = this.highlightedLegendItemIx; if (items[ix]) { if (items[oldIx]) { H.fireEvent( items[oldIx].legendGroup.element, 'mouseout' ); } // Scroll if we have to if (items[ix].pageIx !== undefined && items[ix].pageIx + 1 !== this.legend.currentPage) { this.legend.scroll(1 + items[ix].pageIx - this.legend.currentPage); } // Focus this.setFocusToElement( items[ix].legendItem, items[ix].a11yProxyElement ); H.fireEvent(items[ix].legendGroup.element, 'mouseover'); return true; } return false; }; // Keep track of pressed state for legend items H.addEvent(H.Legend, 'afterColorizeItem', function (e) { var chart = this.chart, a11yOptions = chart.options.accessibility, legendItem = e.item; if (a11yOptions.enabled && legendItem && legendItem.a11yProxyElement) { legendItem.a11yProxyElement.setAttribute( 'aria-pressed', e.visible ? 'false' : 'true' ); } }); /** * The LegendComponent class * * @private * @class * @name Highcharts.LegendComponent * @param {Highcharts.Chart} chart * Chart object */ var LegendComponent = function (chart) { this.initBase(chart); }; LegendComponent.prototype = new AccessibilityComponent(); H.extend(LegendComponent.prototype, /** @lends Highcharts.LegendComponent */ { /** * The legend needs updates on every render, in order to update positioning * of the proxy overlays. */ onChartRender: function () { var chart = this.chart, a11yOptions = chart.options.accessibility, items = chart.legend && chart.legend.allItems, component = this; // Ignore render after proxy clicked. No need to destroy it, and // destroying also kills focus. if (component.legendProxyButtonClicked) { delete component.legendProxyButtonClicked; return; } // Always Remove group if exists this.removeElement(this.legendProxyGroup); // Skip everything if we do not have legend items, or if we have a // color axis if ( !items || !items.length || chart.colorAxis && chart.colorAxis.length || !chart.options.legend.accessibility.enabled ) { return; } // Add proxy group this.legendProxyGroup = this.addProxyGroup({ 'aria-label': chart.langFormat( 'accessibility.legendLabel' ), 'role': a11yOptions.landmarkVerbosity === 'all' ? 'region' : null }); // Proxy the legend items items.forEach(function (item) { if (item.legendItem && item.legendItem.element) { item.a11yProxyElement = component.createProxyButton( item.legendItem, component.legendProxyGroup, { tabindex: -1, 'aria-pressed': !item.visible, 'aria-label': chart.langFormat( 'accessibility.legendItem', { chart: chart, itemName: component.stripTags(item.name) } ) }, // Consider useHTML item.legendGroup.div ? item.legendItem : item.legendGroup, // Additional click event (fires first) function () { // Keep track of when we should ignore next render component.legendProxyButtonClicked = true; } ); } }); }, /** * Get keyboard navigation handler for this component. * @return {Highcharts.KeyboardNavigationHandler} */ getKeyboardNavigation: function () { var keys = this.keyCodes, component = this, chart = this.chart, a11yOptions = chart.options.accessibility; return new KeyboardNavigationHandler(chart, { keyCodeMap: [ // Arrow key handling [[ keys.left, keys.right, keys.up, keys.down ], function (keyCode) { var direction = ( keyCode === keys.left || keyCode === keys.up ) ? -1 : 1; // Try to highlight next/prev legend item var res = chart.highlightLegendItem( component.highlightedLegendItemIx + direction ); if (res) { component.highlightedLegendItemIx += direction; return this.response.success; } // Failed, can we wrap around? if ( chart.legend.allItems.length > 1 && a11yOptions.keyboardNavigation.wrapAround ) { // Wrap around if we failed and have more than 1 item this.init(direction); return this.response.success; } // No wrap, move return this.response[direction > 0 ? 'next' : 'prev']; }], // Click item [[ keys.enter, keys.space ], function () { var legendItem = chart.legend.allItems[ component.highlightedLegendItemIx ]; if (legendItem && legendItem.a11yProxyElement) { H.fireEvent(legendItem.a11yProxyElement, 'click'); } return this.response.success; }] ], // Only run this module if we have at least one legend - wait for // it - item. Don't run if the legend is populated by a colorAxis. // Don't run if legend navigation is disabled. validate: function () { var legendOptions = chart.options.legend; return chart.legend && chart.legend.allItems && chart.legend.display && !(chart.colorAxis && chart.colorAxis.length) && legendOptions && legendOptions.accessibility && legendOptions.accessibility.enabled && legendOptions.accessibility.keyboardNavigation && legendOptions.accessibility.keyboardNavigation.enabled; }, // Focus first/last item init: function (direction) { var ix = direction > 0 ? 0 : chart.legend.allItems.length - 1; chart.highlightLegendItem(ix); component.highlightedLegendItemIx = ix; } }); } }); return LegendComponent; }); _registerModule(_modules, 'modules/accessibility/components/MenuComponent.js', [_modules['parts/Globals.js'], _modules['modules/accessibility/AccessibilityComponent.js'], _modules['modules/accessibility/KeyboardNavigationHandler.js']], function (H, AccessibilityComponent, KeyboardNavigationHandler) { /* * * * (c) 2009-2019 Øystein Moseng * * Accessibility component for exporting menu. * * License: www.highcharts.com/license * * */ /** * Show the export menu and focus the first item (if exists). * * @private * @function Highcharts.Chart#showExportMenu */ H.Chart.prototype.showExportMenu = function () { if (this.exportSVGElements && this.exportSVGElements[0]) { this.exportSVGElements[0].element.onclick(); this.highlightExportItem(0); } }; /** * Hide export menu. * * @private * @function Highcharts.Chart#hideExportMenu */ H.Chart.prototype.hideExportMenu = function () { var chart = this, exportList = chart.exportDivElements; if (exportList && chart.exportContextMenu) { // Reset hover states etc. exportList.forEach(function (el) { if (el.className === 'highcharts-menu-item' && el.onmouseout) { el.onmouseout(); } }); chart.highlightedExportItemIx = 0; // Hide the menu div chart.exportContextMenu.hideMenu(); // Make sure the chart has focus and can capture keyboard events chart.container.focus(); } }; /** * Highlight export menu item by index. * * @private * @function Highcharts.Chart#highlightExportItem * * @param {number} ix * * @return {true|undefined} */ H.Chart.prototype.highlightExportItem = function (ix) { var listItem = this.exportDivElements && this.exportDivElements[ix], curHighlighted = this.exportDivElements && this.exportDivElements[this.highlightedExportItemIx], hasSVGFocusSupport; if ( listItem && listItem.tagName === 'DIV' && !(listItem.children && listItem.children.length) ) { // Test if we have focus support for SVG elements hasSVGFocusSupport = !!( this.renderTo.getElementsByTagName('g')[0] || {} ).focus; // Only focus if we can set focus back to the elements after // destroying the menu (#7422) if (listItem.focus && hasSVGFocusSupport) { listItem.focus(); } if (curHighlighted && curHighlighted.onmouseout) { curHighlighted.onmouseout(); } if (listItem.onmouseover) { listItem.onmouseover(); } this.highlightedExportItemIx = ix; return true; } }; /** * Try to highlight the last valid export menu item. * * @private * @function Highcharts.Chart#highlightLastExportItem */ H.Chart.prototype.highlightLastExportItem = function () { var chart = this, i; if (chart.exportDivElements) { i = chart.exportDivElements.length; while (i--) { if (chart.highlightExportItem(i)) { return true; } } } return false; }; /** * The MenuComponent class * * @private * @class * @name Highcharts.MenuComponent * @param {Highcharts.Chart} chart * Chart object */ var MenuComponent = function (chart) { this.initBase(chart); this.init(); }; MenuComponent.prototype = new AccessibilityComponent(); H.extend(MenuComponent.prototype, /** @lends Highcharts.MenuComponent */ { /** * Init the component */ init: function () { var chart = this.chart; // Hide the export menu