391 lines
12 KiB
JavaScript
391 lines
12 KiB
JavaScript
|
/* *
|
||
|
*
|
||
|
* (c) 2009-2024 Øystein Moseng
|
||
|
*
|
||
|
* Accessibility module for Highcharts
|
||
|
*
|
||
|
* License: www.highcharts.com/license
|
||
|
*
|
||
|
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
|
||
|
*
|
||
|
* */
|
||
|
'use strict';
|
||
|
import D from '../Core/Defaults.js';
|
||
|
const { defaultOptions } = D;
|
||
|
import H from '../Core/Globals.js';
|
||
|
const { doc } = H;
|
||
|
import U from '../Core/Utilities.js';
|
||
|
const { addEvent, extend, fireEvent, merge } = U;
|
||
|
import HU from './Utils/HTMLUtilities.js';
|
||
|
const { removeElement } = HU;
|
||
|
import A11yI18n from './A11yI18n.js';
|
||
|
import ContainerComponent from './Components/ContainerComponent.js';
|
||
|
import FocusBorderComposition from './FocusBorder.js';
|
||
|
import InfoRegionsComponent from './Components/InfoRegionsComponent.js';
|
||
|
import KeyboardNavigation from './KeyboardNavigation.js';
|
||
|
import LegendComponent from './Components/LegendComponent.js';
|
||
|
import MenuComponent from './Components/MenuComponent.js';
|
||
|
import NavigatorComponent from './Components/NavigatorComponent.js';
|
||
|
import NewDataAnnouncer from './Components/SeriesComponent/NewDataAnnouncer.js';
|
||
|
import ProxyProvider from './ProxyProvider.js';
|
||
|
import RangeSelectorComponent from './Components/RangeSelectorComponent.js';
|
||
|
import SeriesComponent from './Components/SeriesComponent/SeriesComponent.js';
|
||
|
import ZoomComponent from './Components/ZoomComponent.js';
|
||
|
import whcm from './HighContrastMode.js';
|
||
|
import highContrastTheme from './HighContrastTheme.js';
|
||
|
import defaultOptionsA11Y from './Options/A11yDefaults.js';
|
||
|
import defaultLangOptions from './Options/LangDefaults.js';
|
||
|
import copyDeprecatedOptions from './Options/DeprecatedOptions.js';
|
||
|
/* *
|
||
|
*
|
||
|
* Class
|
||
|
*
|
||
|
* */
|
||
|
/**
|
||
|
* The Accessibility class
|
||
|
*
|
||
|
* @private
|
||
|
* @requires module:modules/accessibility
|
||
|
*
|
||
|
* @class
|
||
|
* @name Highcharts.Accessibility
|
||
|
*
|
||
|
* @param {Highcharts.Chart} chart
|
||
|
* Chart object
|
||
|
*/
|
||
|
class Accessibility {
|
||
|
/* *
|
||
|
*
|
||
|
* Constructor
|
||
|
*
|
||
|
* */
|
||
|
constructor(chart) {
|
||
|
this.init(chart);
|
||
|
}
|
||
|
/* *
|
||
|
*
|
||
|
* Functions
|
||
|
*
|
||
|
* */
|
||
|
/**
|
||
|
* Initialize the accessibility class
|
||
|
* @private
|
||
|
* @param {Highcharts.Chart} chart
|
||
|
* Chart object
|
||
|
*/
|
||
|
init(chart) {
|
||
|
this.chart = chart;
|
||
|
// Abort on old browsers
|
||
|
if (!doc.addEventListener) {
|
||
|
this.zombie = true;
|
||
|
this.components = {};
|
||
|
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.proxyProvider = new ProxyProvider(this.chart);
|
||
|
this.initComponents();
|
||
|
this.keyboardNavigation = new KeyboardNavigation(chart, this.components);
|
||
|
}
|
||
|
/**
|
||
|
* @private
|
||
|
*/
|
||
|
initComponents() {
|
||
|
const chart = this.chart;
|
||
|
const proxyProvider = this.proxyProvider;
|
||
|
const 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(),
|
||
|
navigator: new NavigatorComponent()
|
||
|
};
|
||
|
if (a11yOptions.customComponents) {
|
||
|
extend(this.components, a11yOptions.customComponents);
|
||
|
}
|
||
|
const components = this.components;
|
||
|
this.getComponentOrder().forEach(function (componentName) {
|
||
|
components[componentName].initBase(chart, proxyProvider);
|
||
|
components[componentName].init();
|
||
|
});
|
||
|
}
|
||
|
/**
|
||
|
* Get order to update components in.
|
||
|
* @private
|
||
|
*/
|
||
|
getComponentOrder() {
|
||
|
if (!this.components) {
|
||
|
return []; // For zombie accessibility object on old browsers
|
||
|
}
|
||
|
if (!this.components.series) {
|
||
|
return Object.keys(this.components);
|
||
|
}
|
||
|
const componentsExceptSeries = Object.keys(this.components)
|
||
|
.filter((c) => c !== 'series');
|
||
|
// Update series first, so that other components can read accessibility
|
||
|
// info on points.
|
||
|
return ['series'].concat(componentsExceptSeries);
|
||
|
}
|
||
|
/**
|
||
|
* Update all components.
|
||
|
*/
|
||
|
update() {
|
||
|
const 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 proxies. We don't update proxy positions since most likely we
|
||
|
// need to recreate the proxies on update.
|
||
|
const kbdNavOrder = a11yOptions.keyboardNavigation.order;
|
||
|
this.proxyProvider.updateGroupOrder(kbdNavOrder);
|
||
|
// Update markup
|
||
|
this.getComponentOrder().forEach(function (componentName) {
|
||
|
components[componentName].onChartUpdate();
|
||
|
fireEvent(chart, 'afterA11yComponentUpdate', {
|
||
|
name: componentName,
|
||
|
component: components[componentName]
|
||
|
});
|
||
|
});
|
||
|
// Update keyboard navigation
|
||
|
this.keyboardNavigation.update(kbdNavOrder);
|
||
|
// Handle high contrast mode
|
||
|
// Should only be applied once, and not if explicitly disabled
|
||
|
if (!chart.highContrastModeActive &&
|
||
|
a11yOptions.highContrastMode !== false && (whcm.isHighContrastModeActive() ||
|
||
|
a11yOptions.highContrastMode === true)) {
|
||
|
whcm.setHighContrastTheme(chart);
|
||
|
}
|
||
|
fireEvent(chart, 'afterA11yUpdate', {
|
||
|
accessibility: this
|
||
|
});
|
||
|
}
|
||
|
/**
|
||
|
* Destroy all elements.
|
||
|
*/
|
||
|
destroy() {
|
||
|
const chart = this.chart || {};
|
||
|
// Destroy components
|
||
|
const components = this.components;
|
||
|
Object.keys(components).forEach(function (componentName) {
|
||
|
components[componentName].destroy();
|
||
|
components[componentName].destroyBase();
|
||
|
});
|
||
|
// Destroy proxy provider
|
||
|
if (this.proxyProvider) {
|
||
|
this.proxyProvider.destroy();
|
||
|
}
|
||
|
// Remove announcer container
|
||
|
if (chart.announcerContainer) {
|
||
|
removeElement(chart.announcerContainer);
|
||
|
}
|
||
|
// 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() {
|
||
|
const types = {};
|
||
|
this.chart.series.forEach(function (series) {
|
||
|
types[series.type] = 1;
|
||
|
});
|
||
|
return Object.keys(types);
|
||
|
}
|
||
|
}
|
||
|
/* *
|
||
|
*
|
||
|
* Class Namespace
|
||
|
*
|
||
|
* */
|
||
|
(function (Accessibility) {
|
||
|
/* *
|
||
|
*
|
||
|
* Declarations
|
||
|
*
|
||
|
* */
|
||
|
/* *
|
||
|
*
|
||
|
* Constants
|
||
|
*
|
||
|
* */
|
||
|
Accessibility.i18nFormat = A11yI18n.i18nFormat;
|
||
|
/* *
|
||
|
*
|
||
|
* Functions
|
||
|
*
|
||
|
* */
|
||
|
/**
|
||
|
* Destroy with chart.
|
||
|
* @private
|
||
|
*/
|
||
|
function chartOnDestroy() {
|
||
|
if (this.accessibility) {
|
||
|
this.accessibility.destroy();
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* Handle updates to the module and send render updates to components.
|
||
|
* @private
|
||
|
*/
|
||
|
function chartOnRender() {
|
||
|
// Update/destroy
|
||
|
if (this.a11yDirty && this.renderTo) {
|
||
|
delete this.a11yDirty;
|
||
|
this.updateA11yEnabled();
|
||
|
}
|
||
|
const a11y = this.accessibility;
|
||
|
if (a11y && !a11y.zombie) {
|
||
|
a11y.proxyProvider.updateProxyElementPositions();
|
||
|
a11y.getComponentOrder().forEach(function (componentName) {
|
||
|
a11y.components[componentName].onChartRender();
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* Update with chart/series/point updates.
|
||
|
* @private
|
||
|
*/
|
||
|
function chartOnUpdate(e) {
|
||
|
// Merge new options
|
||
|
const 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;
|
||
|
}
|
||
|
/**
|
||
|
* @private
|
||
|
*/
|
||
|
function chartUpdateA11yEnabled() {
|
||
|
let a11y = this.accessibility;
|
||
|
const accessibilityOptions = this.options.accessibility;
|
||
|
if (accessibilityOptions && accessibilityOptions.enabled) {
|
||
|
if (a11y && !a11y.zombie) {
|
||
|
a11y.update();
|
||
|
}
|
||
|
else {
|
||
|
this.accessibility = a11y = new Accessibility(this);
|
||
|
if (a11y && !a11y.zombie) {
|
||
|
a11y.update();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
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);
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* @private
|
||
|
*/
|
||
|
function compose(ChartClass, LegendClass, PointClass, SeriesClass, SVGElementClass, RangeSelectorClass) {
|
||
|
// Ordered:
|
||
|
KeyboardNavigation.compose(ChartClass);
|
||
|
NewDataAnnouncer.compose(SeriesClass);
|
||
|
LegendComponent.compose(ChartClass, LegendClass);
|
||
|
MenuComponent.compose(ChartClass);
|
||
|
SeriesComponent.compose(ChartClass, PointClass, SeriesClass);
|
||
|
A11yI18n.compose(ChartClass);
|
||
|
FocusBorderComposition.compose(ChartClass, SVGElementClass);
|
||
|
// RangeSelector
|
||
|
if (RangeSelectorClass) {
|
||
|
RangeSelectorComponent.compose(ChartClass, RangeSelectorClass);
|
||
|
}
|
||
|
const chartProto = ChartClass.prototype;
|
||
|
if (!chartProto.updateA11yEnabled) {
|
||
|
chartProto.updateA11yEnabled = chartUpdateA11yEnabled;
|
||
|
addEvent(ChartClass, 'destroy', chartOnDestroy);
|
||
|
addEvent(ChartClass, 'render', chartOnRender);
|
||
|
addEvent(ChartClass, 'update', chartOnUpdate);
|
||
|
// Mark dirty for update
|
||
|
['addSeries', 'init'].forEach((event) => {
|
||
|
addEvent(ChartClass, event, function () {
|
||
|
this.a11yDirty = true;
|
||
|
});
|
||
|
});
|
||
|
// Direct updates (events happen after render)
|
||
|
['afterApplyDrilldown', 'drillupall'].forEach((event) => {
|
||
|
addEvent(ChartClass, event, function chartOnAfterDrilldown() {
|
||
|
const a11y = this.accessibility;
|
||
|
if (a11y && !a11y.zombie) {
|
||
|
a11y.update();
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
addEvent(PointClass, 'update', pointOnUpdate);
|
||
|
// Mark dirty for update
|
||
|
['update', 'updatedData', 'remove'].forEach((event) => {
|
||
|
addEvent(SeriesClass, event, function () {
|
||
|
if (this.chart.accessibility) {
|
||
|
this.chart.a11yDirty = true;
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
Accessibility.compose = compose;
|
||
|
/**
|
||
|
* Mark dirty for update.
|
||
|
* @private
|
||
|
*/
|
||
|
function pointOnUpdate() {
|
||
|
if (this.series.chart.accessibility) {
|
||
|
this.series.chart.a11yDirty = true;
|
||
|
}
|
||
|
}
|
||
|
})(Accessibility || (Accessibility = {}));
|
||
|
/* *
|
||
|
*
|
||
|
* Registry
|
||
|
*
|
||
|
* */
|
||
|
// Add default options
|
||
|
merge(true, defaultOptions, defaultOptionsA11Y, {
|
||
|
accessibility: {
|
||
|
highContrastTheme: highContrastTheme
|
||
|
},
|
||
|
lang: defaultLangOptions
|
||
|
});
|
||
|
/* *
|
||
|
*
|
||
|
* Default Export
|
||
|
*
|
||
|
* */
|
||
|
export default Accessibility;
|