281 lines
9.2 KiB
JavaScript
281 lines
9.2 KiB
JavaScript
|
/* *
|
||
|
*
|
||
|
* (c) 2009-2020 Øystein Moseng
|
||
|
*
|
||
|
* Accessibility module for Highcharts
|
||
|
*
|
||
|
* License: www.highcharts.com/license
|
||
|
*
|
||
|
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
|
||
|
*
|
||
|
* */
|
||
|
'use strict';
|
||
|
import ChartUtilities from './utils/chartUtilities.js';
|
||
|
import H from '../../parts/Globals.js';
|
||
|
import KeyboardNavigationHandler from './KeyboardNavigationHandler.js';
|
||
|
import Point from '../../parts/Point.js';
|
||
|
import U from '../../parts/Utilities.js';
|
||
|
var addEvent = U.addEvent, extend = U.extend, fireEvent = U.fireEvent, merge = U.merge;
|
||
|
var doc = H.win.document;
|
||
|
import AccessibilityComponent from './AccessibilityComponent.js';
|
||
|
import KeyboardNavigation from './KeyboardNavigation.js';
|
||
|
import LegendComponent from './components/LegendComponent.js';
|
||
|
import MenuComponent from './components/MenuComponent.js';
|
||
|
import SeriesComponent from './components/SeriesComponent/SeriesComponent.js';
|
||
|
import ZoomComponent from './components/ZoomComponent.js';
|
||
|
import RangeSelectorComponent from './components/RangeSelectorComponent.js';
|
||
|
import InfoRegionsComponent from './components/InfoRegionsComponent.js';
|
||
|
import ContainerComponent from './components/ContainerComponent.js';
|
||
|
import whcm from './high-contrast-mode.js';
|
||
|
import highContrastTheme from './high-contrast-theme.js';
|
||
|
import defaultOptions from './options/options.js';
|
||
|
import defaultLangOptions from './options/langOptions.js';
|
||
|
import copyDeprecatedOptions from './options/deprecatedOptions.js';
|
||
|
import './a11y-i18n.js';
|
||
|
import './focusBorder.js';
|
||
|
// Add default options
|
||
|
merge(true, H.defaultOptions, defaultOptions, {
|
||
|
accessibility: {
|
||
|
highContrastTheme: highContrastTheme
|
||
|
},
|
||
|
lang: defaultLangOptions
|
||
|
});
|
||
|
// Expose functionality on Highcharts namespace
|
||
|
H.A11yChartUtilities = ChartUtilities;
|
||
|
H.KeyboardNavigationHandler = KeyboardNavigationHandler;
|
||
|
H.AccessibilityComponent = AccessibilityComponent;
|
||
|
/* eslint-disable no-invalid-this, valid-jsdoc */
|
||
|
/**
|
||
|
* The Accessibility class
|
||
|
*
|
||
|
* @private
|
||
|
* @requires module:modules/accessibility
|
||
|
*
|
||
|
* @class
|
||
|
* @name Highcharts.Accessibility
|
||
|
*
|
||
|
* @param {Highcharts.Chart} chart
|
||
|
* Chart object
|
||
|
*/
|
||
|
function Accessibility(chart) {
|
||
|
this.init(chart);
|
||
|
}
|
||
|
Accessibility.prototype = {
|
||
|
/**
|
||
|
* Initialize the accessibility class
|
||
|
* @private
|
||
|
* @param {Highcharts.Chart} chart
|
||
|
* Chart object
|
||
|
*/
|
||
|
init: function (chart) {
|
||
|
this.chart = chart;
|
||
|
// Abort on old browsers
|
||
|
if (!doc.addEventListener || !chart.renderer.isSVG) {
|
||
|
chart.renderTo.setAttribute('aria-hidden', true);
|
||
|
return;
|
||
|
}
|
||
|
// Copy over any deprecated options that are used. We could do this on
|
||
|
// every update, but it is probably not needed.
|
||
|
copyDeprecatedOptions(chart);
|
||
|
this.initComponents();
|
||
|
this.keyboardNavigation = new KeyboardNavigation(chart, this.components);
|
||
|
this.update();
|
||
|
},
|
||
|
/**
|
||
|
* @private
|
||
|
*/
|
||
|
initComponents: function () {
|
||
|
var chart = this.chart, a11yOptions = chart.options.accessibility;
|
||
|
this.components = {
|
||
|
container: new ContainerComponent(),
|
||
|
infoRegions: new InfoRegionsComponent(),
|
||
|
legend: new LegendComponent(),
|
||
|
chartMenu: new MenuComponent(),
|
||
|
rangeSelector: new RangeSelectorComponent(),
|
||
|
series: new SeriesComponent(),
|
||
|
zoom: new ZoomComponent()
|
||
|
};
|
||
|
if (a11yOptions.customComponents) {
|
||
|
extend(this.components, a11yOptions.customComponents);
|
||
|
}
|
||
|
var components = this.components;
|
||
|
this.getComponentOrder().forEach(function (componentName) {
|
||
|
components[componentName].initBase(chart);
|
||
|
components[componentName].init();
|
||
|
});
|
||
|
},
|
||
|
/**
|
||
|
* Get order to update components in.
|
||
|
* @private
|
||
|
*/
|
||
|
getComponentOrder: function () {
|
||
|
if (!this.components) {
|
||
|
return []; // For zombie accessibility object on old browsers
|
||
|
}
|
||
|
if (!this.components.series) {
|
||
|
return Object.keys(this.components);
|
||
|
}
|
||
|
var componentsExceptSeries = Object.keys(this.components)
|
||
|
.filter(function (c) { return c !== 'series'; });
|
||
|
// Update series first, so that other components can read accessibility
|
||
|
// info on points.
|
||
|
return ['series'].concat(componentsExceptSeries);
|
||
|
},
|
||
|
/**
|
||
|
* Update all components.
|
||
|
*/
|
||
|
update: function () {
|
||
|
var components = this.components, chart = this.chart, a11yOptions = chart.options.accessibility;
|
||
|
fireEvent(chart, 'beforeA11yUpdate');
|
||
|
// Update the chart type list as this is used by multiple modules
|
||
|
chart.types = this.getChartTypes();
|
||
|
// Update markup
|
||
|
this.getComponentOrder().forEach(function (componentName) {
|
||
|
components[componentName].onChartUpdate();
|
||
|
fireEvent(chart, 'afterA11yComponentUpdate', {
|
||
|
name: componentName,
|
||
|
component: components[componentName]
|
||
|
});
|
||
|
});
|
||
|
// Update keyboard navigation
|
||
|
this.keyboardNavigation.update(a11yOptions.keyboardNavigation.order);
|
||
|
// Handle high contrast mode
|
||
|
if (!chart.highContrastModeActive && // Only do this once
|
||
|
whcm.isHighContrastModeActive()) {
|
||
|
whcm.setHighContrastTheme(chart);
|
||
|
}
|
||
|
fireEvent(chart, 'afterA11yUpdate', {
|
||
|
accessibility: this
|
||
|
});
|
||
|
},
|
||
|
/**
|
||
|
* Destroy all elements.
|
||
|
*/
|
||
|
destroy: function () {
|
||
|
var chart = this.chart || {};
|
||
|
// Destroy components
|
||
|
var components = this.components;
|
||
|
Object.keys(components).forEach(function (componentName) {
|
||
|
components[componentName].destroy();
|
||
|
components[componentName].destroyBase();
|
||
|
});
|
||
|
// Kill keyboard nav
|
||
|
if (this.keyboardNavigation) {
|
||
|
this.keyboardNavigation.destroy();
|
||
|
}
|
||
|
// Hide container from screen readers if it exists
|
||
|
if (chart.renderTo) {
|
||
|
chart.renderTo.setAttribute('aria-hidden', true);
|
||
|
}
|
||
|
// Remove focus border if it exists
|
||
|
if (chart.focusElement) {
|
||
|
chart.focusElement.removeFocusBorder();
|
||
|
}
|
||
|
},
|
||
|
/**
|
||
|
* Return a list of the types of series we have in the chart.
|
||
|
* @private
|
||
|
*/
|
||
|
getChartTypes: function () {
|
||
|
var types = {};
|
||
|
this.chart.series.forEach(function (series) {
|
||
|
types[series.type] = 1;
|
||
|
});
|
||
|
return Object.keys(types);
|
||
|
}
|
||
|
};
|
||
|
/**
|
||
|
* @private
|
||
|
*/
|
||
|
H.Chart.prototype.updateA11yEnabled = function () {
|
||
|
var a11y = this.accessibility, accessibilityOptions = this.options.accessibility;
|
||
|
if (accessibilityOptions && accessibilityOptions.enabled) {
|
||
|
if (a11y) {
|
||
|
a11y.update();
|
||
|
}
|
||
|
else {
|
||
|
this.accessibility = a11y = new Accessibility(this);
|
||
|
}
|
||
|
}
|
||
|
else if (a11y) {
|
||
|
// Destroy if after update we have a11y and it is disabled
|
||
|
if (a11y.destroy) {
|
||
|
a11y.destroy();
|
||
|
}
|
||
|
delete this.accessibility;
|
||
|
}
|
||
|
else {
|
||
|
// Just hide container
|
||
|
this.renderTo.setAttribute('aria-hidden', true);
|
||
|
}
|
||
|
};
|
||
|
// Handle updates to the module and send render updates to components
|
||
|
addEvent(H.Chart, 'render', function (e) {
|
||
|
// Update/destroy
|
||
|
if (this.a11yDirty && this.renderTo) {
|
||
|
delete this.a11yDirty;
|
||
|
this.updateA11yEnabled();
|
||
|
}
|
||
|
var a11y = this.accessibility;
|
||
|
if (a11y) {
|
||
|
a11y.getComponentOrder().forEach(function (componentName) {
|
||
|
a11y.components[componentName].onChartRender();
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
// Update with chart/series/point updates
|
||
|
addEvent(H.Chart, 'update', function (e) {
|
||
|
// Merge new options
|
||
|
var newOptions = e.options.accessibility;
|
||
|
if (newOptions) {
|
||
|
// Handle custom component updating specifically
|
||
|
if (newOptions.customComponents) {
|
||
|
this.options.accessibility.customComponents =
|
||
|
newOptions.customComponents;
|
||
|
delete newOptions.customComponents;
|
||
|
}
|
||
|
merge(true, this.options.accessibility, newOptions);
|
||
|
// Recreate from scratch
|
||
|
if (this.accessibility && this.accessibility.destroy) {
|
||
|
this.accessibility.destroy();
|
||
|
delete this.accessibility;
|
||
|
}
|
||
|
}
|
||
|
// Mark dirty for update
|
||
|
this.a11yDirty = true;
|
||
|
});
|
||
|
// Mark dirty for update
|
||
|
addEvent(Point, 'update', function () {
|
||
|
if (this.series.chart.accessibility) {
|
||
|
this.series.chart.a11yDirty = true;
|
||
|
}
|
||
|
});
|
||
|
['addSeries', 'init'].forEach(function (event) {
|
||
|
addEvent(H.Chart, event, function () {
|
||
|
this.a11yDirty = true;
|
||
|
});
|
||
|
});
|
||
|
['update', 'updatedData', 'remove'].forEach(function (event) {
|
||
|
addEvent(H.Series, event, function () {
|
||
|
if (this.chart.accessibility) {
|
||
|
this.chart.a11yDirty = true;
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
// Direct updates (events happen after render)
|
||
|
[
|
||
|
'afterDrilldown', 'drillupall'
|
||
|
].forEach(function (event) {
|
||
|
addEvent(H.Chart, event, function () {
|
||
|
if (this.accessibility) {
|
||
|
this.accessibility.update();
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
// Destroy with chart
|
||
|
addEvent(H.Chart, 'destroy', function () {
|
||
|
if (this.accessibility) {
|
||
|
this.accessibility.destroy();
|
||
|
}
|
||
|
});
|