/* * * * (c) 2009-2020 Øystein Moseng * * Accessibility component class definition * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ 'use strict'; import H from '../../parts/Globals.js'; var win = H.win, doc = win.document; import U from '../../parts/Utilities.js'; var extend = U.extend, fireEvent = U.fireEvent, merge = U.merge; import HTMLUtilities from './utils/htmlUtilities.js'; var removeElement = HTMLUtilities.removeElement, getFakeMouseEvent = HTMLUtilities.getFakeMouseEvent; import ChartUtilities from './utils/chartUtilities.js'; var unhideChartElementFromAT = ChartUtilities.unhideChartElementFromAT; import EventProvider from './utils/EventProvider.js'; import DOMElementProvider from './utils/DOMElementProvider.js'; /* 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); export default AccessibilityComponent;