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