123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388 |
- /* *
- *
- * (c) 2009-2021 Øystein Moseng
- *
- * Accessibility component class definition
- *
- * License: www.highcharts.com/license
- *
- * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
- *
- * */
- 'use strict';
- import ChartUtilities from './Utils/ChartUtilities.js';
- var unhideChartElementFromAT = ChartUtilities.unhideChartElementFromAT;
- import DOMElementProvider from './Utils/DOMElementProvider.js';
- import EventProvider from './Utils/EventProvider.js';
- import H from '../Core/Globals.js';
- var doc = H.doc, win = H.win;
- import HTMLUtilities from './Utils/HTMLUtilities.js';
- var removeElement = HTMLUtilities.removeElement, getFakeMouseEvent = HTMLUtilities.getFakeMouseEvent;
- import U from '../Core/Utilities.js';
- var extend = U.extend, fireEvent = U.fireEvent, merge = U.merge;
- /* eslint-disable valid-jsdoc */
- /** @lends Highcharts.AccessibilityComponent */
- var functionsToOverrideByDerivedClasses = {
- /**
- * Called on component initialization.
- */
- init: function () { },
- /**
- * Get keyboard navigation handler for this component.
- * @return {Highcharts.KeyboardNavigationHandler}
- */
- getKeyboardNavigation: function () { },
- /**
- * Called on updates to the chart, including options changes.
- * Note that this is also called on first render of chart.
- */
- onChartUpdate: function () { },
- /**
- * Called on every chart render.
- */
- onChartRender: function () { },
- /**
- * Called when accessibility is disabled or chart is destroyed.
- */
- destroy: function () { }
- };
- /**
- * The AccessibilityComponent base class, representing a part of the chart that
- * has accessibility logic connected to it. This class can be inherited from to
- * create a custom accessibility component for a chart.
- *
- * Components should take care to destroy added elements and unregister event
- * handlers on destroy. This is handled automatically if using this.addEvent and
- * this.createElement.
- *
- * @sample highcharts/accessibility/custom-component
- * Custom accessibility component
- *
- * @requires module:modules/accessibility
- * @class
- * @name Highcharts.AccessibilityComponent
- */
- function AccessibilityComponent() { }
- /**
- * @lends Highcharts.AccessibilityComponent
- */
- AccessibilityComponent.prototype = {
- /**
- * Initialize the class
- * @private
- * @param {Highcharts.Chart} chart
- * Chart object
- */
- initBase: function (chart) {
- this.chart = chart;
- this.eventProvider = new EventProvider();
- this.domElementProvider = new DOMElementProvider();
- // Key code enum for common keys
- this.keyCodes = {
- left: 37,
- right: 39,
- up: 38,
- down: 40,
- enter: 13,
- space: 32,
- esc: 27,
- tab: 9
- };
- },
- /**
- * Add an event to an element and keep track of it for later removal.
- * See EventProvider for details.
- * @private
- */
- addEvent: function () {
- return this.eventProvider.addEvent
- .apply(this.eventProvider, arguments);
- },
- /**
- * Create an element and keep track of it for later removal.
- * See DOMElementProvider for details.
- * @private
- */
- createElement: function () {
- return this.domElementProvider.createElement.apply(this.domElementProvider, arguments);
- },
- /**
- * Fire an event on an element that is either wrapped by Highcharts,
- * or a DOM element
- * @private
- * @param {Highcharts.HTMLElement|Highcharts.HTMLDOMElement|
- * Highcharts.SVGDOMElement|Highcharts.SVGElement} el
- * @param {Event} eventObject
- */
- fireEventOnWrappedOrUnwrappedElement: function (el, eventObject) {
- var type = eventObject.type;
- if (doc.createEvent && (el.dispatchEvent || el.fireEvent)) {
- if (el.dispatchEvent) {
- el.dispatchEvent(eventObject);
- }
- else {
- el.fireEvent(type, eventObject);
- }
- }
- else {
- fireEvent(el, type, eventObject);
- }
- },
- /**
- * Utility function to attempt to fake a click event on an element.
- * @private
- * @param {Highcharts.HTMLDOMElement|Highcharts.SVGDOMElement} element
- */
- fakeClickEvent: function (element) {
- if (element) {
- var fakeEventObject = getFakeMouseEvent('click');
- this.fireEventOnWrappedOrUnwrappedElement(element, fakeEventObject);
- }
- },
- /**
- * Add a new proxy group to the proxy container. Creates the proxy container
- * if it does not exist.
- * @private
- * @param {Highcharts.HTMLAttributes} [attrs]
- * The attributes to set on the new group div.
- * @return {Highcharts.HTMLDOMElement}
- * The new proxy group element.
- */
- addProxyGroup: function (attrs) {
- this.createOrUpdateProxyContainer();
- var groupDiv = this.createElement('div');
- Object.keys(attrs || {}).forEach(function (prop) {
- if (attrs[prop] !== null) {
- groupDiv.setAttribute(prop, attrs[prop]);
- }
- });
- this.chart.a11yProxyContainer.appendChild(groupDiv);
- return groupDiv;
- },
- /**
- * Creates and updates DOM position of proxy container
- * @private
- */
- createOrUpdateProxyContainer: function () {
- var chart = this.chart, rendererSVGEl = chart.renderer.box;
- chart.a11yProxyContainer = chart.a11yProxyContainer ||
- this.createProxyContainerElement();
- if (rendererSVGEl.nextSibling !== chart.a11yProxyContainer) {
- chart.container.insertBefore(chart.a11yProxyContainer, rendererSVGEl.nextSibling);
- }
- },
- /**
- * @private
- * @return {Highcharts.HTMLDOMElement} element
- */
- createProxyContainerElement: function () {
- var pc = doc.createElement('div');
- pc.className = 'highcharts-a11y-proxy-container';
- return pc;
- },
- /**
- * Create an invisible proxy HTML button in the same position as an SVG
- * element
- * @private
- * @param {Highcharts.SVGElement} svgElement
- * The wrapped svg el to proxy.
- * @param {Highcharts.HTMLDOMElement} parentGroup
- * The proxy group element in the proxy container to add this button to.
- * @param {Highcharts.SVGAttributes} [attributes]
- * Additional attributes to set.
- * @param {Highcharts.SVGElement} [posElement]
- * Element to use for positioning instead of svgElement.
- * @param {Function} [preClickEvent]
- * Function to call before click event fires.
- *
- * @return {Highcharts.HTMLDOMElement} The proxy button.
- */
- createProxyButton: function (svgElement, parentGroup, attributes, posElement, preClickEvent) {
- var svgEl = svgElement.element, proxy = this.createElement('button'), attrs = merge({
- 'aria-label': svgEl.getAttribute('aria-label')
- }, attributes);
- Object.keys(attrs).forEach(function (prop) {
- if (attrs[prop] !== null) {
- proxy.setAttribute(prop, attrs[prop]);
- }
- });
- proxy.className = 'highcharts-a11y-proxy-button';
- if (svgElement.hasClass('highcharts-no-tooltip')) {
- proxy.className += ' highcharts-no-tooltip';
- }
- if (preClickEvent) {
- this.addEvent(proxy, 'click', preClickEvent);
- }
- this.setProxyButtonStyle(proxy);
- this.updateProxyButtonPosition(proxy, posElement || svgElement);
- this.proxyMouseEventsForButton(svgEl, proxy);
- // Add to chart div and unhide from screen readers
- parentGroup.appendChild(proxy);
- if (!attrs['aria-hidden']) {
- unhideChartElementFromAT(this.chart, proxy);
- }
- return proxy;
- },
- /**
- * Get the position relative to chart container for a wrapped SVG element.
- * @private
- * @param {Highcharts.SVGElement} element
- * The element to calculate position for.
- * @return {Highcharts.BBoxObject}
- * Object with x and y props for the position.
- */
- getElementPosition: function (element) {
- var el = element.element, div = this.chart.renderTo;
- if (div && el && el.getBoundingClientRect) {
- var rectEl = el.getBoundingClientRect(), rectDiv = div.getBoundingClientRect();
- return {
- x: rectEl.left - rectDiv.left,
- y: rectEl.top - rectDiv.top,
- width: rectEl.right - rectEl.left,
- height: rectEl.bottom - rectEl.top
- };
- }
- return { x: 0, y: 0, width: 1, height: 1 };
- },
- /**
- * @private
- * @param {Highcharts.HTMLElement} button The proxy element.
- */
- setProxyButtonStyle: function (button) {
- merge(true, button.style, {
- borderWidth: '0',
- backgroundColor: 'transparent',
- cursor: 'pointer',
- outline: 'none',
- opacity: '0.001',
- filter: 'alpha(opacity=1)',
- zIndex: '999',
- overflow: 'hidden',
- padding: '0',
- margin: '0',
- display: 'block',
- position: 'absolute'
- });
- button.style['-ms-filter'] =
- 'progid:DXImageTransform.Microsoft.Alpha(Opacity=1)';
- },
- /**
- * @private
- * @param {Highcharts.HTMLElement} proxy The proxy to update position of.
- * @param {Highcharts.SVGElement} posElement The element to overlay and take position from.
- */
- updateProxyButtonPosition: function (proxy, posElement) {
- var bBox = this.getElementPosition(posElement);
- merge(true, proxy.style, {
- width: (bBox.width || 1) + 'px',
- height: (bBox.height || 1) + 'px',
- left: (Math.round(bBox.x) || 0) + 'px',
- top: (Math.round(bBox.y) || 0) + 'px'
- });
- },
- /**
- * @private
- * @param {Highcharts.HTMLElement|Highcharts.HTMLDOMElement|
- * Highcharts.SVGDOMElement|Highcharts.SVGElement} source
- * @param {Highcharts.HTMLElement} button
- */
- proxyMouseEventsForButton: function (source, button) {
- var component = this;
- [
- 'click', 'touchstart', 'touchend', 'touchcancel', 'touchmove',
- 'mouseover', 'mouseenter', 'mouseleave', 'mouseout'
- ].forEach(function (evtType) {
- var isTouchEvent = evtType.indexOf('touch') === 0;
- component.addEvent(button, evtType, function (e) {
- var clonedEvent = isTouchEvent ?
- component.cloneTouchEvent(e) :
- component.cloneMouseEvent(e);
- if (source) {
- component.fireEventOnWrappedOrUnwrappedElement(source, clonedEvent);
- }
- e.stopPropagation();
- // #9682, #15318: Touch scrolling didnt work when touching a
- // component
- if (evtType !== 'touchstart' && evtType !== 'touchmove' && evtType !== 'touchend') {
- e.preventDefault();
- }
- }, { passive: false });
- });
- },
- /**
- * Utility function to clone a mouse event for re-dispatching.
- * @private
- * @param {global.MouseEvent} e The event to clone.
- * @return {global.MouseEvent} The cloned event
- */
- cloneMouseEvent: function (e) {
- if (typeof win.MouseEvent === 'function') {
- return new win.MouseEvent(e.type, e);
- }
- // No MouseEvent support, try using initMouseEvent
- if (doc.createEvent) {
- var evt = doc.createEvent('MouseEvent');
- if (evt.initMouseEvent) {
- evt.initMouseEvent(e.type, e.bubbles, // #10561, #12161
- e.cancelable, e.view || win, e.detail, e.screenX, e.screenY, e.clientX, e.clientY, e.ctrlKey, e.altKey, e.shiftKey, e.metaKey, e.button, e.relatedTarget);
- return evt;
- }
- }
- return getFakeMouseEvent(e.type);
- },
- /**
- * Utility function to clone a touch event for re-dispatching.
- * @private
- * @param {global.TouchEvent} e The event to clone.
- * @return {global.TouchEvent} The cloned event
- */
- cloneTouchEvent: function (e) {
- var touchListToTouchArray = function (l) {
- var touchArray = [];
- for (var i = 0; i < l.length; ++i) {
- var item = l.item(i);
- if (item) {
- touchArray.push(item);
- }
- }
- return touchArray;
- };
- if (typeof win.TouchEvent === 'function') {
- var newEvent = new win.TouchEvent(e.type, {
- touches: touchListToTouchArray(e.touches),
- targetTouches: touchListToTouchArray(e.targetTouches),
- changedTouches: touchListToTouchArray(e.changedTouches),
- ctrlKey: e.ctrlKey,
- shiftKey: e.shiftKey,
- altKey: e.altKey,
- metaKey: e.metaKey,
- bubbles: e.bubbles,
- cancelable: e.cancelable,
- composed: e.composed,
- detail: e.detail,
- view: e.view
- });
- if (e.defaultPrevented) {
- newEvent.preventDefault();
- }
- return newEvent;
- }
- // Fallback to mouse event
- var fakeEvt = this.cloneMouseEvent(e);
- fakeEvt.touches = e.touches;
- fakeEvt.changedTouches = e.changedTouches;
- fakeEvt.targetTouches = e.targetTouches;
- return fakeEvt;
- },
- /**
- * Remove traces of the component.
- * @private
- */
- destroyBase: function () {
- removeElement(this.chart.a11yProxyContainer);
- this.domElementProvider.destroyCreatedElements();
- this.eventProvider.removeAddedEvents();
- }
- };
- extend(AccessibilityComponent.prototype, functionsToOverrideByDerivedClasses);
- export default AccessibilityComponent;
|