highcharts
Version:
JavaScript charting framework
1,315 lines (1,162 loc) • 229 kB
JavaScript
/**
* @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, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/\//g, '/');
},
// 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