380 lines
11 KiB
JavaScript
380 lines
11 KiB
JavaScript
/* *
|
|
*
|
|
* (c) 2009-2019 Øystein Moseng
|
|
*
|
|
* Accessibility component for chart info region and table.
|
|
*
|
|
* License: www.highcharts.com/license
|
|
*
|
|
* */
|
|
|
|
'use strict';
|
|
|
|
import H from '../../../parts/Globals.js';
|
|
import AccessibilityComponent from '../AccessibilityComponent.js';
|
|
|
|
var merge = H.merge,
|
|
pick = H.pick;
|
|
|
|
|
|
/**
|
|
* Return simplified text description of chart type. Some types will not be
|
|
* familiar to most users, but in those cases we try to add a description of the
|
|
* type.
|
|
*
|
|
* @private
|
|
* @function Highcharts.Chart#getTypeDescription
|
|
* @param {Array<string>} types The series types in this chart.
|
|
* @return {string} The text description of the chart type.
|
|
*/
|
|
H.Chart.prototype.getTypeDescription = function (types) {
|
|
var firstType = types[0],
|
|
firstSeries = this.series && this.series[0] || {},
|
|
mapTitle = firstSeries.mapTitle,
|
|
formatContext = {
|
|
numSeries: this.series.length,
|
|
numPoints: firstSeries.points && firstSeries.points.length,
|
|
chart: this,
|
|
mapTitle: mapTitle
|
|
};
|
|
|
|
if (!firstType) {
|
|
return this.langFormat(
|
|
'accessibility.chartTypes.emptyChart', formatContext
|
|
);
|
|
}
|
|
|
|
if (firstType === 'map') {
|
|
return mapTitle ?
|
|
this.langFormat(
|
|
'accessibility.chartTypes.mapTypeDescription',
|
|
formatContext
|
|
) :
|
|
this.langFormat(
|
|
'accessibility.chartTypes.unknownMap',
|
|
formatContext
|
|
);
|
|
}
|
|
|
|
if (this.types.length > 1) {
|
|
return this.langFormat(
|
|
'accessibility.chartTypes.combinationChart', formatContext
|
|
);
|
|
}
|
|
|
|
var typeDesc = this.langFormat(
|
|
'accessibility.seriesTypeDescriptions.' + firstType,
|
|
{ chart: this }
|
|
),
|
|
multi = this.series && this.series.length === 1 ? 'Single' : 'Multiple';
|
|
|
|
return (
|
|
this.langFormat(
|
|
'accessibility.chartTypes.' + firstType + multi,
|
|
formatContext
|
|
) ||
|
|
this.langFormat(
|
|
'accessibility.chartTypes.default' + multi,
|
|
formatContext
|
|
)
|
|
) +
|
|
(typeDesc ? ' ' + typeDesc : '');
|
|
};
|
|
|
|
|
|
/**
|
|
* The InfoRegionComponent class
|
|
*
|
|
* @private
|
|
* @class
|
|
* @name Highcharts.InfoRegionComponent
|
|
* @param {Highcharts.Chart} chart
|
|
* Chart object
|
|
*/
|
|
var InfoRegionComponent = function (chart) {
|
|
this.initBase(chart);
|
|
this.init();
|
|
};
|
|
InfoRegionComponent.prototype = new AccessibilityComponent();
|
|
H.extend(InfoRegionComponent.prototype, /** @lends Highcharts.InfoRegionComponent */ { // eslint-disable-line
|
|
|
|
/**
|
|
* Init the component
|
|
* @private
|
|
*/
|
|
init: function () {
|
|
// Add ID and summary attr to table HTML
|
|
var chart = this.chart,
|
|
component = this;
|
|
this.addEvent(chart, 'afterGetTable', function (e) {
|
|
if (chart.options.accessibility.enabled) {
|
|
component.tableAnchor.setAttribute('aria-expanded', true);
|
|
e.html = e.html
|
|
.replace(
|
|
'<table ',
|
|
'<table tabindex="0" summary="' + chart.langFormat(
|
|
'accessibility.tableSummary', { chart: chart }
|
|
) + '"'
|
|
);
|
|
}
|
|
});
|
|
|
|
// Focus table after viewing
|
|
this.addEvent(chart, 'afterViewData', function (tableDiv) {
|
|
// Use small delay to give browsers & AT time to register new table
|
|
setTimeout(function () {
|
|
var table = tableDiv &&
|
|
tableDiv.getElementsByTagName('table')[0];
|
|
if (table && table.focus) {
|
|
table.focus();
|
|
}
|
|
}, 300);
|
|
});
|
|
},
|
|
|
|
|
|
/**
|
|
* Called on first render/updates to the chart, including options changes.
|
|
*/
|
|
onChartUpdate: function () {
|
|
// Create/update the screen reader region
|
|
var chart = this.chart,
|
|
a11yOptions = chart.options.accessibility,
|
|
hiddenSectionId = 'highcharts-information-region-' + chart.index,
|
|
hiddenSection = this.screenReaderRegion =
|
|
this.screenReaderRegion || this.createElement('div'),
|
|
tableShortcut = this.tableHeading =
|
|
this.tableHeading || this.createElement('h6'),
|
|
tableShortcutAnchor = this.tableAnchor =
|
|
this.tableAnchor || this.createElement('a'),
|
|
chartHeading = this.chartHeading =
|
|
this.chartHeading || this.createElement('h6');
|
|
|
|
hiddenSection.setAttribute('id', hiddenSectionId);
|
|
if (a11yOptions.landmarkVerbosity === 'all') {
|
|
hiddenSection.setAttribute('role', 'region');
|
|
}
|
|
hiddenSection.setAttribute(
|
|
'aria-label',
|
|
chart.langFormat(
|
|
'accessibility.screenReaderRegionLabel', { chart: chart }
|
|
)
|
|
);
|
|
|
|
hiddenSection.innerHTML = a11yOptions.screenReaderSectionFormatter ?
|
|
a11yOptions.screenReaderSectionFormatter(chart) :
|
|
this.defaultScreenReaderSectionFormatter(chart);
|
|
|
|
// Add shortcut to data table if export-data is loaded
|
|
if (chart.getCSV && chart.options.accessibility.addTableShortcut) {
|
|
var tableId = 'highcharts-data-table-' + chart.index;
|
|
tableShortcutAnchor.innerHTML = chart.langFormat(
|
|
'accessibility.viewAsDataTable', { chart: chart }
|
|
);
|
|
tableShortcutAnchor.href = '#' + tableId;
|
|
// Make this unreachable by user tabbing
|
|
tableShortcutAnchor.setAttribute('tabindex', '-1');
|
|
tableShortcutAnchor.setAttribute('role', 'button');
|
|
tableShortcutAnchor.setAttribute('aria-expanded', false);
|
|
tableShortcutAnchor.onclick =
|
|
chart.options.accessibility.onTableAnchorClick || function () {
|
|
chart.viewData();
|
|
};
|
|
tableShortcut.appendChild(tableShortcutAnchor);
|
|
hiddenSection.appendChild(tableShortcut);
|
|
}
|
|
|
|
// Note: JAWS seems to refuse to read aria-label on the container, so
|
|
// add an h6 element as title for the chart.
|
|
chartHeading.innerHTML = chart.langFormat(
|
|
'accessibility.chartHeading', { chart: chart }
|
|
);
|
|
chartHeading.setAttribute('aria-hidden', false);
|
|
chart.renderTo.insertBefore(chartHeading, chart.renderTo.firstChild);
|
|
chart.renderTo.insertBefore(hiddenSection, chart.renderTo.firstChild);
|
|
this.unhideElementFromScreenReaders(hiddenSection);
|
|
|
|
// Visually hide the section and the chart heading
|
|
merge(true, chartHeading.style, this.hiddenStyle);
|
|
merge(true, hiddenSection.style, this.hiddenStyle);
|
|
},
|
|
|
|
|
|
/**
|
|
* The default formatter for the screen reader section.
|
|
* @private
|
|
*/
|
|
defaultScreenReaderSectionFormatter: function () {
|
|
var chart = this.chart,
|
|
options = chart.options,
|
|
chartTypes = chart.types,
|
|
axesDesc = this.getAxesDescription();
|
|
|
|
return '<h5>' +
|
|
(
|
|
options.accessibility.typeDescription ||
|
|
chart.getTypeDescription(chartTypes)
|
|
) + '</h5>' + (
|
|
options.subtitle && options.subtitle.text ?
|
|
'<div>' + this.htmlencode(options.subtitle.text) + '</div>' : ''
|
|
) + (
|
|
options.accessibility.description ?
|
|
'<div>' + options.accessibility.description + '</div>' : ''
|
|
) + (axesDesc.xAxis ? (
|
|
'<div>' + axesDesc.xAxis + '</div>'
|
|
) : '') +
|
|
(axesDesc.yAxis ? (
|
|
'<div>' + axesDesc.yAxis + '</div>'
|
|
) : '');
|
|
},
|
|
|
|
|
|
/**
|
|
* Return object with text description of each of the chart's axes.
|
|
* @private
|
|
* @return {object}
|
|
*/
|
|
getAxesDescription: function () {
|
|
var chart = this.chart,
|
|
component = this,
|
|
xAxes = chart.xAxis,
|
|
// Figure out when to show axis info in the region
|
|
showXAxes = xAxes.length > 1 || xAxes[0] &&
|
|
pick(
|
|
xAxes[0].options.accessibility &&
|
|
xAxes[0].options.accessibility.enabled,
|
|
!chart.angular && chart.hasCartesianSeries &&
|
|
chart.types.indexOf('map') < 0
|
|
),
|
|
yAxes = chart.yAxis,
|
|
showYAxes = yAxes.length > 1 || yAxes[0] &&
|
|
pick(
|
|
yAxes[0].options.accessibility &&
|
|
yAxes[0].options.accessibility.enabled,
|
|
chart.hasCartesianSeries && chart.types.indexOf('map') < 0
|
|
),
|
|
desc = {};
|
|
|
|
if (showXAxes) {
|
|
desc.xAxis = chart.langFormat(
|
|
'accessibility.axis.xAxisDescription' + (
|
|
xAxes.length > 1 ? 'Plural' : 'Singular'
|
|
),
|
|
{
|
|
chart: chart,
|
|
names: chart.xAxis.map(function (axis) {
|
|
return axis.getDescription();
|
|
}),
|
|
ranges: chart.xAxis.map(function (axis) {
|
|
return component.getAxisRangeDescription(axis);
|
|
}),
|
|
numAxes: xAxes.length
|
|
}
|
|
);
|
|
}
|
|
|
|
if (showYAxes) {
|
|
desc.yAxis = chart.langFormat(
|
|
'accessibility.axis.yAxisDescription' + (
|
|
yAxes.length > 1 ? 'Plural' : 'Singular'
|
|
),
|
|
{
|
|
chart: chart,
|
|
names: chart.yAxis.map(function (axis) {
|
|
return axis.getDescription();
|
|
}),
|
|
ranges: chart.yAxis.map(function (axis) {
|
|
return component.getAxisRangeDescription(axis);
|
|
}),
|
|
numAxes: yAxes.length
|
|
}
|
|
);
|
|
}
|
|
|
|
return desc;
|
|
},
|
|
|
|
|
|
/**
|
|
* Return string with text description of the axis range.
|
|
* @private
|
|
* @param {Highcharts.Axis} axis The axis to get range desc of.
|
|
* @return {string} A string with the range description for the axis.
|
|
*/
|
|
getAxisRangeDescription: function (axis) {
|
|
var chart = this.chart,
|
|
axisOptions = axis.options || {};
|
|
|
|
// Handle overridden range description
|
|
if (
|
|
axisOptions.accessibility &&
|
|
axisOptions.accessibility.rangeDescription !== undefined
|
|
) {
|
|
return axisOptions.accessibility.rangeDescription;
|
|
}
|
|
|
|
// Handle category axes
|
|
if (axis.categories) {
|
|
return chart.langFormat(
|
|
'accessibility.axis.rangeCategories',
|
|
{
|
|
chart: chart,
|
|
axis: axis,
|
|
numCategories: axis.dataMax - axis.dataMin + 1
|
|
}
|
|
);
|
|
}
|
|
|
|
// Use range, not from-to?
|
|
if (axis.isDatetimeAxis && (axis.min === 0 || axis.dataMin === 0)) {
|
|
var range = {},
|
|
rangeUnit = 'Seconds';
|
|
range.Seconds = (axis.max - axis.min) / 1000;
|
|
range.Minutes = range.Seconds / 60;
|
|
range.Hours = range.Minutes / 60;
|
|
range.Days = range.Hours / 24;
|
|
['Minutes', 'Hours', 'Days'].forEach(function (unit) {
|
|
if (range[unit] > 2) {
|
|
rangeUnit = unit;
|
|
}
|
|
});
|
|
range.value = range[rangeUnit].toFixed(
|
|
rangeUnit !== 'Seconds' &&
|
|
rangeUnit !== 'Minutes' ? 1 : 0 // Use decimals for days/hours
|
|
);
|
|
|
|
// We have the range and the unit to use, find the desc format
|
|
return chart.langFormat(
|
|
'accessibility.axis.timeRange' + rangeUnit,
|
|
{
|
|
chart: chart,
|
|
axis: axis,
|
|
range: range.value.replace('.0', '')
|
|
}
|
|
);
|
|
}
|
|
|
|
// Just use from and to.
|
|
// We have the range and the unit to use, find the desc format
|
|
var a11yOptions = chart.options.accessibility;
|
|
return chart.langFormat(
|
|
'accessibility.axis.rangeFromTo',
|
|
{
|
|
chart: chart,
|
|
axis: axis,
|
|
rangeFrom: axis.isDatetimeAxis ?
|
|
chart.time.dateFormat(
|
|
a11yOptions.axisRangeDateFormat, axis.min
|
|
) : axis.min,
|
|
rangeTo: axis.isDatetimeAxis ?
|
|
chart.time.dateFormat(
|
|
a11yOptions.axisRangeDateFormat, axis.max
|
|
) : axis.max
|
|
}
|
|
);
|
|
}
|
|
|
|
});
|
|
|
|
export default InfoRegionComponent;
|