chore: version inicial del codigo fuente del LMS Estafeta
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
/* *
|
||||
*
|
||||
* (c) 2009-2019 Øystein Moseng
|
||||
*
|
||||
* Annotations accessibility code.
|
||||
*
|
||||
* License: www.highcharts.com/license
|
||||
*
|
||||
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
|
||||
*
|
||||
* */
|
||||
'use strict';
|
||||
import HTMLUtilities from '../Utils/HTMLUtilities.js';
|
||||
var escapeStringForHTML = HTMLUtilities.escapeStringForHTML, stripHTMLTagsFromString = HTMLUtilities.stripHTMLTagsFromString;
|
||||
/**
|
||||
* Get list of all annotation labels in the chart.
|
||||
*
|
||||
* @private
|
||||
* @param {Highcharts.Chart} chart The chart to get annotation info on.
|
||||
* @return {Array<object>} The labels, or empty array if none.
|
||||
*/
|
||||
function getChartAnnotationLabels(chart) {
|
||||
var annotations = chart.annotations || [];
|
||||
return annotations.reduce(function (acc, cur) {
|
||||
var _a;
|
||||
if (((_a = cur.options) === null || _a === void 0 ? void 0 : _a.visible) !== false) {
|
||||
acc = acc.concat(cur.labels);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
/**
|
||||
* Get the text of an annotation label.
|
||||
*
|
||||
* @private
|
||||
* @param {object} label The annotation label object
|
||||
* @return {string} The text in the label.
|
||||
*/
|
||||
function getLabelText(label) {
|
||||
var _a, _b, _c, _d;
|
||||
var a11yDesc = (_b = (_a = label.options) === null || _a === void 0 ? void 0 : _a.accessibility) === null || _b === void 0 ? void 0 : _b.description;
|
||||
return a11yDesc ? a11yDesc : ((_d = (_c = label.graphic) === null || _c === void 0 ? void 0 : _c.text) === null || _d === void 0 ? void 0 : _d.textStr) || '';
|
||||
}
|
||||
/**
|
||||
* Describe an annotation label.
|
||||
*
|
||||
* @private
|
||||
* @param {object} label The annotation label object to describe
|
||||
* @return {string} The description for the label.
|
||||
*/
|
||||
function getAnnotationLabelDescription(label) {
|
||||
var _a, _b;
|
||||
var a11yDesc = (_b = (_a = label.options) === null || _a === void 0 ? void 0 : _a.accessibility) === null || _b === void 0 ? void 0 : _b.description;
|
||||
if (a11yDesc) {
|
||||
return a11yDesc;
|
||||
}
|
||||
var chart = label.chart;
|
||||
var labelText = getLabelText(label);
|
||||
var points = label.points;
|
||||
var getAriaLabel = function (point) { var _a, _b; return ((_b = (_a = point === null || point === void 0 ? void 0 : point.graphic) === null || _a === void 0 ? void 0 : _a.element) === null || _b === void 0 ? void 0 : _b.getAttribute('aria-label')) || ''; };
|
||||
var getValueDesc = function (point) {
|
||||
var _a;
|
||||
var valDesc = ((_a = point === null || point === void 0 ? void 0 : point.accessibility) === null || _a === void 0 ? void 0 : _a.valueDescription) || getAriaLabel(point);
|
||||
var seriesName = (point === null || point === void 0 ? void 0 : point.series.name) || '';
|
||||
return (seriesName ? seriesName + ', ' : '') + 'data point ' + valDesc;
|
||||
};
|
||||
var pointValueDescriptions = points
|
||||
.filter(function (p) { return !!p.graphic; }) // Filter out mock points
|
||||
.map(getValueDesc)
|
||||
.filter(function (desc) { return !!desc; }); // Filter out points we can't describe
|
||||
var numPoints = pointValueDescriptions.length;
|
||||
var pointsSelector = numPoints > 1 ? 'MultiplePoints' : numPoints ? 'SinglePoint' : 'NoPoints';
|
||||
var langFormatStr = 'accessibility.screenReaderSection.annotations.description' + pointsSelector;
|
||||
var context = {
|
||||
annotationText: labelText,
|
||||
numPoints: numPoints,
|
||||
annotationPoint: pointValueDescriptions[0],
|
||||
additionalAnnotationPoints: pointValueDescriptions.slice(1)
|
||||
};
|
||||
return chart.langFormat(langFormatStr, context);
|
||||
}
|
||||
/**
|
||||
* Return array of HTML strings for each annotation label in the chart.
|
||||
*
|
||||
* @private
|
||||
* @param {Highcharts.Chart} chart The chart to get annotation info on.
|
||||
* @return {Array<string>} Array of strings with HTML content for each annotation label.
|
||||
*/
|
||||
function getAnnotationListItems(chart) {
|
||||
var labels = getChartAnnotationLabels(chart);
|
||||
return labels.map(function (label) {
|
||||
var desc = escapeStringForHTML(stripHTMLTagsFromString(getAnnotationLabelDescription(label)));
|
||||
return desc ? "<li>" + desc + "</li>" : '';
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Return the annotation info for a chart as string.
|
||||
*
|
||||
* @private
|
||||
* @param {Highcharts.Chart} chart The chart to get annotation info on.
|
||||
* @return {string} String with HTML content or empty string if no annotations.
|
||||
*/
|
||||
function getAnnotationsInfoHTML(chart) {
|
||||
var annotations = chart.annotations;
|
||||
if (!(annotations && annotations.length)) {
|
||||
return '';
|
||||
}
|
||||
var annotationItems = getAnnotationListItems(chart);
|
||||
return "<ul>" + annotationItems.join(' ') + "</ul>";
|
||||
}
|
||||
/**
|
||||
* Return the texts for the annotation(s) connected to a point, or empty array
|
||||
* if none.
|
||||
*
|
||||
* @private
|
||||
* @param {Highcharts.Point} point The data point to get the annotation info from.
|
||||
* @return {Array<string>} Annotation texts
|
||||
*/
|
||||
function getPointAnnotationTexts(point) {
|
||||
var labels = getChartAnnotationLabels(point.series.chart);
|
||||
var pointLabels = labels
|
||||
.filter(function (label) { return label.points.indexOf(point) > -1; });
|
||||
if (!pointLabels.length) {
|
||||
return [];
|
||||
}
|
||||
return pointLabels.map(function (label) { return "" + getLabelText(label); });
|
||||
}
|
||||
var AnnotationsA11y = {
|
||||
getAnnotationsInfoHTML: getAnnotationsInfoHTML,
|
||||
getAnnotationLabelDescription: getAnnotationLabelDescription,
|
||||
getAnnotationListItems: getAnnotationListItems,
|
||||
getPointAnnotationTexts: getPointAnnotationTexts
|
||||
};
|
||||
export default AnnotationsA11y;
|
||||
@@ -0,0 +1,115 @@
|
||||
/* *
|
||||
*
|
||||
* (c) 2009-2020 Øystein Moseng
|
||||
*
|
||||
* Accessibility component for chart container.
|
||||
*
|
||||
* License: www.highcharts.com/license
|
||||
*
|
||||
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
|
||||
*
|
||||
* */
|
||||
'use strict';
|
||||
import H from '../../Core/Globals.js';
|
||||
var doc = H.win.document;
|
||||
import U from '../../Core/Utilities.js';
|
||||
var extend = U.extend;
|
||||
import HTMLUtilities from '../Utils/HTMLUtilities.js';
|
||||
var stripHTMLTags = HTMLUtilities.stripHTMLTagsFromString;
|
||||
import ChartUtilities from '../Utils/ChartUtilities.js';
|
||||
var unhideChartElementFromAT = ChartUtilities.unhideChartElementFromAT, getChartTitle = ChartUtilities.getChartTitle;
|
||||
import AccessibilityComponent from '../AccessibilityComponent.js';
|
||||
/* eslint-disable valid-jsdoc */
|
||||
/**
|
||||
* The ContainerComponent class
|
||||
*
|
||||
* @private
|
||||
* @class
|
||||
* @name Highcharts.ContainerComponent
|
||||
*/
|
||||
var ContainerComponent = function () { };
|
||||
ContainerComponent.prototype = new AccessibilityComponent();
|
||||
extend(ContainerComponent.prototype, /** @lends Highcharts.ContainerComponent */ {
|
||||
/**
|
||||
* Called on first render/updates to the chart, including options changes.
|
||||
*/
|
||||
onChartUpdate: function () {
|
||||
this.handleSVGTitleElement();
|
||||
this.setSVGContainerLabel();
|
||||
this.setGraphicContainerAttrs();
|
||||
this.setRenderToAttrs();
|
||||
this.makeCreditsAccessible();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
handleSVGTitleElement: function () {
|
||||
var chart = this.chart, titleId = 'highcharts-title-' + chart.index, titleContents = stripHTMLTags(chart.langFormat('accessibility.svgContainerTitle', {
|
||||
chartTitle: getChartTitle(chart)
|
||||
}));
|
||||
if (titleContents.length) {
|
||||
var titleElement = this.svgTitleElement =
|
||||
this.svgTitleElement || doc.createElementNS('http://www.w3.org/2000/svg', 'title');
|
||||
titleElement.textContent = titleContents;
|
||||
titleElement.id = titleId;
|
||||
chart.renderTo.insertBefore(titleElement, chart.renderTo.firstChild);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
setSVGContainerLabel: function () {
|
||||
var chart = this.chart, svgContainerLabel = stripHTMLTags(chart.langFormat('accessibility.svgContainerLabel', {
|
||||
chartTitle: getChartTitle(chart)
|
||||
}));
|
||||
if (chart.renderer.box && svgContainerLabel.length) {
|
||||
chart.renderer.box.setAttribute('aria-label', svgContainerLabel);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
setGraphicContainerAttrs: function () {
|
||||
var chart = this.chart, label = chart.langFormat('accessibility.graphicContainerLabel', {
|
||||
chartTitle: getChartTitle(chart)
|
||||
});
|
||||
if (label.length) {
|
||||
chart.container.setAttribute('aria-label', label);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
setRenderToAttrs: function () {
|
||||
var chart = this.chart;
|
||||
if (chart.options.accessibility.landmarkVerbosity !== 'disabled') {
|
||||
chart.renderTo.setAttribute('role', 'region');
|
||||
}
|
||||
else {
|
||||
chart.renderTo.removeAttribute('role');
|
||||
}
|
||||
chart.renderTo.setAttribute('aria-label', chart.langFormat('accessibility.chartContainerLabel', {
|
||||
title: getChartTitle(chart),
|
||||
chart: chart
|
||||
}));
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
makeCreditsAccessible: function () {
|
||||
var chart = this.chart, credits = chart.credits;
|
||||
if (credits) {
|
||||
if (credits.textStr) {
|
||||
credits.element.setAttribute('aria-label', stripHTMLTags(chart.langFormat('accessibility.credits', { creditsStr: credits.textStr })));
|
||||
}
|
||||
unhideChartElementFromAT(chart, credits.element);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Accessibility disabled/chart destroyed.
|
||||
*/
|
||||
destroy: function () {
|
||||
this.chart.renderTo.setAttribute('aria-hidden', true);
|
||||
}
|
||||
});
|
||||
export default ContainerComponent;
|
||||
@@ -0,0 +1,563 @@
|
||||
/* *
|
||||
*
|
||||
* (c) 2009-2020 Øystein Moseng
|
||||
*
|
||||
* Accessibility component for chart info region and table.
|
||||
*
|
||||
* License: www.highcharts.com/license
|
||||
*
|
||||
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
|
||||
*
|
||||
* */
|
||||
'use strict';
|
||||
import H from '../../Core/Globals.js';
|
||||
var doc = H.doc;
|
||||
import U from '../../Core/Utilities.js';
|
||||
var extend = U.extend, format = U.format, pick = U.pick;
|
||||
import AccessibilityComponent from '../AccessibilityComponent.js';
|
||||
import Announcer from '../Utils/Announcer.js';
|
||||
import AnnotationsA11y from './AnnotationsA11y.js';
|
||||
var getAnnotationsInfoHTML = AnnotationsA11y.getAnnotationsInfoHTML;
|
||||
import ChartUtilities from '../Utils/ChartUtilities.js';
|
||||
var unhideChartElementFromAT = ChartUtilities.unhideChartElementFromAT, getChartTitle = ChartUtilities.getChartTitle, getAxisDescription = ChartUtilities.getAxisDescription;
|
||||
import HTMLUtilities from '../Utils/HTMLUtilities.js';
|
||||
var addClass = HTMLUtilities.addClass, setElAttrs = HTMLUtilities.setElAttrs, escapeStringForHTML = HTMLUtilities.escapeStringForHTML, stripHTMLTagsFromString = HTMLUtilities.stripHTMLTagsFromString, getElement = HTMLUtilities.getElement, visuallyHideElement = HTMLUtilities.visuallyHideElement;
|
||||
/* eslint-disable no-invalid-this, valid-jsdoc */
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
function getTypeDescForMapChart(chart, formatContext) {
|
||||
return formatContext.mapTitle ?
|
||||
chart.langFormat('accessibility.chartTypes.mapTypeDescription', formatContext) :
|
||||
chart.langFormat('accessibility.chartTypes.unknownMap', formatContext);
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
function getTypeDescForCombinationChart(chart, formatContext) {
|
||||
return chart.langFormat('accessibility.chartTypes.combinationChart', formatContext);
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
function getTypeDescForEmptyChart(chart, formatContext) {
|
||||
return chart.langFormat('accessibility.chartTypes.emptyChart', formatContext);
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
function buildTypeDescriptionFromSeries(chart, types, context) {
|
||||
var firstType = types[0], typeExplaination = chart.langFormat('accessibility.seriesTypeDescriptions.' + firstType, context), multi = chart.series && chart.series.length < 2 ? 'Single' : 'Multiple';
|
||||
return (chart.langFormat('accessibility.chartTypes.' + firstType + multi, context) ||
|
||||
chart.langFormat('accessibility.chartTypes.default' + multi, context)) + (typeExplaination ? ' ' + typeExplaination : '');
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
function getTableSummary(chart) {
|
||||
return chart.langFormat('accessibility.table.tableSummary', { chart: chart });
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
function stripEmptyHTMLTags(str) {
|
||||
return str.replace(/<(\w+)[^>]*?>\s*<\/\1>/g, '');
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
function enableSimpleHTML(str) {
|
||||
return str
|
||||
.replace(/<(h[1-7]|p|div|ul|ol|li)>/g, '<$1>')
|
||||
.replace(/</(h[1-7]|p|div|ul|ol|li|a|button)>/g, '</$1>')
|
||||
.replace(/<(div|a|button) id="([a-zA-Z\-0-9#]*?)">/g, '<$1 id="$2">');
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
function stringToSimpleHTML(str) {
|
||||
return stripEmptyHTMLTags(enableSimpleHTML(escapeStringForHTML(str)));
|
||||
}
|
||||
/**
|
||||
* Return simplified explaination of chart type. Some types will not be familiar
|
||||
* to most users, but in those cases we try to add an explaination 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] || {}, formatContext = {
|
||||
numSeries: this.series.length,
|
||||
numPoints: firstSeries.points && firstSeries.points.length,
|
||||
chart: this,
|
||||
mapTitle: firstSeries.mapTitle
|
||||
};
|
||||
if (!firstType) {
|
||||
return getTypeDescForEmptyChart(this, formatContext);
|
||||
}
|
||||
if (firstType === 'map') {
|
||||
return getTypeDescForMapChart(this, formatContext);
|
||||
}
|
||||
if (this.types.length > 1) {
|
||||
return getTypeDescForCombinationChart(this, formatContext);
|
||||
}
|
||||
return buildTypeDescriptionFromSeries(this, types, formatContext);
|
||||
};
|
||||
/**
|
||||
* The InfoRegionsComponent class
|
||||
*
|
||||
* @private
|
||||
* @class
|
||||
* @name Highcharts.InfoRegionsComponent
|
||||
*/
|
||||
var InfoRegionsComponent = function () { };
|
||||
InfoRegionsComponent.prototype = new AccessibilityComponent();
|
||||
extend(InfoRegionsComponent.prototype, /** @lends Highcharts.InfoRegionsComponent */ {
|
||||
/**
|
||||
* Init the component
|
||||
* @private
|
||||
*/
|
||||
init: function () {
|
||||
var chart = this.chart;
|
||||
var component = this;
|
||||
this.initRegionsDefinitions();
|
||||
this.addEvent(chart, 'afterGetTable', function (e) {
|
||||
component.onDataTableCreated(e);
|
||||
});
|
||||
this.addEvent(chart, 'afterViewData', function (tableDiv) {
|
||||
component.dataTableDiv = tableDiv;
|
||||
// Use small delay to give browsers & AT time to register new table
|
||||
setTimeout(function () {
|
||||
component.focusDataTable();
|
||||
}, 300);
|
||||
});
|
||||
this.announcer = new Announcer(chart, 'assertive');
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
initRegionsDefinitions: function () {
|
||||
var component = this;
|
||||
this.screenReaderSections = {
|
||||
before: {
|
||||
element: null,
|
||||
buildContent: function (chart) {
|
||||
var formatter = chart.options.accessibility
|
||||
.screenReaderSection.beforeChartFormatter;
|
||||
return formatter ? formatter(chart) :
|
||||
component.defaultBeforeChartFormatter(chart);
|
||||
},
|
||||
insertIntoDOM: function (el, chart) {
|
||||
chart.renderTo.insertBefore(el, chart.renderTo.firstChild);
|
||||
},
|
||||
afterInserted: function () {
|
||||
if (typeof component.sonifyButtonId !== 'undefined') {
|
||||
component.initSonifyButton(component.sonifyButtonId);
|
||||
}
|
||||
if (typeof component.dataTableButtonId !== 'undefined') {
|
||||
component.initDataTableButton(component.dataTableButtonId);
|
||||
}
|
||||
}
|
||||
},
|
||||
after: {
|
||||
element: null,
|
||||
buildContent: function (chart) {
|
||||
var formatter = chart.options.accessibility.screenReaderSection
|
||||
.afterChartFormatter;
|
||||
return formatter ? formatter(chart) :
|
||||
component.defaultAfterChartFormatter();
|
||||
},
|
||||
insertIntoDOM: function (el, chart) {
|
||||
chart.renderTo.insertBefore(el, chart.container.nextSibling);
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Called on chart render. Have to update the sections on render, in order
|
||||
* to get a11y info from series.
|
||||
*/
|
||||
onChartRender: function () {
|
||||
var component = this;
|
||||
this.linkedDescriptionElement = this.getLinkedDescriptionElement();
|
||||
this.setLinkedDescriptionAttrs();
|
||||
Object.keys(this.screenReaderSections).forEach(function (regionKey) {
|
||||
component.updateScreenReaderSection(regionKey);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
getLinkedDescriptionElement: function () {
|
||||
var chartOptions = this.chart.options, linkedDescOption = chartOptions.accessibility.linkedDescription;
|
||||
if (!linkedDescOption) {
|
||||
return;
|
||||
}
|
||||
if (typeof linkedDescOption !== 'string') {
|
||||
return linkedDescOption;
|
||||
}
|
||||
var query = format(linkedDescOption, this.chart), queryMatch = doc.querySelectorAll(query);
|
||||
if (queryMatch.length === 1) {
|
||||
return queryMatch[0];
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
setLinkedDescriptionAttrs: function () {
|
||||
var el = this.linkedDescriptionElement;
|
||||
if (el) {
|
||||
el.setAttribute('aria-hidden', 'true');
|
||||
addClass(el, 'highcharts-linked-description');
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {string} regionKey The name/key of the region to update
|
||||
*/
|
||||
updateScreenReaderSection: function (regionKey) {
|
||||
var chart = this.chart, region = this.screenReaderSections[regionKey], content = region.buildContent(chart), sectionDiv = region.element = (region.element || this.createElement('div')), hiddenDiv = (sectionDiv.firstChild || this.createElement('div'));
|
||||
this.setScreenReaderSectionAttribs(sectionDiv, regionKey);
|
||||
hiddenDiv.innerHTML = content;
|
||||
sectionDiv.appendChild(hiddenDiv);
|
||||
region.insertIntoDOM(sectionDiv, chart);
|
||||
visuallyHideElement(hiddenDiv);
|
||||
unhideChartElementFromAT(chart, hiddenDiv);
|
||||
if (region.afterInserted) {
|
||||
region.afterInserted();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Highcharts.HTMLDOMElement} sectionDiv The section element
|
||||
* @param {string} regionKey Name/key of the region we are setting attrs for
|
||||
*/
|
||||
setScreenReaderSectionAttribs: function (sectionDiv, regionKey) {
|
||||
var labelLangKey = ('accessibility.screenReaderSection.' + regionKey + 'RegionLabel'), chart = this.chart, labelText = chart.langFormat(labelLangKey, { chart: chart }), sectionId = 'highcharts-screen-reader-region-' + regionKey + '-' +
|
||||
chart.index;
|
||||
setElAttrs(sectionDiv, {
|
||||
id: sectionId,
|
||||
'aria-label': labelText
|
||||
});
|
||||
// Sections are wrapped to be positioned relatively to chart in case
|
||||
// elements inside are tabbed to.
|
||||
sectionDiv.style.position = 'relative';
|
||||
if (chart.options.accessibility.landmarkVerbosity === 'all' &&
|
||||
labelText) {
|
||||
sectionDiv.setAttribute('role', 'region');
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @return {string}
|
||||
*/
|
||||
defaultBeforeChartFormatter: function () {
|
||||
var _a;
|
||||
var chart = this.chart, format = chart.options.accessibility
|
||||
.screenReaderSection.beforeChartFormat, axesDesc = this.getAxesDescription(), shouldHaveSonifyBtn = chart.sonify && ((_a = chart.options.sonification) === null || _a === void 0 ? void 0 : _a.enabled), sonifyButtonId = 'highcharts-a11y-sonify-data-btn-' +
|
||||
chart.index, dataTableButtonId = 'hc-linkto-highcharts-data-table-' +
|
||||
chart.index, annotationsList = getAnnotationsInfoHTML(chart), annotationsTitleStr = chart.langFormat('accessibility.screenReaderSection.annotations.heading', { chart: chart }), context = {
|
||||
chartTitle: getChartTitle(chart),
|
||||
typeDescription: this.getTypeDescriptionText(),
|
||||
chartSubtitle: this.getSubtitleText(),
|
||||
chartLongdesc: this.getLongdescText(),
|
||||
xAxisDescription: axesDesc.xAxis,
|
||||
yAxisDescription: axesDesc.yAxis,
|
||||
playAsSoundButton: shouldHaveSonifyBtn ?
|
||||
this.getSonifyButtonText(sonifyButtonId) : '',
|
||||
viewTableButton: chart.getCSV ?
|
||||
this.getDataTableButtonText(dataTableButtonId) : '',
|
||||
annotationsTitle: annotationsList ? annotationsTitleStr : '',
|
||||
annotationsList: annotationsList
|
||||
}, formattedString = H.i18nFormat(format, context, chart);
|
||||
this.dataTableButtonId = dataTableButtonId;
|
||||
this.sonifyButtonId = sonifyButtonId;
|
||||
return stringToSimpleHTML(formattedString);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @return {string}
|
||||
*/
|
||||
defaultAfterChartFormatter: function () {
|
||||
var chart = this.chart, format = chart.options.accessibility
|
||||
.screenReaderSection.afterChartFormat, context = {
|
||||
endOfChartMarker: this.getEndOfChartMarkerText()
|
||||
}, formattedString = H.i18nFormat(format, context, chart);
|
||||
return stringToSimpleHTML(formattedString);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @return {string}
|
||||
*/
|
||||
getLinkedDescription: function () {
|
||||
var el = this.linkedDescriptionElement, content = el && el.innerHTML || '';
|
||||
return stripHTMLTagsFromString(content);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @return {string}
|
||||
*/
|
||||
getLongdescText: function () {
|
||||
var chartOptions = this.chart.options, captionOptions = chartOptions.caption, captionText = captionOptions && captionOptions.text, linkedDescription = this.getLinkedDescription();
|
||||
return (chartOptions.accessibility.description ||
|
||||
linkedDescription ||
|
||||
captionText ||
|
||||
'');
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @return {string}
|
||||
*/
|
||||
getTypeDescriptionText: function () {
|
||||
var chart = this.chart;
|
||||
return chart.types ?
|
||||
chart.options.accessibility.typeDescription ||
|
||||
chart.getTypeDescription(chart.types) : '';
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {string} buttonId
|
||||
* @return {string}
|
||||
*/
|
||||
getDataTableButtonText: function (buttonId) {
|
||||
var chart = this.chart, buttonText = chart.langFormat('accessibility.table.viewAsDataTableButtonText', { chart: chart, chartTitle: getChartTitle(chart) });
|
||||
return '<button id="' + buttonId + '">' + buttonText + '</button>';
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {string} buttonId
|
||||
* @return {string}
|
||||
*/
|
||||
getSonifyButtonText: function (buttonId) {
|
||||
var _a;
|
||||
var chart = this.chart;
|
||||
if (((_a = chart.options.sonification) === null || _a === void 0 ? void 0 : _a.enabled) === false) {
|
||||
return '';
|
||||
}
|
||||
var buttonText = chart.langFormat('accessibility.sonification.playAsSoundButtonText', { chart: chart, chartTitle: getChartTitle(chart) });
|
||||
return '<button id="' + buttonId + '">' + buttonText + '</button>';
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @return {string}
|
||||
*/
|
||||
getSubtitleText: function () {
|
||||
var subtitle = (this.chart.options.subtitle);
|
||||
return stripHTMLTagsFromString(subtitle && subtitle.text || '');
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @return {string}
|
||||
*/
|
||||
getEndOfChartMarkerText: function () {
|
||||
var chart = this.chart, markerText = chart.langFormat('accessibility.screenReaderSection.endOfChartMarker', { chart: chart }), id = 'highcharts-end-of-chart-marker-' + chart.index;
|
||||
return '<div id="' + id + '">' + markerText + '</div>';
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Highcharts.Dictionary<string>} e
|
||||
*/
|
||||
onDataTableCreated: function (e) {
|
||||
var chart = this.chart;
|
||||
if (chart.options.accessibility.enabled) {
|
||||
if (this.viewDataTableButton) {
|
||||
this.viewDataTableButton.setAttribute('aria-expanded', 'true');
|
||||
}
|
||||
e.html = e.html.replace('<table ', '<table tabindex="-1" summary="' + getTableSummary(chart) + '"');
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
focusDataTable: function () {
|
||||
var tableDiv = this.dataTableDiv, table = tableDiv && tableDiv.getElementsByTagName('table')[0];
|
||||
if (table && table.focus) {
|
||||
table.focus();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {string} sonifyButtonId
|
||||
*/
|
||||
initSonifyButton: function (sonifyButtonId) {
|
||||
var _this = this;
|
||||
var el = this.sonifyButton = getElement(sonifyButtonId);
|
||||
var chart = this.chart;
|
||||
var defaultHandler = function (e) {
|
||||
el === null || el === void 0 ? void 0 : el.setAttribute('aria-hidden', 'true');
|
||||
el === null || el === void 0 ? void 0 : el.setAttribute('aria-label', '');
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
var announceMsg = chart.langFormat('accessibility.sonification.playAsSoundClickAnnouncement', { chart: chart });
|
||||
_this.announcer.announce(announceMsg);
|
||||
setTimeout(function () {
|
||||
el === null || el === void 0 ? void 0 : el.removeAttribute('aria-hidden');
|
||||
el === null || el === void 0 ? void 0 : el.removeAttribute('aria-label');
|
||||
if (chart.sonify) {
|
||||
chart.sonify();
|
||||
}
|
||||
}, 1000); // Delay to let screen reader speak the button press
|
||||
};
|
||||
if (el && chart) {
|
||||
setElAttrs(el, {
|
||||
tabindex: '-1'
|
||||
});
|
||||
el.onclick = function (e) {
|
||||
var _a;
|
||||
var onPlayAsSoundClick = (_a = chart.options.accessibility) === null || _a === void 0 ? void 0 : _a.screenReaderSection.onPlayAsSoundClick;
|
||||
(onPlayAsSoundClick || defaultHandler).call(this, e, chart);
|
||||
};
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Set attribs and handlers for default viewAsDataTable button if exists.
|
||||
* @private
|
||||
* @param {string} tableButtonId
|
||||
*/
|
||||
initDataTableButton: function (tableButtonId) {
|
||||
var el = this.viewDataTableButton = getElement(tableButtonId), chart = this.chart, tableId = tableButtonId.replace('hc-linkto-', '');
|
||||
if (el) {
|
||||
setElAttrs(el, {
|
||||
tabindex: '-1',
|
||||
'aria-expanded': !!getElement(tableId)
|
||||
});
|
||||
el.onclick = chart.options.accessibility
|
||||
.screenReaderSection.onViewDataTableClick ||
|
||||
function () {
|
||||
chart.viewData();
|
||||
};
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Return object with text description of each of the chart's axes.
|
||||
* @private
|
||||
* @return {Highcharts.Dictionary<string>}
|
||||
*/
|
||||
getAxesDescription: function () {
|
||||
var chart = this.chart, shouldDescribeColl = function (collectionKey, defaultCondition) {
|
||||
var axes = chart[collectionKey];
|
||||
return axes.length > 1 || axes[0] &&
|
||||
pick(axes[0].options.accessibility &&
|
||||
axes[0].options.accessibility.enabled, defaultCondition);
|
||||
}, hasNoMap = !!chart.types && chart.types.indexOf('map') < 0, hasCartesian = !!chart.hasCartesianSeries, showXAxes = shouldDescribeColl('xAxis', !chart.angular && hasCartesian && hasNoMap), showYAxes = shouldDescribeColl('yAxis', hasCartesian && hasNoMap), desc = {};
|
||||
if (showXAxes) {
|
||||
desc.xAxis = this.getAxisDescriptionText('xAxis');
|
||||
}
|
||||
if (showYAxes) {
|
||||
desc.yAxis = this.getAxisDescriptionText('yAxis');
|
||||
}
|
||||
return desc;
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {string} collectionKey
|
||||
* @return {string}
|
||||
*/
|
||||
getAxisDescriptionText: function (collectionKey) {
|
||||
var component = this, chart = this.chart, axes = chart[collectionKey];
|
||||
return chart.langFormat('accessibility.axis.' + collectionKey + 'Description' + (axes.length > 1 ? 'Plural' : 'Singular'), {
|
||||
chart: chart,
|
||||
names: axes.map(function (axis) {
|
||||
return getAxisDescription(axis);
|
||||
}),
|
||||
ranges: axes.map(function (axis) {
|
||||
return component.getAxisRangeDescription(axis);
|
||||
}),
|
||||
numAxes: axes.length
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 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 axisOptions = axis.options || {};
|
||||
// Handle overridden range description
|
||||
if (axisOptions.accessibility &&
|
||||
typeof axisOptions.accessibility.rangeDescription !== 'undefined') {
|
||||
return axisOptions.accessibility.rangeDescription;
|
||||
}
|
||||
// Handle category axes
|
||||
if (axis.categories) {
|
||||
return this.getCategoryAxisRangeDesc(axis);
|
||||
}
|
||||
// Use time range, not from-to?
|
||||
if (axis.dateTime && (axis.min === 0 || axis.dataMin === 0)) {
|
||||
return this.getAxisTimeLengthDesc(axis);
|
||||
}
|
||||
// Just use from and to.
|
||||
// We have the range and the unit to use, find the desc format
|
||||
return this.getAxisFromToDescription(axis);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Highcharts.Axis} axis
|
||||
* @return {string}
|
||||
*/
|
||||
getCategoryAxisRangeDesc: function (axis) {
|
||||
var chart = this.chart;
|
||||
if (axis.dataMax && axis.dataMin) {
|
||||
return chart.langFormat('accessibility.axis.rangeCategories', {
|
||||
chart: chart,
|
||||
axis: axis,
|
||||
numCategories: axis.dataMax - axis.dataMin + 1
|
||||
});
|
||||
}
|
||||
return '';
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Highcharts.Axis} axis
|
||||
* @return {string}
|
||||
*/
|
||||
getAxisTimeLengthDesc: function (axis) {
|
||||
var chart = this.chart, range = {}, rangeUnit = 'Seconds';
|
||||
range.Seconds = ((axis.max || 0) - (axis.min || 0)) / 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;
|
||||
}
|
||||
});
|
||||
var rangeValue = 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: rangeValue.replace('.0', '')
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Highcharts.Axis} axis
|
||||
* @return {string}
|
||||
*/
|
||||
getAxisFromToDescription: function (axis) {
|
||||
var chart = this.chart, dateRangeFormat = chart.options.accessibility
|
||||
.screenReaderSection.axisRangeDateFormat, format = function (axisKey) {
|
||||
return axis.dateTime ? chart.time.dateFormat(dateRangeFormat, axis[axisKey]) : axis[axisKey];
|
||||
};
|
||||
return chart.langFormat('accessibility.axis.rangeFromTo', {
|
||||
chart: chart,
|
||||
axis: axis,
|
||||
rangeFrom: format('min'),
|
||||
rangeTo: format('max')
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Remove component traces
|
||||
*/
|
||||
destroy: function () {
|
||||
var _a;
|
||||
(_a = this.announcer) === null || _a === void 0 ? void 0 : _a.destroy();
|
||||
}
|
||||
});
|
||||
export default InfoRegionsComponent;
|
||||
@@ -0,0 +1,300 @@
|
||||
/* *
|
||||
*
|
||||
* (c) 2009-2020 Øystein Moseng
|
||||
*
|
||||
* Accessibility component for chart legend.
|
||||
*
|
||||
* License: www.highcharts.com/license
|
||||
*
|
||||
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
|
||||
*
|
||||
* */
|
||||
'use strict';
|
||||
import H from '../../Core/Globals.js';
|
||||
import Legend from '../../Core/Legend.js';
|
||||
import U from '../../Core/Utilities.js';
|
||||
var addEvent = U.addEvent, extend = U.extend, find = U.find, fireEvent = U.fireEvent;
|
||||
import AccessibilityComponent from '../AccessibilityComponent.js';
|
||||
import KeyboardNavigationHandler from '../KeyboardNavigationHandler.js';
|
||||
import HTMLUtilities from '../Utils/HTMLUtilities.js';
|
||||
var stripHTMLTags = HTMLUtilities.stripHTMLTagsFromString, removeElement = HTMLUtilities.removeElement;
|
||||
/* eslint-disable no-invalid-this, valid-jsdoc */
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
function scrollLegendToItem(legend, itemIx) {
|
||||
var itemPage = legend.allItems[itemIx].pageIx, curPage = legend.currentPage;
|
||||
if (typeof itemPage !== 'undefined' && itemPage + 1 !== curPage) {
|
||||
legend.scroll(1 + itemPage - curPage);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
function shouldDoLegendA11y(chart) {
|
||||
var items = chart.legend && chart.legend.allItems, legendA11yOptions = (chart.options.legend.accessibility || {});
|
||||
return !!(items && items.length &&
|
||||
!(chart.colorAxis && chart.colorAxis.length) &&
|
||||
legendA11yOptions.enabled !== false);
|
||||
}
|
||||
/**
|
||||
* Highlight legend item by index.
|
||||
*
|
||||
* @private
|
||||
* @function Highcharts.Chart#highlightLegendItem
|
||||
*
|
||||
* @param {number} ix
|
||||
*
|
||||
* @return {boolean}
|
||||
*/
|
||||
H.Chart.prototype.highlightLegendItem = function (ix) {
|
||||
var items = this.legend.allItems, oldIx = this.highlightedLegendItemIx;
|
||||
if (items[ix]) {
|
||||
if (items[oldIx]) {
|
||||
fireEvent(items[oldIx].legendGroup.element, 'mouseout');
|
||||
}
|
||||
scrollLegendToItem(this.legend, ix);
|
||||
this.setFocusToElement(items[ix].legendItem, items[ix].a11yProxyElement);
|
||||
fireEvent(items[ix].legendGroup.element, 'mouseover');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
// Keep track of pressed state for legend items
|
||||
addEvent(Legend, 'afterColorizeItem', function (e) {
|
||||
var chart = this.chart, a11yOptions = chart.options.accessibility, legendItem = e.item;
|
||||
if (a11yOptions.enabled && legendItem && legendItem.a11yProxyElement) {
|
||||
legendItem.a11yProxyElement.setAttribute('aria-pressed', e.visible ? 'false' : 'true');
|
||||
}
|
||||
});
|
||||
/**
|
||||
* The LegendComponent class
|
||||
*
|
||||
* @private
|
||||
* @class
|
||||
* @name Highcharts.LegendComponent
|
||||
*/
|
||||
var LegendComponent = function () { };
|
||||
LegendComponent.prototype = new AccessibilityComponent();
|
||||
extend(LegendComponent.prototype, /** @lends Highcharts.LegendComponent */ {
|
||||
/**
|
||||
* Init the component
|
||||
* @private
|
||||
*/
|
||||
init: function () {
|
||||
var component = this;
|
||||
this.proxyElementsList = [];
|
||||
this.recreateProxies();
|
||||
// Note: Chart could create legend dynamically, so events can not be
|
||||
// tied to the component's chart's current legend.
|
||||
this.addEvent(Legend, 'afterScroll', function () {
|
||||
if (this.chart === component.chart) {
|
||||
component.updateProxiesPositions();
|
||||
component.updateLegendItemProxyVisibility();
|
||||
this.chart.highlightLegendItem(component.highlightedLegendItemIx);
|
||||
}
|
||||
});
|
||||
this.addEvent(Legend, 'afterPositionItem', function (e) {
|
||||
if (this.chart === component.chart && this.chart.renderer) {
|
||||
component.updateProxyPositionForItem(e.item);
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
updateLegendItemProxyVisibility: function () {
|
||||
var legend = this.chart.legend, items = legend.allItems || [], curPage = legend.currentPage || 1, clipHeight = legend.clipHeight || 0;
|
||||
items.forEach(function (item) {
|
||||
var itemPage = item.pageIx || 0, y = item._legendItemPos ? item._legendItemPos[1] : 0, h = item.legendItem ? Math.round(item.legendItem.getBBox().height) : 0, hide = y + h - legend.pages[itemPage] > clipHeight || itemPage !== curPage - 1;
|
||||
if (item.a11yProxyElement) {
|
||||
item.a11yProxyElement.style.visibility = hide ?
|
||||
'hidden' : 'visible';
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* The legend needs updates on every render, in order to update positioning
|
||||
* of the proxy overlays.
|
||||
*/
|
||||
onChartRender: function () {
|
||||
if (shouldDoLegendA11y(this.chart)) {
|
||||
this.updateProxiesPositions();
|
||||
}
|
||||
else {
|
||||
this.removeProxies();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
updateProxiesPositions: function () {
|
||||
for (var _i = 0, _a = this.proxyElementsList; _i < _a.length; _i++) {
|
||||
var _b = _a[_i], element = _b.element, posElement = _b.posElement;
|
||||
this.updateProxyButtonPosition(element, posElement);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
updateProxyPositionForItem: function (item) {
|
||||
var proxyRef = find(this.proxyElementsList, function (ref) { return ref.item === item; });
|
||||
if (proxyRef) {
|
||||
this.updateProxyButtonPosition(proxyRef.element, proxyRef.posElement);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
recreateProxies: function () {
|
||||
this.removeProxies();
|
||||
if (shouldDoLegendA11y(this.chart)) {
|
||||
this.addLegendProxyGroup();
|
||||
this.proxyLegendItems();
|
||||
this.updateLegendItemProxyVisibility();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
removeProxies: function () {
|
||||
removeElement(this.legendProxyGroup);
|
||||
this.proxyElementsList = [];
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
addLegendProxyGroup: function () {
|
||||
var a11yOptions = this.chart.options.accessibility, groupLabel = this.chart.langFormat('accessibility.legend.legendLabel', {}), groupRole = a11yOptions.landmarkVerbosity === 'all' ?
|
||||
'region' : null;
|
||||
this.legendProxyGroup = this.addProxyGroup({
|
||||
'aria-label': groupLabel,
|
||||
'role': groupRole
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
proxyLegendItems: function () {
|
||||
var component = this, items = (this.chart.legend &&
|
||||
this.chart.legend.allItems || []);
|
||||
items.forEach(function (item) {
|
||||
if (item.legendItem && item.legendItem.element) {
|
||||
component.proxyLegendItem(item);
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Highcharts.BubbleLegend|Point|Highcharts.Series} item
|
||||
*/
|
||||
proxyLegendItem: function (item) {
|
||||
if (!item.legendItem || !item.legendGroup) {
|
||||
return;
|
||||
}
|
||||
var itemLabel = this.chart.langFormat('accessibility.legend.legendItem', {
|
||||
chart: this.chart,
|
||||
itemName: stripHTMLTags(item.name)
|
||||
}), attribs = {
|
||||
tabindex: -1,
|
||||
'aria-pressed': !item.visible,
|
||||
'aria-label': itemLabel
|
||||
},
|
||||
// Considers useHTML
|
||||
proxyPositioningElement = item.legendGroup.div ?
|
||||
item.legendItem : item.legendGroup;
|
||||
item.a11yProxyElement = this.createProxyButton(item.legendItem, this.legendProxyGroup, attribs, proxyPositioningElement);
|
||||
this.proxyElementsList.push({
|
||||
item: item,
|
||||
element: item.a11yProxyElement,
|
||||
posElement: proxyPositioningElement
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Get keyboard navigation handler for this component.
|
||||
* @return {Highcharts.KeyboardNavigationHandler}
|
||||
*/
|
||||
getKeyboardNavigation: function () {
|
||||
var keys = this.keyCodes, component = this, chart = this.chart;
|
||||
return new KeyboardNavigationHandler(chart, {
|
||||
keyCodeMap: [
|
||||
[
|
||||
[keys.left, keys.right, keys.up, keys.down],
|
||||
function (keyCode) {
|
||||
return component.onKbdArrowKey(this, keyCode);
|
||||
}
|
||||
],
|
||||
[
|
||||
[keys.enter, keys.space],
|
||||
function () {
|
||||
return component.onKbdClick(this);
|
||||
}
|
||||
]
|
||||
],
|
||||
validate: function () {
|
||||
return component.shouldHaveLegendNavigation();
|
||||
},
|
||||
init: function (direction) {
|
||||
return component.onKbdNavigationInit(direction);
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Highcharts.KeyboardNavigationHandler} keyboardNavigationHandler
|
||||
* @param {number} keyCode
|
||||
* @return {number}
|
||||
* Response code
|
||||
*/
|
||||
onKbdArrowKey: function (keyboardNavigationHandler, keyCode) {
|
||||
var keys = this.keyCodes, response = keyboardNavigationHandler.response, chart = this.chart, a11yOptions = chart.options.accessibility, numItems = chart.legend.allItems.length, direction = (keyCode === keys.left || keyCode === keys.up) ? -1 : 1;
|
||||
var res = chart.highlightLegendItem(this.highlightedLegendItemIx + direction);
|
||||
if (res) {
|
||||
this.highlightedLegendItemIx += direction;
|
||||
return response.success;
|
||||
}
|
||||
if (numItems > 1 &&
|
||||
a11yOptions.keyboardNavigation.wrapAround) {
|
||||
keyboardNavigationHandler.init(direction);
|
||||
return response.success;
|
||||
}
|
||||
// No wrap, move
|
||||
return response[direction > 0 ? 'next' : 'prev'];
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Highcharts.KeyboardNavigationHandler} keyboardNavigationHandler
|
||||
* @return {number}
|
||||
* Response code
|
||||
*/
|
||||
onKbdClick: function (keyboardNavigationHandler) {
|
||||
var legendItem = this.chart.legend.allItems[this.highlightedLegendItemIx];
|
||||
if (legendItem && legendItem.a11yProxyElement) {
|
||||
fireEvent(legendItem.a11yProxyElement, 'click');
|
||||
}
|
||||
return keyboardNavigationHandler.response.success;
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @return {boolean|undefined}
|
||||
*/
|
||||
shouldHaveLegendNavigation: function () {
|
||||
var chart = this.chart, legendOptions = chart.options.legend || {}, hasLegend = chart.legend && chart.legend.allItems, hasColorAxis = chart.colorAxis && chart.colorAxis.length, legendA11yOptions = (legendOptions.accessibility || {});
|
||||
return !!(hasLegend &&
|
||||
chart.legend.display &&
|
||||
!hasColorAxis &&
|
||||
legendA11yOptions.enabled &&
|
||||
legendA11yOptions.keyboardNavigation &&
|
||||
legendA11yOptions.keyboardNavigation.enabled);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {number} direction
|
||||
*/
|
||||
onKbdNavigationInit: function (direction) {
|
||||
var chart = this.chart, lastIx = chart.legend.allItems.length - 1, ixToHighlight = direction > 0 ? 0 : lastIx;
|
||||
chart.highlightLegendItem(ixToHighlight);
|
||||
this.highlightedLegendItemIx = ixToHighlight;
|
||||
}
|
||||
});
|
||||
export default LegendComponent;
|
||||
@@ -0,0 +1,349 @@
|
||||
/* *
|
||||
*
|
||||
* (c) 2009-2020 Øystein Moseng
|
||||
*
|
||||
* Accessibility component for exporting menu.
|
||||
*
|
||||
* License: www.highcharts.com/license
|
||||
*
|
||||
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
|
||||
*
|
||||
* */
|
||||
'use strict';
|
||||
import H from '../../Core/Globals.js';
|
||||
import U from '../../Core/Utilities.js';
|
||||
var extend = U.extend;
|
||||
import AccessibilityComponent from '../AccessibilityComponent.js';
|
||||
import KeyboardNavigationHandler from '../KeyboardNavigationHandler.js';
|
||||
import ChartUtilities from '../Utils/ChartUtilities.js';
|
||||
var unhideChartElementFromAT = ChartUtilities.unhideChartElementFromAT;
|
||||
import HTMLUtilities from '../Utils/HTMLUtilities.js';
|
||||
var removeElement = HTMLUtilities.removeElement, getFakeMouseEvent = HTMLUtilities.getFakeMouseEvent;
|
||||
/* eslint-disable no-invalid-this, valid-jsdoc */
|
||||
/**
|
||||
* Get the wrapped export button element of a chart.
|
||||
*
|
||||
* @private
|
||||
* @param {Highcharts.Chart} chart
|
||||
* @returns {Highcharts.SVGElement}
|
||||
*/
|
||||
function getExportMenuButtonElement(chart) {
|
||||
return chart.exportSVGElements && chart.exportSVGElements[0];
|
||||
}
|
||||
/**
|
||||
* Show the export menu and focus the first item (if exists).
|
||||
*
|
||||
* @private
|
||||
* @function Highcharts.Chart#showExportMenu
|
||||
*/
|
||||
H.Chart.prototype.showExportMenu = function () {
|
||||
var exportButton = getExportMenuButtonElement(this);
|
||||
if (exportButton) {
|
||||
var el = exportButton.element;
|
||||
if (el.onclick) {
|
||||
el.onclick(getFakeMouseEvent('click'));
|
||||
}
|
||||
}
|
||||
};
|
||||
/**
|
||||
* @private
|
||||
* @function Highcharts.Chart#hideExportMenu
|
||||
*/
|
||||
H.Chart.prototype.hideExportMenu = function () {
|
||||
var chart = this, exportList = chart.exportDivElements;
|
||||
if (exportList && chart.exportContextMenu) {
|
||||
// Reset hover states etc.
|
||||
exportList.forEach(function (el) {
|
||||
if (el.className === 'highcharts-menu-item' && el.onmouseout) {
|
||||
el.onmouseout(getFakeMouseEvent('mouseout'));
|
||||
}
|
||||
});
|
||||
chart.highlightedExportItemIx = 0;
|
||||
// Hide the menu div
|
||||
chart.exportContextMenu.hideMenu();
|
||||
// Make sure the chart has focus and can capture keyboard events
|
||||
chart.container.focus();
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Highlight export menu item by index.
|
||||
*
|
||||
* @private
|
||||
* @function Highcharts.Chart#highlightExportItem
|
||||
*
|
||||
* @param {number} ix
|
||||
*
|
||||
* @return {boolean}
|
||||
*/
|
||||
H.Chart.prototype.highlightExportItem = function (ix) {
|
||||
var listItem = this.exportDivElements && this.exportDivElements[ix], curHighlighted = this.exportDivElements &&
|
||||
this.exportDivElements[this.highlightedExportItemIx], hasSVGFocusSupport;
|
||||
if (listItem &&
|
||||
listItem.tagName === 'LI' &&
|
||||
!(listItem.children && listItem.children.length)) {
|
||||
// Test if we have focus support for SVG elements
|
||||
hasSVGFocusSupport = !!(this.renderTo.getElementsByTagName('g')[0] || {}).focus;
|
||||
// Only focus if we can set focus back to the elements after
|
||||
// destroying the menu (#7422)
|
||||
if (listItem.focus && hasSVGFocusSupport) {
|
||||
listItem.focus();
|
||||
}
|
||||
if (curHighlighted && curHighlighted.onmouseout) {
|
||||
curHighlighted.onmouseout(getFakeMouseEvent('mouseout'));
|
||||
}
|
||||
if (listItem.onmouseover) {
|
||||
listItem.onmouseover(getFakeMouseEvent('mouseover'));
|
||||
}
|
||||
this.highlightedExportItemIx = ix;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
/**
|
||||
* Try to highlight the last valid export menu item.
|
||||
*
|
||||
* @private
|
||||
* @function Highcharts.Chart#highlightLastExportItem
|
||||
* @return {boolean}
|
||||
*/
|
||||
H.Chart.prototype.highlightLastExportItem = function () {
|
||||
var chart = this, i;
|
||||
if (chart.exportDivElements) {
|
||||
i = chart.exportDivElements.length;
|
||||
while (i--) {
|
||||
if (chart.highlightExportItem(i)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
/**
|
||||
* @private
|
||||
* @param {Highcharts.Chart} chart
|
||||
*/
|
||||
function exportingShouldHaveA11y(chart) {
|
||||
var exportingOpts = chart.options.exporting, exportButton = getExportMenuButtonElement(chart);
|
||||
return !!(exportingOpts &&
|
||||
exportingOpts.enabled !== false &&
|
||||
exportingOpts.accessibility &&
|
||||
exportingOpts.accessibility.enabled &&
|
||||
exportButton &&
|
||||
exportButton.element);
|
||||
}
|
||||
/**
|
||||
* The MenuComponent class
|
||||
*
|
||||
* @private
|
||||
* @class
|
||||
* @name Highcharts.MenuComponent
|
||||
*/
|
||||
var MenuComponent = function () { };
|
||||
MenuComponent.prototype = new AccessibilityComponent();
|
||||
extend(MenuComponent.prototype, /** @lends Highcharts.MenuComponent */ {
|
||||
/**
|
||||
* Init the component
|
||||
*/
|
||||
init: function () {
|
||||
var chart = this.chart, component = this;
|
||||
this.addEvent(chart, 'exportMenuShown', function () {
|
||||
component.onMenuShown();
|
||||
});
|
||||
this.addEvent(chart, 'exportMenuHidden', function () {
|
||||
component.onMenuHidden();
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
onMenuHidden: function () {
|
||||
var menu = this.chart.exportContextMenu;
|
||||
if (menu) {
|
||||
menu.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
this.isExportMenuShown = false;
|
||||
this.setExportButtonExpandedState('false');
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
onMenuShown: function () {
|
||||
var chart = this.chart, menu = chart.exportContextMenu;
|
||||
if (menu) {
|
||||
this.addAccessibleContextMenuAttribs();
|
||||
unhideChartElementFromAT(chart, menu);
|
||||
}
|
||||
this.isExportMenuShown = true;
|
||||
this.setExportButtonExpandedState('true');
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {string} stateStr
|
||||
*/
|
||||
setExportButtonExpandedState: function (stateStr) {
|
||||
var button = this.exportButtonProxy;
|
||||
if (button) {
|
||||
button.setAttribute('aria-expanded', stateStr);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Called on each render of the chart. We need to update positioning of the
|
||||
* proxy overlay.
|
||||
*/
|
||||
onChartRender: function () {
|
||||
var chart = this.chart, a11yOptions = chart.options.accessibility;
|
||||
// Always start with a clean slate
|
||||
removeElement(this.exportProxyGroup);
|
||||
// Set screen reader properties on export menu
|
||||
if (exportingShouldHaveA11y(chart)) {
|
||||
// Proxy button and group
|
||||
this.exportProxyGroup = this.addProxyGroup(
|
||||
// Wrap in a region div if verbosity is high
|
||||
a11yOptions.landmarkVerbosity === 'all' ? {
|
||||
'aria-label': chart.langFormat('accessibility.exporting.exportRegionLabel', { chart: chart }),
|
||||
'role': 'region'
|
||||
} : {});
|
||||
var button = getExportMenuButtonElement(this.chart);
|
||||
this.exportButtonProxy = this.createProxyButton(button, this.exportProxyGroup, {
|
||||
'aria-label': chart.langFormat('accessibility.exporting.menuButtonLabel', { chart: chart }),
|
||||
'aria-expanded': 'false'
|
||||
});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
addAccessibleContextMenuAttribs: function () {
|
||||
var chart = this.chart, exportList = chart.exportDivElements;
|
||||
if (exportList && exportList.length) {
|
||||
// Set tabindex on the menu items to allow focusing by script
|
||||
// Set role to give screen readers a chance to pick up the contents
|
||||
exportList.forEach(function (item) {
|
||||
if (item.tagName === 'LI' &&
|
||||
!(item.children && item.children.length)) {
|
||||
item.setAttribute('tabindex', -1);
|
||||
}
|
||||
else {
|
||||
item.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
});
|
||||
// Set accessibility properties on parent div
|
||||
var parentDiv = exportList[0].parentNode;
|
||||
parentDiv.removeAttribute('aria-hidden');
|
||||
parentDiv.setAttribute('aria-label', chart.langFormat('accessibility.exporting.chartMenuLabel', { chart: chart }));
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Get keyboard navigation handler for this component.
|
||||
* @return {Highcharts.KeyboardNavigationHandler}
|
||||
*/
|
||||
getKeyboardNavigation: function () {
|
||||
var keys = this.keyCodes, chart = this.chart, component = this;
|
||||
return new KeyboardNavigationHandler(chart, {
|
||||
keyCodeMap: [
|
||||
// Arrow prev handler
|
||||
[
|
||||
[keys.left, keys.up],
|
||||
function () {
|
||||
return component.onKbdPrevious(this);
|
||||
}
|
||||
],
|
||||
// Arrow next handler
|
||||
[
|
||||
[keys.right, keys.down],
|
||||
function () {
|
||||
return component.onKbdNext(this);
|
||||
}
|
||||
],
|
||||
// Click handler
|
||||
[
|
||||
[keys.enter, keys.space],
|
||||
function () {
|
||||
return component.onKbdClick(this);
|
||||
}
|
||||
]
|
||||
],
|
||||
// Only run exporting navigation if exporting support exists and is
|
||||
// enabled on chart
|
||||
validate: function () {
|
||||
return chart.exportChart &&
|
||||
chart.options.exporting.enabled !== false &&
|
||||
chart.options.exporting.accessibility.enabled !==
|
||||
false;
|
||||
},
|
||||
// Focus export menu button
|
||||
init: function () {
|
||||
var exportBtn = component.exportButtonProxy, exportGroup = chart.exportingGroup;
|
||||
if (exportGroup && exportBtn) {
|
||||
chart.setFocusToElement(exportGroup, exportBtn);
|
||||
}
|
||||
},
|
||||
// Hide the menu
|
||||
terminate: function () {
|
||||
chart.hideExportMenu();
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Highcharts.KeyboardNavigationHandler} keyboardNavigationHandler
|
||||
* @return {number}
|
||||
* Response code
|
||||
*/
|
||||
onKbdPrevious: function (keyboardNavigationHandler) {
|
||||
var chart = this.chart, a11yOptions = chart.options.accessibility, response = keyboardNavigationHandler.response, i = chart.highlightedExportItemIx || 0;
|
||||
// Try to highlight prev item in list. Highlighting e.g.
|
||||
// separators will fail.
|
||||
while (i--) {
|
||||
if (chart.highlightExportItem(i)) {
|
||||
return response.success;
|
||||
}
|
||||
}
|
||||
// We failed, so wrap around or move to prev module
|
||||
if (a11yOptions.keyboardNavigation.wrapAround) {
|
||||
chart.highlightLastExportItem();
|
||||
return response.success;
|
||||
}
|
||||
return response.prev;
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Highcharts.KeyboardNavigationHandler} keyboardNavigationHandler
|
||||
* @return {number}
|
||||
* Response code
|
||||
*/
|
||||
onKbdNext: function (keyboardNavigationHandler) {
|
||||
var chart = this.chart, a11yOptions = chart.options.accessibility, response = keyboardNavigationHandler.response, i = (chart.highlightedExportItemIx || 0) + 1;
|
||||
// Try to highlight next item in list. Highlighting e.g.
|
||||
// separators will fail.
|
||||
for (; i < chart.exportDivElements.length; ++i) {
|
||||
if (chart.highlightExportItem(i)) {
|
||||
return response.success;
|
||||
}
|
||||
}
|
||||
// We failed, so wrap around or move to next module
|
||||
if (a11yOptions.keyboardNavigation.wrapAround) {
|
||||
chart.highlightExportItem(0);
|
||||
return response.success;
|
||||
}
|
||||
return response.next;
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Highcharts.KeyboardNavigationHandler} keyboardNavigationHandler
|
||||
* @return {number}
|
||||
* Response code
|
||||
*/
|
||||
onKbdClick: function (keyboardNavigationHandler) {
|
||||
var chart = this.chart, curHighlightedItem = chart.exportDivElements[chart.highlightedExportItemIx], exportButtonElement = getExportMenuButtonElement(chart).element;
|
||||
if (this.isExportMenuShown) {
|
||||
this.fakeClickEvent(curHighlightedItem);
|
||||
}
|
||||
else {
|
||||
this.fakeClickEvent(exportButtonElement);
|
||||
chart.highlightExportItem(0);
|
||||
}
|
||||
return keyboardNavigationHandler.response.success;
|
||||
}
|
||||
});
|
||||
export default MenuComponent;
|
||||
@@ -0,0 +1,266 @@
|
||||
/* *
|
||||
*
|
||||
* (c) 2009-2020 Øystein Moseng
|
||||
*
|
||||
* Accessibility component for the range selector.
|
||||
*
|
||||
* License: www.highcharts.com/license
|
||||
*
|
||||
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
|
||||
*
|
||||
* */
|
||||
'use strict';
|
||||
import H from '../../Core/Globals.js';
|
||||
import U from '../../Core/Utilities.js';
|
||||
var extend = U.extend;
|
||||
import AccessibilityComponent from '../AccessibilityComponent.js';
|
||||
import KeyboardNavigationHandler from '../KeyboardNavigationHandler.js';
|
||||
import ChartUtilities from '../Utils/ChartUtilities.js';
|
||||
var unhideChartElementFromAT = ChartUtilities.unhideChartElementFromAT;
|
||||
import HTMLUtilities from '../Utils/HTMLUtilities.js';
|
||||
var setElAttrs = HTMLUtilities.setElAttrs;
|
||||
/* eslint-disable no-invalid-this, valid-jsdoc */
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
function shouldRunInputNavigation(chart) {
|
||||
var inputVisible = (chart.rangeSelector &&
|
||||
chart.rangeSelector.inputGroup &&
|
||||
chart.rangeSelector.inputGroup.element
|
||||
.getAttribute('visibility') !== 'hidden');
|
||||
return (inputVisible &&
|
||||
chart.options.rangeSelector.inputEnabled !== false &&
|
||||
chart.rangeSelector.minInput &&
|
||||
chart.rangeSelector.maxInput);
|
||||
}
|
||||
/**
|
||||
* Highlight range selector button by index.
|
||||
*
|
||||
* @private
|
||||
* @function Highcharts.Chart#highlightRangeSelectorButton
|
||||
*
|
||||
* @param {number} ix
|
||||
*
|
||||
* @return {boolean}
|
||||
*/
|
||||
H.Chart.prototype.highlightRangeSelectorButton = function (ix) {
|
||||
var buttons = this.rangeSelector.buttons, curSelectedIx = this.highlightedRangeSelectorItemIx;
|
||||
// Deselect old
|
||||
if (typeof curSelectedIx !== 'undefined' && buttons[curSelectedIx]) {
|
||||
buttons[curSelectedIx].setState(this.oldRangeSelectorItemState || 0);
|
||||
}
|
||||
// Select new
|
||||
this.highlightedRangeSelectorItemIx = ix;
|
||||
if (buttons[ix]) {
|
||||
this.setFocusToElement(buttons[ix].box, buttons[ix].element);
|
||||
this.oldRangeSelectorItemState = buttons[ix].state;
|
||||
buttons[ix].setState(2);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
/**
|
||||
* The RangeSelectorComponent class
|
||||
*
|
||||
* @private
|
||||
* @class
|
||||
* @name Highcharts.RangeSelectorComponent
|
||||
*/
|
||||
var RangeSelectorComponent = function () { };
|
||||
RangeSelectorComponent.prototype = new AccessibilityComponent();
|
||||
extend(RangeSelectorComponent.prototype, /** @lends Highcharts.RangeSelectorComponent */ {
|
||||
/**
|
||||
* Called on first render/updates to the chart, including options changes.
|
||||
*/
|
||||
onChartUpdate: function () {
|
||||
var chart = this.chart, component = this, rangeSelector = chart.rangeSelector;
|
||||
if (!rangeSelector) {
|
||||
return;
|
||||
}
|
||||
if (rangeSelector.buttons && rangeSelector.buttons.length) {
|
||||
rangeSelector.buttons.forEach(function (button) {
|
||||
unhideChartElementFromAT(chart, button.element);
|
||||
component.setRangeButtonAttrs(button);
|
||||
});
|
||||
}
|
||||
// Make sure input boxes are accessible and focusable
|
||||
if (rangeSelector.maxInput && rangeSelector.minInput) {
|
||||
['minInput', 'maxInput'].forEach(function (key, i) {
|
||||
var input = rangeSelector[key];
|
||||
if (input) {
|
||||
unhideChartElementFromAT(chart, input);
|
||||
component.setRangeInputAttrs(input, 'accessibility.rangeSelector.' + (i ? 'max' : 'min') +
|
||||
'InputLabel');
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Highcharts.SVGElement} button
|
||||
*/
|
||||
setRangeButtonAttrs: function (button) {
|
||||
var chart = this.chart, label = chart.langFormat('accessibility.rangeSelector.buttonText', {
|
||||
chart: chart,
|
||||
buttonText: button.text && button.text.textStr
|
||||
});
|
||||
setElAttrs(button.element, {
|
||||
tabindex: -1,
|
||||
role: 'button',
|
||||
'aria-label': label
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
setRangeInputAttrs: function (input, langKey) {
|
||||
var chart = this.chart;
|
||||
setElAttrs(input, {
|
||||
tabindex: -1,
|
||||
role: 'textbox',
|
||||
'aria-label': chart.langFormat(langKey, { chart: chart })
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Get navigation for the range selector buttons.
|
||||
* @private
|
||||
* @return {Highcharts.KeyboardNavigationHandler} The module object.
|
||||
*/
|
||||
getRangeSelectorButtonNavigation: function () {
|
||||
var chart = this.chart, keys = this.keyCodes, component = this;
|
||||
return new KeyboardNavigationHandler(chart, {
|
||||
keyCodeMap: [
|
||||
[
|
||||
[keys.left, keys.right, keys.up, keys.down],
|
||||
function (keyCode) {
|
||||
return component.onButtonNavKbdArrowKey(this, keyCode);
|
||||
}
|
||||
],
|
||||
[
|
||||
[keys.enter, keys.space],
|
||||
function () {
|
||||
return component.onButtonNavKbdClick(this);
|
||||
}
|
||||
]
|
||||
],
|
||||
validate: function () {
|
||||
var hasRangeSelector = chart.rangeSelector &&
|
||||
chart.rangeSelector.buttons &&
|
||||
chart.rangeSelector.buttons.length;
|
||||
return hasRangeSelector;
|
||||
},
|
||||
init: function (direction) {
|
||||
var lastButtonIx = (chart.rangeSelector.buttons.length - 1);
|
||||
chart.highlightRangeSelectorButton(direction > 0 ? 0 : lastButtonIx);
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Highcharts.KeyboardNavigationHandler} keyboardNavigationHandler
|
||||
* @param {number} keyCode
|
||||
* @return {number} Response code
|
||||
*/
|
||||
onButtonNavKbdArrowKey: function (keyboardNavigationHandler, keyCode) {
|
||||
var response = keyboardNavigationHandler.response, keys = this.keyCodes, chart = this.chart, wrapAround = chart.options.accessibility
|
||||
.keyboardNavigation.wrapAround, direction = (keyCode === keys.left || keyCode === keys.up) ? -1 : 1, didHighlight = chart.highlightRangeSelectorButton(chart.highlightedRangeSelectorItemIx + direction);
|
||||
if (!didHighlight) {
|
||||
if (wrapAround) {
|
||||
keyboardNavigationHandler.init(direction);
|
||||
return response.success;
|
||||
}
|
||||
return response[direction > 0 ? 'next' : 'prev'];
|
||||
}
|
||||
return response.success;
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
onButtonNavKbdClick: function (keyboardNavigationHandler) {
|
||||
var response = keyboardNavigationHandler.response, chart = this.chart, wasDisabled = chart.oldRangeSelectorItemState === 3;
|
||||
if (!wasDisabled) {
|
||||
this.fakeClickEvent(chart.rangeSelector.buttons[chart.highlightedRangeSelectorItemIx].element);
|
||||
}
|
||||
return response.success;
|
||||
},
|
||||
/**
|
||||
* Get navigation for the range selector input boxes.
|
||||
* @private
|
||||
* @return {Highcharts.KeyboardNavigationHandler}
|
||||
* The module object.
|
||||
*/
|
||||
getRangeSelectorInputNavigation: function () {
|
||||
var chart = this.chart, keys = this.keyCodes, component = this;
|
||||
return new KeyboardNavigationHandler(chart, {
|
||||
keyCodeMap: [
|
||||
[
|
||||
[
|
||||
keys.tab, keys.up, keys.down
|
||||
],
|
||||
function (keyCode, e) {
|
||||
var direction = (keyCode === keys.tab && e.shiftKey ||
|
||||
keyCode === keys.up) ? -1 : 1;
|
||||
return component.onInputKbdMove(this, direction);
|
||||
}
|
||||
]
|
||||
],
|
||||
validate: function () {
|
||||
return shouldRunInputNavigation(chart);
|
||||
},
|
||||
init: function (direction) {
|
||||
component.onInputNavInit(direction);
|
||||
},
|
||||
terminate: function () {
|
||||
component.onInputNavTerminate();
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Highcharts.KeyboardNavigationHandler} keyboardNavigationHandler
|
||||
* @param {number} direction
|
||||
* @return {number} Response code
|
||||
*/
|
||||
onInputKbdMove: function (keyboardNavigationHandler, direction) {
|
||||
var chart = this.chart, response = keyboardNavigationHandler.response, newIx = chart.highlightedInputRangeIx =
|
||||
chart.highlightedInputRangeIx + direction, newIxOutOfRange = newIx > 1 || newIx < 0;
|
||||
if (newIxOutOfRange) {
|
||||
return response[direction > 0 ? 'next' : 'prev'];
|
||||
}
|
||||
chart.rangeSelector[newIx ? 'maxInput' : 'minInput'].focus();
|
||||
return response.success;
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {number} direction
|
||||
*/
|
||||
onInputNavInit: function (direction) {
|
||||
var chart = this.chart, buttonIxToHighlight = direction > 0 ? 0 : 1;
|
||||
chart.highlightedInputRangeIx = buttonIxToHighlight;
|
||||
chart.rangeSelector[buttonIxToHighlight ? 'maxInput' : 'minInput'].focus();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
onInputNavTerminate: function () {
|
||||
var rangeSel = (this.chart.rangeSelector || {});
|
||||
if (rangeSel.maxInput) {
|
||||
rangeSel.hideInput('max');
|
||||
}
|
||||
if (rangeSel.minInput) {
|
||||
rangeSel.hideInput('min');
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Get keyboard navigation handlers for this component.
|
||||
* @return {Array<Highcharts.KeyboardNavigationHandler>}
|
||||
* List of module objects.
|
||||
*/
|
||||
getKeyboardNavigation: function () {
|
||||
return [
|
||||
this.getRangeSelectorButtonNavigation(),
|
||||
this.getRangeSelectorInputNavigation()
|
||||
];
|
||||
}
|
||||
});
|
||||
export default RangeSelectorComponent;
|
||||
@@ -0,0 +1,171 @@
|
||||
/* *
|
||||
*
|
||||
* (c) 2009-2020 Øystein Moseng
|
||||
*
|
||||
* Handle forcing series markers.
|
||||
*
|
||||
* License: www.highcharts.com/license
|
||||
*
|
||||
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
|
||||
*
|
||||
* */
|
||||
'use strict';
|
||||
import H from '../../../Core/Globals.js';
|
||||
import U from '../../../Core/Utilities.js';
|
||||
var addEvent = U.addEvent, merge = U.merge;
|
||||
/* eslint-disable no-invalid-this, valid-jsdoc */
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
function isWithinDescriptionThreshold(series) {
|
||||
var a11yOptions = series.chart.options.accessibility;
|
||||
return series.points.length <
|
||||
a11yOptions.series.pointDescriptionEnabledThreshold ||
|
||||
a11yOptions.series.pointDescriptionEnabledThreshold === false;
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
function shouldForceMarkers(series) {
|
||||
var chart = series.chart, chartA11yEnabled = chart.options.accessibility.enabled, seriesA11yEnabled = (series.options.accessibility &&
|
||||
series.options.accessibility.enabled) !== false;
|
||||
return chartA11yEnabled && seriesA11yEnabled && isWithinDescriptionThreshold(series);
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
function hasIndividualPointMarkerOptions(series) {
|
||||
return !!(series._hasPointMarkers && series.points && series.points.length);
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
function unforceSeriesMarkerOptions(series) {
|
||||
var resetMarkerOptions = series.resetA11yMarkerOptions;
|
||||
if (resetMarkerOptions) {
|
||||
merge(true, series.options, {
|
||||
marker: {
|
||||
enabled: resetMarkerOptions.enabled,
|
||||
states: {
|
||||
normal: {
|
||||
opacity: resetMarkerOptions.states &&
|
||||
resetMarkerOptions.states.normal &&
|
||||
resetMarkerOptions.states.normal.opacity
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
function forceZeroOpacityMarkerOptions(options) {
|
||||
merge(true, options, {
|
||||
marker: {
|
||||
enabled: true,
|
||||
states: {
|
||||
normal: {
|
||||
opacity: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
function getPointMarkerOpacity(pointOptions) {
|
||||
return pointOptions.marker.states &&
|
||||
pointOptions.marker.states.normal &&
|
||||
pointOptions.marker.states.normal.opacity || 1;
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
function unforcePointMarkerOptions(pointOptions) {
|
||||
merge(true, pointOptions.marker, {
|
||||
states: {
|
||||
normal: {
|
||||
opacity: getPointMarkerOpacity(pointOptions)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
function handleForcePointMarkers(series) {
|
||||
var i = series.points.length;
|
||||
while (i--) {
|
||||
var point = series.points[i];
|
||||
var pointOptions = point.options;
|
||||
delete point.hasForcedA11yMarker;
|
||||
if (pointOptions.marker) {
|
||||
if (pointOptions.marker.enabled) {
|
||||
unforcePointMarkerOptions(pointOptions);
|
||||
point.hasForcedA11yMarker = false;
|
||||
}
|
||||
else {
|
||||
forceZeroOpacityMarkerOptions(pointOptions);
|
||||
point.hasForcedA11yMarker = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
function addForceMarkersEvents() {
|
||||
/**
|
||||
* Keep track of forcing markers.
|
||||
* @private
|
||||
*/
|
||||
addEvent(H.Series, 'render', function () {
|
||||
var series = this, options = series.options;
|
||||
if (shouldForceMarkers(series)) {
|
||||
if (options.marker && options.marker.enabled === false) {
|
||||
series.a11yMarkersForced = true;
|
||||
forceZeroOpacityMarkerOptions(series.options);
|
||||
}
|
||||
if (hasIndividualPointMarkerOptions(series)) {
|
||||
handleForcePointMarkers(series);
|
||||
}
|
||||
}
|
||||
else if (series.a11yMarkersForced) {
|
||||
delete series.a11yMarkersForced;
|
||||
unforceSeriesMarkerOptions(series);
|
||||
}
|
||||
});
|
||||
/**
|
||||
* Keep track of options to reset markers to if no longer forced.
|
||||
* @private
|
||||
*/
|
||||
addEvent(H.Series, 'afterSetOptions', function (e) {
|
||||
this.resetA11yMarkerOptions = merge(e.options.marker || {}, this.userOptions.marker || {});
|
||||
});
|
||||
/**
|
||||
* Process marker graphics after render
|
||||
* @private
|
||||
*/
|
||||
addEvent(H.Series, 'afterRender', function () {
|
||||
var series = this;
|
||||
// For styled mode the rendered graphic does not reflect the style
|
||||
// options, and we need to add/remove classes to achieve the same.
|
||||
if (series.chart.styledMode) {
|
||||
if (series.markerGroup) {
|
||||
series.markerGroup[series.a11yMarkersForced ? 'addClass' : 'removeClass']('highcharts-a11y-markers-hidden');
|
||||
}
|
||||
// Do we need to handle individual points?
|
||||
if (hasIndividualPointMarkerOptions(series)) {
|
||||
series.points.forEach(function (point) {
|
||||
if (point.graphic) {
|
||||
point.graphic[point.hasForcedA11yMarker ? 'addClass' : 'removeClass']('highcharts-a11y-marker-hidden');
|
||||
point.graphic[point.hasForcedA11yMarker === false ? 'addClass' : 'removeClass']('highcharts-a11y-marker-visible');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
export default addForceMarkersEvents;
|
||||
@@ -0,0 +1,254 @@
|
||||
/* *
|
||||
*
|
||||
* (c) 2009-2020 Øystein Moseng
|
||||
*
|
||||
* Handle announcing new data for a chart.
|
||||
*
|
||||
* License: www.highcharts.com/license
|
||||
*
|
||||
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
|
||||
*
|
||||
* */
|
||||
'use strict';
|
||||
import H from '../../../Core/Globals.js';
|
||||
import U from '../../../Core/Utilities.js';
|
||||
var extend = U.extend, defined = U.defined;
|
||||
import ChartUtilities from '../../Utils/ChartUtilities.js';
|
||||
var getChartTitle = ChartUtilities.getChartTitle;
|
||||
import SeriesDescriber from './SeriesDescriber.js';
|
||||
var defaultPointDescriptionFormatter = SeriesDescriber
|
||||
.defaultPointDescriptionFormatter, defaultSeriesDescriptionFormatter = SeriesDescriber
|
||||
.defaultSeriesDescriptionFormatter;
|
||||
import Announcer from '../../Utils/Announcer.js';
|
||||
import EventProvider from '../../Utils/EventProvider.js';
|
||||
/* eslint-disable no-invalid-this, valid-jsdoc */
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
function chartHasAnnounceEnabled(chart) {
|
||||
return !!chart.options.accessibility.announceNewData.enabled;
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
function findPointInDataArray(point) {
|
||||
var candidates = point.series.data.filter(function (candidate) {
|
||||
return point.x === candidate.x && point.y === candidate.y;
|
||||
});
|
||||
return candidates.length === 1 ? candidates[0] : point;
|
||||
}
|
||||
/**
|
||||
* Get array of unique series from two arrays
|
||||
* @private
|
||||
*/
|
||||
function getUniqueSeries(arrayA, arrayB) {
|
||||
var uniqueSeries = (arrayA || []).concat(arrayB || [])
|
||||
.reduce(function (acc, cur) {
|
||||
acc[cur.name + cur.index] = cur;
|
||||
return acc;
|
||||
}, {});
|
||||
return Object.keys(uniqueSeries).map(function (ix) {
|
||||
return uniqueSeries[ix];
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
* @class
|
||||
*/
|
||||
var NewDataAnnouncer = function (chart) {
|
||||
this.chart = chart;
|
||||
};
|
||||
extend(NewDataAnnouncer.prototype, {
|
||||
/**
|
||||
* Initialize the new data announcer.
|
||||
* @private
|
||||
*/
|
||||
init: function () {
|
||||
var chart = this.chart;
|
||||
var announceOptions = chart.options.accessibility.announceNewData;
|
||||
var announceType = announceOptions.interruptUser ? 'assertive' : 'polite';
|
||||
this.lastAnnouncementTime = 0;
|
||||
this.dirty = {
|
||||
allSeries: {}
|
||||
};
|
||||
this.eventProvider = new EventProvider();
|
||||
this.announcer = new Announcer(chart, announceType);
|
||||
this.addEventListeners();
|
||||
},
|
||||
/**
|
||||
* Remove traces of announcer.
|
||||
* @private
|
||||
*/
|
||||
destroy: function () {
|
||||
this.eventProvider.removeAddedEvents();
|
||||
this.announcer.destroy();
|
||||
},
|
||||
/**
|
||||
* Add event listeners for the announcer
|
||||
* @private
|
||||
*/
|
||||
addEventListeners: function () {
|
||||
var announcer = this, chart = this.chart, e = this.eventProvider;
|
||||
e.addEvent(chart, 'afterDrilldown', function () {
|
||||
announcer.lastAnnouncementTime = 0;
|
||||
});
|
||||
e.addEvent(H.Series, 'updatedData', function () {
|
||||
announcer.onSeriesUpdatedData(this);
|
||||
});
|
||||
e.addEvent(chart, 'afterAddSeries', function (e) {
|
||||
announcer.onSeriesAdded(e.series);
|
||||
});
|
||||
e.addEvent(H.Series, 'addPoint', function (e) {
|
||||
announcer.onPointAdded(e.point);
|
||||
});
|
||||
e.addEvent(chart, 'redraw', function () {
|
||||
announcer.announceDirtyData();
|
||||
});
|
||||
},
|
||||
/**
|
||||
* On new data in the series, make sure we add it to the dirty list.
|
||||
* @private
|
||||
* @param {Highcharts.Series} series
|
||||
*/
|
||||
onSeriesUpdatedData: function (series) {
|
||||
var chart = this.chart;
|
||||
if (series.chart === chart && chartHasAnnounceEnabled(chart)) {
|
||||
this.dirty.hasDirty = true;
|
||||
this.dirty.allSeries[series.name + series.index] = series;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* On new data series added, update dirty list.
|
||||
* @private
|
||||
* @param {Highcharts.Series} series
|
||||
*/
|
||||
onSeriesAdded: function (series) {
|
||||
if (chartHasAnnounceEnabled(this.chart)) {
|
||||
this.dirty.hasDirty = true;
|
||||
this.dirty.allSeries[series.name + series.index] = series;
|
||||
// Add it to newSeries storage unless we already have one
|
||||
this.dirty.newSeries = defined(this.dirty.newSeries) ?
|
||||
void 0 : series;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* On new point added, update dirty list.
|
||||
* @private
|
||||
* @param {Highcharts.Point} point
|
||||
*/
|
||||
onPointAdded: function (point) {
|
||||
var chart = point.series.chart;
|
||||
if (this.chart === chart && chartHasAnnounceEnabled(chart)) {
|
||||
// Add it to newPoint storage unless we already have one
|
||||
this.dirty.newPoint = defined(this.dirty.newPoint) ?
|
||||
void 0 : point;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Gather what we know and announce the data to user.
|
||||
* @private
|
||||
*/
|
||||
announceDirtyData: function () {
|
||||
var chart = this.chart, announcer = this;
|
||||
if (chart.options.accessibility.announceNewData &&
|
||||
this.dirty.hasDirty) {
|
||||
var newPoint = this.dirty.newPoint;
|
||||
// If we have a single new point, see if we can find it in the
|
||||
// data array. Otherwise we can only pass through options to
|
||||
// the description builder, and it is a bit sparse in info.
|
||||
if (newPoint) {
|
||||
newPoint = findPointInDataArray(newPoint);
|
||||
}
|
||||
this.queueAnnouncement(Object.keys(this.dirty.allSeries).map(function (ix) {
|
||||
return announcer.dirty.allSeries[ix];
|
||||
}), this.dirty.newSeries, newPoint);
|
||||
// Reset
|
||||
this.dirty = {
|
||||
allSeries: {}
|
||||
};
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Announce to user that there is new data.
|
||||
* @private
|
||||
* @param {Array<Highcharts.Series>} dirtySeries
|
||||
* Array of series with new data.
|
||||
* @param {Highcharts.Series} [newSeries]
|
||||
* If a single new series was added, a reference to this series.
|
||||
* @param {Highcharts.Point} [newPoint]
|
||||
* If a single point was added, a reference to this point.
|
||||
*/
|
||||
queueAnnouncement: function (dirtySeries, newSeries, newPoint) {
|
||||
var _this = this;
|
||||
var chart = this.chart;
|
||||
var annOptions = chart.options.accessibility.announceNewData;
|
||||
if (annOptions.enabled) {
|
||||
var now = +new Date();
|
||||
var dTime = now - this.lastAnnouncementTime;
|
||||
var time = Math.max(0, annOptions.minAnnounceInterval - dTime);
|
||||
// Add series from previously queued announcement.
|
||||
var allSeries = getUniqueSeries(this.queuedAnnouncement && this.queuedAnnouncement.series, dirtySeries);
|
||||
// Build message and announce
|
||||
var message = this.buildAnnouncementMessage(allSeries, newSeries, newPoint);
|
||||
if (message) {
|
||||
// Is there already one queued?
|
||||
if (this.queuedAnnouncement) {
|
||||
clearTimeout(this.queuedAnnouncementTimer);
|
||||
}
|
||||
// Build the announcement
|
||||
this.queuedAnnouncement = {
|
||||
time: now,
|
||||
message: message,
|
||||
series: allSeries
|
||||
};
|
||||
// Queue the announcement
|
||||
this.queuedAnnouncementTimer = setTimeout(function () {
|
||||
if (_this && _this.announcer) {
|
||||
_this.lastAnnouncementTime = +new Date();
|
||||
_this.announcer.announce(_this.queuedAnnouncement.message);
|
||||
delete _this.queuedAnnouncement;
|
||||
delete _this.queuedAnnouncementTimer;
|
||||
}
|
||||
}, time);
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Get announcement message for new data.
|
||||
* @private
|
||||
* @param {Array<Highcharts.Series>} dirtySeries
|
||||
* Array of series with new data.
|
||||
* @param {Highcharts.Series} [newSeries]
|
||||
* If a single new series was added, a reference to this series.
|
||||
* @param {Highcharts.Point} [newPoint]
|
||||
* If a single point was added, a reference to this point.
|
||||
*
|
||||
* @return {string|null}
|
||||
* The announcement message to give to user.
|
||||
*/
|
||||
buildAnnouncementMessage: function (dirtySeries, newSeries, newPoint) {
|
||||
var chart = this.chart, annOptions = chart.options.accessibility.announceNewData;
|
||||
// User supplied formatter?
|
||||
if (annOptions.announcementFormatter) {
|
||||
var formatterRes = annOptions.announcementFormatter(dirtySeries, newSeries, newPoint);
|
||||
if (formatterRes !== false) {
|
||||
return formatterRes.length ? formatterRes : null;
|
||||
}
|
||||
}
|
||||
// Default formatter - use lang options
|
||||
var multiple = H.charts && H.charts.length > 1 ? 'Multiple' : 'Single', langKey = newSeries ? 'newSeriesAnnounce' + multiple :
|
||||
newPoint ? 'newPointAnnounce' + multiple : 'newDataAnnounce', chartTitle = getChartTitle(chart);
|
||||
return chart.langFormat('accessibility.announceNewData.' + langKey, {
|
||||
chartTitle: chartTitle,
|
||||
seriesDesc: newSeries ?
|
||||
defaultSeriesDescriptionFormatter(newSeries) :
|
||||
null,
|
||||
pointDesc: newPoint ?
|
||||
defaultPointDescriptionFormatter(newPoint) :
|
||||
null,
|
||||
point: newPoint,
|
||||
series: newSeries
|
||||
});
|
||||
}
|
||||
});
|
||||
export default NewDataAnnouncer;
|
||||
@@ -0,0 +1,109 @@
|
||||
/* *
|
||||
*
|
||||
* (c) 2009-2020 Øystein Moseng
|
||||
*
|
||||
* Accessibility component for series and points.
|
||||
*
|
||||
* License: www.highcharts.com/license
|
||||
*
|
||||
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
|
||||
*
|
||||
* */
|
||||
'use strict';
|
||||
import H from '../../../Core/Globals.js';
|
||||
import U from '../../../Core/Utilities.js';
|
||||
var extend = U.extend;
|
||||
import AccessibilityComponent from '../../AccessibilityComponent.js';
|
||||
import SeriesKeyboardNavigation from './SeriesKeyboardNavigation.js';
|
||||
import NewDataAnnouncer from './NewDataAnnouncer.js';
|
||||
import addForceMarkersEvents from './ForcedMarkers.js';
|
||||
import ChartUtilities from '../../Utils/ChartUtilities.js';
|
||||
var hideSeriesFromAT = ChartUtilities.hideSeriesFromAT;
|
||||
import SeriesDescriber from './SeriesDescriber.js';
|
||||
var describeSeries = SeriesDescriber.describeSeries;
|
||||
import Tooltip from '../../../Core/Tooltip.js';
|
||||
// Expose functionality to users
|
||||
H.SeriesAccessibilityDescriber = SeriesDescriber;
|
||||
// Handle forcing markers
|
||||
addForceMarkersEvents();
|
||||
/* eslint-disable no-invalid-this, valid-jsdoc */
|
||||
/**
|
||||
* The SeriesComponent class
|
||||
*
|
||||
* @private
|
||||
* @class
|
||||
* @name Highcharts.SeriesComponent
|
||||
*/
|
||||
var SeriesComponent = function () { };
|
||||
SeriesComponent.prototype = new AccessibilityComponent();
|
||||
extend(SeriesComponent.prototype, /** @lends Highcharts.SeriesComponent */ {
|
||||
/**
|
||||
* Init the component.
|
||||
*/
|
||||
init: function () {
|
||||
this.newDataAnnouncer = new NewDataAnnouncer(this.chart);
|
||||
this.newDataAnnouncer.init();
|
||||
this.keyboardNavigation = new SeriesKeyboardNavigation(this.chart, this.keyCodes);
|
||||
this.keyboardNavigation.init();
|
||||
this.hideTooltipFromATWhenShown();
|
||||
this.hideSeriesLabelsFromATWhenShown();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
hideTooltipFromATWhenShown: function () {
|
||||
var component = this;
|
||||
this.addEvent(Tooltip, 'refresh', function () {
|
||||
if (this.chart === component.chart &&
|
||||
this.label &&
|
||||
this.label.element) {
|
||||
this.label.element.setAttribute('aria-hidden', true);
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
hideSeriesLabelsFromATWhenShown: function () {
|
||||
this.addEvent(this.chart, 'afterDrawSeriesLabels', function () {
|
||||
this.series.forEach(function (series) {
|
||||
if (series.labelBySeries) {
|
||||
series.labelBySeries.attr('aria-hidden', true);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Called on chart render. It is necessary to do this for render in case
|
||||
* markers change on zoom/pixel density.
|
||||
*/
|
||||
onChartRender: function () {
|
||||
var chart = this.chart;
|
||||
chart.series.forEach(function (series) {
|
||||
var shouldDescribeSeries = (series.options.accessibility &&
|
||||
series.options.accessibility.enabled) !== false &&
|
||||
series.visible;
|
||||
if (shouldDescribeSeries) {
|
||||
describeSeries(series);
|
||||
}
|
||||
else {
|
||||
hideSeriesFromAT(series);
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Get keyboard navigation handler for this component.
|
||||
* @return {Highcharts.KeyboardNavigationHandler}
|
||||
*/
|
||||
getKeyboardNavigation: function () {
|
||||
return this.keyboardNavigation.getKeyboardNavigationHandler();
|
||||
},
|
||||
/**
|
||||
* Remove traces
|
||||
*/
|
||||
destroy: function () {
|
||||
this.newDataAnnouncer.destroy();
|
||||
this.keyboardNavigation.destroy();
|
||||
}
|
||||
});
|
||||
export default SeriesComponent;
|
||||
@@ -0,0 +1,397 @@
|
||||
/* *
|
||||
*
|
||||
* (c) 2009-2020 Øystein Moseng
|
||||
*
|
||||
* Place desriptions on a series and its points.
|
||||
*
|
||||
* License: www.highcharts.com/license
|
||||
*
|
||||
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
|
||||
*
|
||||
* */
|
||||
'use strict';
|
||||
import U from '../../../Core/Utilities.js';
|
||||
var find = U.find, format = U.format, isNumber = U.isNumber, numberFormat = U.numberFormat, pick = U.pick, defined = U.defined;
|
||||
import AnnotationsA11y from '../AnnotationsA11y.js';
|
||||
var getPointAnnotationTexts = AnnotationsA11y.getPointAnnotationTexts;
|
||||
import HTMLUtilities from '../../Utils/HTMLUtilities.js';
|
||||
var escapeStringForHTML = HTMLUtilities.escapeStringForHTML, reverseChildNodes = HTMLUtilities.reverseChildNodes, stripHTMLTags = HTMLUtilities.stripHTMLTagsFromString;
|
||||
import ChartUtilities from '../../Utils/ChartUtilities.js';
|
||||
var getAxisDescription = ChartUtilities.getAxisDescription, getSeriesFirstPointElement = ChartUtilities.getSeriesFirstPointElement, getSeriesA11yElement = ChartUtilities.getSeriesA11yElement, unhideChartElementFromAT = ChartUtilities.unhideChartElementFromAT;
|
||||
import Tooltip from '../../../Core/Tooltip.js';
|
||||
/* eslint-disable valid-jsdoc */
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
function findFirstPointWithGraphic(point) {
|
||||
var sourcePointIndex = point.index;
|
||||
if (!point.series || !point.series.data || !defined(sourcePointIndex)) {
|
||||
return null;
|
||||
}
|
||||
return find(point.series.data, function (p) {
|
||||
return !!(p &&
|
||||
typeof p.index !== 'undefined' &&
|
||||
p.index > sourcePointIndex &&
|
||||
p.graphic &&
|
||||
p.graphic.element);
|
||||
}) || null;
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
function shouldAddDummyPoint(point) {
|
||||
// Note: Sunburst series use isNull for hidden points on drilldown.
|
||||
// Ignore these.
|
||||
var isSunburst = point.series && point.series.is('sunburst'), isNull = point.isNull;
|
||||
return isNull && !isSunburst;
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
function makeDummyElement(point, pos) {
|
||||
var renderer = point.series.chart.renderer, dummy = renderer.rect(pos.x, pos.y, 1, 1);
|
||||
dummy.attr({
|
||||
'class': 'highcharts-a11y-dummy-point',
|
||||
fill: 'none',
|
||||
opacity: 0,
|
||||
'fill-opacity': 0,
|
||||
'stroke-opacity': 0
|
||||
});
|
||||
return dummy;
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
* @param {Highcharts.Point} point
|
||||
* @return {Highcharts.HTMLDOMElement|Highcharts.SVGDOMElement|undefined}
|
||||
*/
|
||||
function addDummyPointElement(point) {
|
||||
var series = point.series, firstPointWithGraphic = findFirstPointWithGraphic(point), firstGraphic = firstPointWithGraphic && firstPointWithGraphic.graphic, parentGroup = firstGraphic ?
|
||||
firstGraphic.parentGroup :
|
||||
series.graph || series.group, dummyPos = firstPointWithGraphic ? {
|
||||
x: pick(point.plotX, firstPointWithGraphic.plotX, 0),
|
||||
y: pick(point.plotY, firstPointWithGraphic.plotY, 0)
|
||||
} : {
|
||||
x: pick(point.plotX, 0),
|
||||
y: pick(point.plotY, 0)
|
||||
}, dummyElement = makeDummyElement(point, dummyPos);
|
||||
if (parentGroup && parentGroup.element) {
|
||||
point.graphic = dummyElement;
|
||||
point.hasDummyGraphic = true;
|
||||
dummyElement.add(parentGroup);
|
||||
// Move to correct pos in DOM
|
||||
parentGroup.element.insertBefore(dummyElement.element, firstGraphic ? firstGraphic.element : null);
|
||||
return dummyElement.element;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
* @param {Highcharts.Series} series
|
||||
* @return {boolean}
|
||||
*/
|
||||
function hasMorePointsThanDescriptionThreshold(series) {
|
||||
var chartA11yOptions = series.chart.options.accessibility, threshold = (chartA11yOptions.series.pointDescriptionEnabledThreshold);
|
||||
return !!(threshold !== false &&
|
||||
series.points &&
|
||||
series.points.length >= threshold);
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
* @param {Highcharts.Series} series
|
||||
* @return {boolean}
|
||||
*/
|
||||
function shouldSetScreenReaderPropsOnPoints(series) {
|
||||
var seriesA11yOptions = series.options.accessibility || {};
|
||||
return !hasMorePointsThanDescriptionThreshold(series) &&
|
||||
!seriesA11yOptions.exposeAsGroupOnly;
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
* @param {Highcharts.Series} series
|
||||
* @return {boolean}
|
||||
*/
|
||||
function shouldSetKeyboardNavPropsOnPoints(series) {
|
||||
var chartA11yOptions = series.chart.options.accessibility, seriesNavOptions = chartA11yOptions.keyboardNavigation.seriesNavigation;
|
||||
return !!(series.points && (series.points.length <
|
||||
seriesNavOptions.pointNavigationEnabledThreshold ||
|
||||
seriesNavOptions.pointNavigationEnabledThreshold === false));
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
* @param {Highcharts.Series} series
|
||||
* @return {boolean}
|
||||
*/
|
||||
function shouldDescribeSeriesElement(series) {
|
||||
var chart = series.chart, chartOptions = chart.options.chart || {}, chartHas3d = chartOptions.options3d && chartOptions.options3d.enabled, hasMultipleSeries = chart.series.length > 1, describeSingleSeriesOption = chart.options.accessibility.series.describeSingleSeries, exposeAsGroupOnlyOption = (series.options.accessibility || {}).exposeAsGroupOnly, noDescribe3D = chartHas3d && hasMultipleSeries;
|
||||
return !noDescribe3D && (hasMultipleSeries || describeSingleSeriesOption ||
|
||||
exposeAsGroupOnlyOption || hasMorePointsThanDescriptionThreshold(series));
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
* @param {Highcharts.Point} point
|
||||
* @param {number} value
|
||||
* @return {string}
|
||||
*/
|
||||
function pointNumberToString(point, value) {
|
||||
var chart = point.series.chart, a11yPointOptions = chart.options.accessibility.point || {}, tooltipOptions = point.series.tooltipOptions || {}, lang = chart.options.lang;
|
||||
if (isNumber(value)) {
|
||||
return numberFormat(value, a11yPointOptions.valueDecimals ||
|
||||
tooltipOptions.valueDecimals ||
|
||||
-1, lang.decimalPoint, lang.accessibility.thousandsSep || lang.thousandsSep);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
* @param {Highcharts.Series} series
|
||||
* @return {string}
|
||||
*/
|
||||
function getSeriesDescriptionText(series) {
|
||||
var seriesA11yOptions = series.options.accessibility || {}, descOpt = seriesA11yOptions.description;
|
||||
return descOpt && series.chart.langFormat('accessibility.series.description', {
|
||||
description: descOpt,
|
||||
series: series
|
||||
}) || '';
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
* @param {Highcharts.series} series
|
||||
* @param {string} axisCollection
|
||||
* @return {string}
|
||||
*/
|
||||
function getSeriesAxisDescriptionText(series, axisCollection) {
|
||||
var axis = series[axisCollection];
|
||||
return series.chart.langFormat('accessibility.series.' + axisCollection + 'Description', {
|
||||
name: getAxisDescription(axis),
|
||||
series: series
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Get accessible time description for a point on a datetime axis.
|
||||
*
|
||||
* @private
|
||||
* @function Highcharts.Point#getTimeDescription
|
||||
* @param {Highcharts.Point} point
|
||||
* @return {string|undefined}
|
||||
* The description as string.
|
||||
*/
|
||||
function getPointA11yTimeDescription(point) {
|
||||
var series = point.series, chart = series.chart, a11yOptions = chart.options.accessibility.point || {}, hasDateXAxis = series.xAxis && series.xAxis.dateTime;
|
||||
if (hasDateXAxis) {
|
||||
var tooltipDateFormat = Tooltip.prototype.getXDateFormat.call({
|
||||
getDateFormat: Tooltip.prototype.getDateFormat,
|
||||
chart: chart
|
||||
}, point, chart.options.tooltip, series.xAxis), dateFormat = a11yOptions.dateFormatter &&
|
||||
a11yOptions.dateFormatter(point) ||
|
||||
a11yOptions.dateFormat ||
|
||||
tooltipDateFormat;
|
||||
return chart.time.dateFormat(dateFormat, point.x, void 0);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
* @param {Highcharts.Point} point
|
||||
* @return {string}
|
||||
*/
|
||||
function getPointXDescription(point) {
|
||||
var timeDesc = getPointA11yTimeDescription(point), xAxis = point.series.xAxis || {}, pointCategory = xAxis.categories && defined(point.category) &&
|
||||
('' + point.category).replace('<br/>', ' '), canUseId = point.id && point.id.indexOf('highcharts-') < 0, fallback = 'x, ' + point.x;
|
||||
return point.name || timeDesc || pointCategory ||
|
||||
(canUseId ? point.id : fallback);
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
* @param {Highcharts.Point} point
|
||||
* @param {string} prefix
|
||||
* @param {string} suffix
|
||||
* @return {string}
|
||||
*/
|
||||
function getPointArrayMapValueDescription(point, prefix, suffix) {
|
||||
var pre = prefix || '', suf = suffix || '', keyToValStr = function (key) {
|
||||
var num = pointNumberToString(point, pick(point[key], point.options[key]));
|
||||
return key + ': ' + pre + num + suf;
|
||||
}, pointArrayMap = point.series.pointArrayMap;
|
||||
return pointArrayMap.reduce(function (desc, key) {
|
||||
return desc + (desc.length ? ', ' : '') + keyToValStr(key);
|
||||
}, '');
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
* @param {Highcharts.Point} point
|
||||
* @return {string}
|
||||
*/
|
||||
function getPointValue(point) {
|
||||
var series = point.series, a11yPointOpts = series.chart.options.accessibility.point || {}, tooltipOptions = series.tooltipOptions || {}, valuePrefix = a11yPointOpts.valuePrefix ||
|
||||
tooltipOptions.valuePrefix || '', valueSuffix = a11yPointOpts.valueSuffix ||
|
||||
tooltipOptions.valueSuffix || '', fallbackKey = (typeof point.value !==
|
||||
'undefined' ?
|
||||
'value' : 'y'), fallbackDesc = pointNumberToString(point, point[fallbackKey]);
|
||||
if (point.isNull) {
|
||||
return series.chart.langFormat('accessibility.series.nullPointValue', {
|
||||
point: point
|
||||
});
|
||||
}
|
||||
if (series.pointArrayMap) {
|
||||
return getPointArrayMapValueDescription(point, valuePrefix, valueSuffix);
|
||||
}
|
||||
return valuePrefix + fallbackDesc + valueSuffix;
|
||||
}
|
||||
/**
|
||||
* Return the description for the annotation(s) connected to a point, or empty
|
||||
* string if none.
|
||||
*
|
||||
* @private
|
||||
* @param {Highcharts.Point} point The data point to get the annotation info from.
|
||||
* @return {string} Annotation description
|
||||
*/
|
||||
function getPointAnnotationDescription(point) {
|
||||
var chart = point.series.chart;
|
||||
var langKey = 'accessibility.series.pointAnnotationsDescription';
|
||||
var annotations = getPointAnnotationTexts(point);
|
||||
var context = { point: point, annotations: annotations };
|
||||
return annotations.length ? chart.langFormat(langKey, context) : '';
|
||||
}
|
||||
/**
|
||||
* Return string with information about point.
|
||||
* @private
|
||||
* @return {string}
|
||||
*/
|
||||
function getPointValueDescription(point) {
|
||||
var series = point.series, chart = series.chart, pointValueDescriptionFormat = chart.options.accessibility
|
||||
.point.valueDescriptionFormat, showXDescription = pick(series.xAxis &&
|
||||
series.xAxis.options.accessibility &&
|
||||
series.xAxis.options.accessibility.enabled, !chart.angular), xDesc = showXDescription ? getPointXDescription(point) : '', context = {
|
||||
point: point,
|
||||
index: defined(point.index) ? (point.index + 1) : '',
|
||||
xDescription: xDesc,
|
||||
value: getPointValue(point),
|
||||
separator: showXDescription ? ', ' : ''
|
||||
};
|
||||
return format(pointValueDescriptionFormat, context, chart);
|
||||
}
|
||||
/**
|
||||
* Return string with information about point.
|
||||
* @private
|
||||
* @return {string}
|
||||
*/
|
||||
function defaultPointDescriptionFormatter(point) {
|
||||
var series = point.series, chart = series.chart, valText = getPointValueDescription(point), description = point.options && point.options.accessibility &&
|
||||
point.options.accessibility.description, userDescText = description ? ' ' + description : '', seriesNameText = chart.series.length > 1 && series.name ?
|
||||
' ' + series.name + '.' : '', annotationsDesc = getPointAnnotationDescription(point), pointAnnotationsText = annotationsDesc ? ' ' + annotationsDesc : '';
|
||||
point.accessibility = point.accessibility || {};
|
||||
point.accessibility.valueDescription = valText;
|
||||
return valText + userDescText + seriesNameText + pointAnnotationsText;
|
||||
}
|
||||
/**
|
||||
* Set a11y props on a point element
|
||||
* @private
|
||||
* @param {Highcharts.Point} point
|
||||
* @param {Highcharts.HTMLDOMElement|Highcharts.SVGDOMElement} pointElement
|
||||
*/
|
||||
function setPointScreenReaderAttribs(point, pointElement) {
|
||||
var series = point.series, a11yPointOptions = series.chart.options.accessibility.point || {}, seriesA11yOptions = series.options.accessibility || {}, label = escapeStringForHTML(stripHTMLTags(seriesA11yOptions.pointDescriptionFormatter &&
|
||||
seriesA11yOptions.pointDescriptionFormatter(point) ||
|
||||
a11yPointOptions.descriptionFormatter &&
|
||||
a11yPointOptions.descriptionFormatter(point) ||
|
||||
defaultPointDescriptionFormatter(point)));
|
||||
pointElement.setAttribute('role', 'img');
|
||||
pointElement.setAttribute('aria-label', label);
|
||||
}
|
||||
/**
|
||||
* Add accessible info to individual point elements of a series
|
||||
* @private
|
||||
* @param {Highcharts.Series} series
|
||||
*/
|
||||
function describePointsInSeries(series) {
|
||||
var setScreenReaderProps = shouldSetScreenReaderPropsOnPoints(series), setKeyboardProps = shouldSetKeyboardNavPropsOnPoints(series);
|
||||
if (setScreenReaderProps || setKeyboardProps) {
|
||||
series.points.forEach(function (point) {
|
||||
var pointEl = point.graphic && point.graphic.element ||
|
||||
shouldAddDummyPoint(point) && addDummyPointElement(point);
|
||||
if (pointEl) {
|
||||
// We always set tabindex, as long as we are setting props.
|
||||
// When setting tabindex, also remove default outline to
|
||||
// avoid ugly border on click.
|
||||
pointEl.setAttribute('tabindex', '-1');
|
||||
pointEl.style.outline = '0';
|
||||
if (setScreenReaderProps) {
|
||||
setPointScreenReaderAttribs(point, pointEl);
|
||||
}
|
||||
else {
|
||||
pointEl.setAttribute('aria-hidden', true);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Return string with information about series.
|
||||
* @private
|
||||
* @return {string}
|
||||
*/
|
||||
function defaultSeriesDescriptionFormatter(series) {
|
||||
var chart = series.chart, chartTypes = chart.types || [], description = getSeriesDescriptionText(series), shouldDescribeAxis = function (coll) {
|
||||
return chart[coll] && chart[coll].length > 1 && series[coll];
|
||||
}, xAxisInfo = getSeriesAxisDescriptionText(series, 'xAxis'), yAxisInfo = getSeriesAxisDescriptionText(series, 'yAxis'), summaryContext = {
|
||||
name: series.name || '',
|
||||
ix: series.index + 1,
|
||||
numSeries: chart.series && chart.series.length,
|
||||
numPoints: series.points && series.points.length,
|
||||
series: series
|
||||
}, combinationSuffix = chartTypes.length > 1 ? 'Combination' : '', summary = chart.langFormat('accessibility.series.summary.' + series.type + combinationSuffix, summaryContext) || chart.langFormat('accessibility.series.summary.default' + combinationSuffix, summaryContext);
|
||||
return summary + (description ? ' ' + description : '') + (shouldDescribeAxis('yAxis') ? ' ' + yAxisInfo : '') + (shouldDescribeAxis('xAxis') ? ' ' + xAxisInfo : '');
|
||||
}
|
||||
/**
|
||||
* Set a11y props on a series element
|
||||
* @private
|
||||
* @param {Highcharts.Series} series
|
||||
* @param {Highcharts.HTMLDOMElement|Highcharts.SVGDOMElement} seriesElement
|
||||
*/
|
||||
function describeSeriesElement(series, seriesElement) {
|
||||
var seriesA11yOptions = series.options.accessibility || {}, a11yOptions = series.chart.options.accessibility, landmarkVerbosity = a11yOptions.landmarkVerbosity;
|
||||
// Handle role attribute
|
||||
if (seriesA11yOptions.exposeAsGroupOnly) {
|
||||
seriesElement.setAttribute('role', 'img');
|
||||
}
|
||||
else if (landmarkVerbosity === 'all') {
|
||||
seriesElement.setAttribute('role', 'region');
|
||||
} /* else do not add role */
|
||||
seriesElement.setAttribute('tabindex', '-1');
|
||||
seriesElement.style.outline = '0'; // Don't show browser outline on click, despite tabindex
|
||||
seriesElement.setAttribute('aria-label', escapeStringForHTML(stripHTMLTags(a11yOptions.series.descriptionFormatter &&
|
||||
a11yOptions.series.descriptionFormatter(series) ||
|
||||
defaultSeriesDescriptionFormatter(series))));
|
||||
}
|
||||
/**
|
||||
* Put accessible info on series and points of a series.
|
||||
* @param {Highcharts.Series} series The series to add info on.
|
||||
*/
|
||||
function describeSeries(series) {
|
||||
var chart = series.chart, firstPointEl = getSeriesFirstPointElement(series), seriesEl = getSeriesA11yElement(series), is3d = chart.is3d && chart.is3d();
|
||||
if (seriesEl) {
|
||||
// For some series types the order of elements do not match the
|
||||
// order of points in series. In that case we have to reverse them
|
||||
// in order for AT to read them out in an understandable order.
|
||||
// Due to z-index issues we can not do this for 3D charts.
|
||||
if (seriesEl.lastChild === firstPointEl && !is3d) {
|
||||
reverseChildNodes(seriesEl);
|
||||
}
|
||||
describePointsInSeries(series);
|
||||
unhideChartElementFromAT(chart, seriesEl);
|
||||
if (shouldDescribeSeriesElement(series)) {
|
||||
describeSeriesElement(series, seriesEl);
|
||||
}
|
||||
else {
|
||||
seriesEl.setAttribute('aria-label', '');
|
||||
}
|
||||
}
|
||||
}
|
||||
var SeriesDescriber = {
|
||||
describeSeries: describeSeries,
|
||||
defaultPointDescriptionFormatter: defaultPointDescriptionFormatter,
|
||||
defaultSeriesDescriptionFormatter: defaultSeriesDescriptionFormatter,
|
||||
getPointA11yTimeDescription: getPointA11yTimeDescription,
|
||||
getPointXDescription: getPointXDescription,
|
||||
getPointValue: getPointValue,
|
||||
getPointValueDescription: getPointValueDescription
|
||||
};
|
||||
export default SeriesDescriber;
|
||||
+569
@@ -0,0 +1,569 @@
|
||||
/* *
|
||||
*
|
||||
* (c) 2009-2020 Øystein Moseng
|
||||
*
|
||||
* Handle keyboard navigation for series.
|
||||
*
|
||||
* License: www.highcharts.com/license
|
||||
*
|
||||
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
|
||||
*
|
||||
* */
|
||||
'use strict';
|
||||
import Chart from '../../../Core/Chart/Chart.js';
|
||||
import H from '../../../Core/Globals.js';
|
||||
import Point from '../../../Core/Series/Point.js';
|
||||
import U from '../../../Core/Utilities.js';
|
||||
var defined = U.defined, extend = U.extend;
|
||||
import KeyboardNavigationHandler from '../../KeyboardNavigationHandler.js';
|
||||
import EventProvider from '../../Utils/EventProvider.js';
|
||||
import ChartUtilities from '../../Utils/ChartUtilities.js';
|
||||
var getPointFromXY = ChartUtilities.getPointFromXY, getSeriesFromName = ChartUtilities.getSeriesFromName, scrollToPoint = ChartUtilities.scrollToPoint;
|
||||
import '../../../Series/ColumnSeries.js';
|
||||
import '../../../Series/PieSeries.js';
|
||||
/* eslint-disable no-invalid-this, valid-jsdoc */
|
||||
/*
|
||||
* Set for which series types it makes sense to move to the closest point with
|
||||
* up/down arrows, and which series types should just move to next series.
|
||||
*/
|
||||
H.Series.prototype.keyboardMoveVertical = true;
|
||||
['column', 'pie'].forEach(function (type) {
|
||||
if (H.seriesTypes[type]) {
|
||||
H.seriesTypes[type].prototype.keyboardMoveVertical = false;
|
||||
}
|
||||
});
|
||||
/**
|
||||
* Get the index of a point in a series. This is needed when using e.g. data
|
||||
* grouping.
|
||||
*
|
||||
* @private
|
||||
* @function getPointIndex
|
||||
*
|
||||
* @param {Highcharts.AccessibilityPoint} point
|
||||
* The point to find index of.
|
||||
*
|
||||
* @return {number|undefined}
|
||||
* The index in the series.points array of the point.
|
||||
*/
|
||||
function getPointIndex(point) {
|
||||
var index = point.index, points = point.series.points, i = points.length;
|
||||
if (points[index] !== point) {
|
||||
while (i--) {
|
||||
if (points[i] === point) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Determine if series navigation should be skipped
|
||||
*
|
||||
* @private
|
||||
* @function isSkipSeries
|
||||
*
|
||||
* @param {Highcharts.Series} series
|
||||
*
|
||||
* @return {boolean|number|undefined}
|
||||
*/
|
||||
function isSkipSeries(series) {
|
||||
var a11yOptions = series.chart.options.accessibility, seriesNavOptions = a11yOptions.keyboardNavigation.seriesNavigation, seriesA11yOptions = series.options.accessibility || {}, seriesKbdNavOptions = seriesA11yOptions.keyboardNavigation;
|
||||
return seriesKbdNavOptions && seriesKbdNavOptions.enabled === false ||
|
||||
seriesA11yOptions.enabled === false ||
|
||||
series.options.enableMouseTracking === false || // #8440
|
||||
!series.visible ||
|
||||
// Skip all points in a series where pointNavigationEnabledThreshold is
|
||||
// reached
|
||||
(seriesNavOptions.pointNavigationEnabledThreshold &&
|
||||
seriesNavOptions.pointNavigationEnabledThreshold <=
|
||||
series.points.length);
|
||||
}
|
||||
/**
|
||||
* Determine if navigation for a point should be skipped
|
||||
*
|
||||
* @private
|
||||
* @function isSkipPoint
|
||||
*
|
||||
* @param {Highcharts.Point} point
|
||||
*
|
||||
* @return {boolean|number|undefined}
|
||||
*/
|
||||
function isSkipPoint(point) {
|
||||
var a11yOptions = point.series.chart.options.accessibility;
|
||||
return point.isNull &&
|
||||
a11yOptions.keyboardNavigation.seriesNavigation.skipNullPoints ||
|
||||
point.visible === false ||
|
||||
isSkipSeries(point.series);
|
||||
}
|
||||
/**
|
||||
* Get the point in a series that is closest (in pixel distance) to a reference
|
||||
* point. Optionally supply weight factors for x and y directions.
|
||||
*
|
||||
* @private
|
||||
* @function getClosestPoint
|
||||
*
|
||||
* @param {Highcharts.Point} point
|
||||
* @param {Highcharts.Series} series
|
||||
* @param {number} [xWeight]
|
||||
* @param {number} [yWeight]
|
||||
*
|
||||
* @return {Highcharts.Point|undefined}
|
||||
*/
|
||||
function getClosestPoint(point, series, xWeight, yWeight) {
|
||||
var minDistance = Infinity, dPoint, minIx, distance, i = series.points.length, hasUndefinedPosition = function (point) {
|
||||
return !(defined(point.plotX) && defined(point.plotY));
|
||||
};
|
||||
if (hasUndefinedPosition(point)) {
|
||||
return;
|
||||
}
|
||||
while (i--) {
|
||||
dPoint = series.points[i];
|
||||
if (hasUndefinedPosition(dPoint)) {
|
||||
continue;
|
||||
}
|
||||
distance = (point.plotX - dPoint.plotX) *
|
||||
(point.plotX - dPoint.plotX) *
|
||||
(xWeight || 1) +
|
||||
(point.plotY - dPoint.plotY) *
|
||||
(point.plotY - dPoint.plotY) *
|
||||
(yWeight || 1);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
minIx = i;
|
||||
}
|
||||
}
|
||||
return defined(minIx) ? series.points[minIx] : void 0;
|
||||
}
|
||||
/**
|
||||
* Highlights a point (show tooltip and display hover state).
|
||||
*
|
||||
* @private
|
||||
* @function Highcharts.Point#highlight
|
||||
*
|
||||
* @return {Highcharts.Point}
|
||||
* This highlighted point.
|
||||
*/
|
||||
Point.prototype.highlight = function () {
|
||||
var chart = this.series.chart;
|
||||
if (!this.isNull) {
|
||||
this.onMouseOver(); // Show the hover marker and tooltip
|
||||
}
|
||||
else {
|
||||
if (chart.tooltip) {
|
||||
chart.tooltip.hide(0);
|
||||
}
|
||||
// Don't call blur on the element, as it messes up the chart div's focus
|
||||
}
|
||||
scrollToPoint(this);
|
||||
// We focus only after calling onMouseOver because the state change can
|
||||
// change z-index and mess up the element.
|
||||
if (this.graphic) {
|
||||
chart.setFocusToElement(this.graphic);
|
||||
}
|
||||
chart.highlightedPoint = this;
|
||||
return this;
|
||||
};
|
||||
/**
|
||||
* Function to highlight next/previous point in chart.
|
||||
*
|
||||
* @private
|
||||
* @function Highcharts.Chart#highlightAdjacentPoint
|
||||
*
|
||||
* @param {boolean} next
|
||||
* Flag for the direction.
|
||||
*
|
||||
* @return {Highcharts.Point|boolean}
|
||||
* Returns highlighted point on success, false on failure (no adjacent
|
||||
* point to highlight in chosen direction).
|
||||
*/
|
||||
Chart.prototype.highlightAdjacentPoint = function (next) {
|
||||
var chart = this, series = chart.series, curPoint = chart.highlightedPoint, curPointIndex = curPoint && getPointIndex(curPoint) || 0, curPoints = (curPoint && curPoint.series.points), lastSeries = chart.series && chart.series[chart.series.length - 1], lastPoint = lastSeries && lastSeries.points &&
|
||||
lastSeries.points[lastSeries.points.length - 1], newSeries, newPoint;
|
||||
// If no points, return false
|
||||
if (!series[0] || !series[0].points) {
|
||||
return false;
|
||||
}
|
||||
if (!curPoint) {
|
||||
// No point is highlighted yet. Try first/last point depending on move
|
||||
// direction
|
||||
newPoint = next ? series[0].points[0] : lastPoint;
|
||||
}
|
||||
else {
|
||||
// We have a highlighted point.
|
||||
// Grab next/prev point & series
|
||||
newSeries = series[curPoint.series.index + (next ? 1 : -1)];
|
||||
newPoint = curPoints[curPointIndex + (next ? 1 : -1)];
|
||||
if (!newPoint && newSeries) {
|
||||
// Done with this series, try next one
|
||||
newPoint = newSeries.points[next ? 0 : newSeries.points.length - 1];
|
||||
}
|
||||
// If there is no adjacent point, we return false
|
||||
if (!newPoint) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Recursively skip points
|
||||
if (isSkipPoint(newPoint)) {
|
||||
// If we skip this whole series, move to the end of the series before we
|
||||
// recurse, just to optimize
|
||||
newSeries = newPoint.series;
|
||||
if (isSkipSeries(newSeries)) {
|
||||
chart.highlightedPoint = next ?
|
||||
newSeries.points[newSeries.points.length - 1] :
|
||||
newSeries.points[0];
|
||||
}
|
||||
else {
|
||||
// Otherwise, just move one point
|
||||
chart.highlightedPoint = newPoint;
|
||||
}
|
||||
// Retry
|
||||
return chart.highlightAdjacentPoint(next);
|
||||
}
|
||||
// There is an adjacent point, highlight it
|
||||
return newPoint.highlight();
|
||||
};
|
||||
/**
|
||||
* Highlight first valid point in a series. Returns the point if successfully
|
||||
* highlighted, otherwise false. If there is a highlighted point in the series,
|
||||
* use that as starting point.
|
||||
*
|
||||
* @private
|
||||
* @function Highcharts.Series#highlightFirstValidPoint
|
||||
*
|
||||
* @return {boolean|Highcharts.Point}
|
||||
*/
|
||||
H.Series.prototype.highlightFirstValidPoint = function () {
|
||||
var curPoint = this.chart.highlightedPoint, start = (curPoint && curPoint.series) === this ?
|
||||
getPointIndex(curPoint) :
|
||||
0, points = this.points, len = points.length;
|
||||
if (points && len) {
|
||||
for (var i = start; i < len; ++i) {
|
||||
if (!isSkipPoint(points[i])) {
|
||||
return points[i].highlight();
|
||||
}
|
||||
}
|
||||
for (var j = start; j >= 0; --j) {
|
||||
if (!isSkipPoint(points[j])) {
|
||||
return points[j].highlight();
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
/**
|
||||
* Highlight next/previous series in chart. Returns false if no adjacent series
|
||||
* in the direction, otherwise returns new highlighted point.
|
||||
*
|
||||
* @private
|
||||
* @function Highcharts.Chart#highlightAdjacentSeries
|
||||
*
|
||||
* @param {boolean} down
|
||||
*
|
||||
* @return {Highcharts.Point|boolean}
|
||||
*/
|
||||
Chart.prototype.highlightAdjacentSeries = function (down) {
|
||||
var chart = this, newSeries, newPoint, adjacentNewPoint, curPoint = chart.highlightedPoint, lastSeries = chart.series && chart.series[chart.series.length - 1], lastPoint = lastSeries && lastSeries.points &&
|
||||
lastSeries.points[lastSeries.points.length - 1];
|
||||
// If no point is highlighted, highlight the first/last point
|
||||
if (!chart.highlightedPoint) {
|
||||
newSeries = down ? (chart.series && chart.series[0]) : lastSeries;
|
||||
newPoint = down ?
|
||||
(newSeries && newSeries.points && newSeries.points[0]) : lastPoint;
|
||||
return newPoint ? newPoint.highlight() : false;
|
||||
}
|
||||
newSeries = chart.series[curPoint.series.index + (down ? -1 : 1)];
|
||||
if (!newSeries) {
|
||||
return false;
|
||||
}
|
||||
// We have a new series in this direction, find the right point
|
||||
// Weigh xDistance as counting much higher than Y distance
|
||||
newPoint = getClosestPoint(curPoint, newSeries, 4);
|
||||
if (!newPoint) {
|
||||
return false;
|
||||
}
|
||||
// New series and point exists, but we might want to skip it
|
||||
if (isSkipSeries(newSeries)) {
|
||||
// Skip the series
|
||||
newPoint.highlight();
|
||||
adjacentNewPoint = chart.highlightAdjacentSeries(down); // Try recurse
|
||||
if (!adjacentNewPoint) {
|
||||
// Recurse failed
|
||||
curPoint.highlight();
|
||||
return false;
|
||||
}
|
||||
// Recurse succeeded
|
||||
return adjacentNewPoint;
|
||||
}
|
||||
// Highlight the new point or any first valid point back or forwards from it
|
||||
newPoint.highlight();
|
||||
return newPoint.series.highlightFirstValidPoint();
|
||||
};
|
||||
/**
|
||||
* Highlight the closest point vertically.
|
||||
*
|
||||
* @private
|
||||
* @function Highcharts.Chart#highlightAdjacentPointVertical
|
||||
*
|
||||
* @param {boolean} down
|
||||
*
|
||||
* @return {Highcharts.Point|boolean}
|
||||
*/
|
||||
Chart.prototype.highlightAdjacentPointVertical = function (down) {
|
||||
var curPoint = this.highlightedPoint, minDistance = Infinity, bestPoint;
|
||||
if (!defined(curPoint.plotX) || !defined(curPoint.plotY)) {
|
||||
return false;
|
||||
}
|
||||
this.series.forEach(function (series) {
|
||||
if (isSkipSeries(series)) {
|
||||
return;
|
||||
}
|
||||
series.points.forEach(function (point) {
|
||||
if (!defined(point.plotY) || !defined(point.plotX) ||
|
||||
point === curPoint) {
|
||||
return;
|
||||
}
|
||||
var yDistance = point.plotY - curPoint.plotY, width = Math.abs(point.plotX - curPoint.plotX), distance = Math.abs(yDistance) * Math.abs(yDistance) +
|
||||
width * width * 4; // Weigh horizontal distance highly
|
||||
// Reverse distance number if axis is reversed
|
||||
if (series.yAxis && series.yAxis.reversed) {
|
||||
yDistance *= -1;
|
||||
}
|
||||
if (yDistance <= 0 && down || yDistance >= 0 && !down || // Chk dir
|
||||
distance < 5 || // Points in same spot => infinite loop
|
||||
isSkipPoint(point)) {
|
||||
return;
|
||||
}
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
bestPoint = point;
|
||||
}
|
||||
});
|
||||
});
|
||||
return bestPoint ? bestPoint.highlight() : false;
|
||||
};
|
||||
/**
|
||||
* @private
|
||||
* @param {Highcharts.Chart} chart
|
||||
* @return {Highcharts.Point|boolean}
|
||||
*/
|
||||
function highlightFirstValidPointInChart(chart) {
|
||||
var res = false;
|
||||
delete chart.highlightedPoint;
|
||||
res = chart.series.reduce(function (acc, cur) {
|
||||
return acc || cur.highlightFirstValidPoint();
|
||||
}, false);
|
||||
return res;
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
* @param {Highcharts.Chart} chart
|
||||
* @return {Highcharts.Point|boolean}
|
||||
*/
|
||||
function highlightLastValidPointInChart(chart) {
|
||||
var numSeries = chart.series.length, i = numSeries, res = false;
|
||||
while (i--) {
|
||||
chart.highlightedPoint = chart.series[i].points[chart.series[i].points.length - 1];
|
||||
// Highlight first valid point in the series will also
|
||||
// look backwards. It always starts from currently
|
||||
// highlighted point.
|
||||
res = chart.series[i].highlightFirstValidPoint();
|
||||
if (res) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
* @param {Highcharts.Chart} chart
|
||||
*/
|
||||
function updateChartFocusAfterDrilling(chart) {
|
||||
highlightFirstValidPointInChart(chart);
|
||||
if (chart.focusElement) {
|
||||
chart.focusElement.removeFocusBorder();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
* @class
|
||||
* @name Highcharts.SeriesKeyboardNavigation
|
||||
*/
|
||||
function SeriesKeyboardNavigation(chart, keyCodes) {
|
||||
this.keyCodes = keyCodes;
|
||||
this.chart = chart;
|
||||
}
|
||||
extend(SeriesKeyboardNavigation.prototype, /** @lends Highcharts.SeriesKeyboardNavigation */ {
|
||||
/**
|
||||
* Init the keyboard navigation
|
||||
*/
|
||||
init: function () {
|
||||
var keyboardNavigation = this, chart = this.chart, e = this.eventProvider = new EventProvider();
|
||||
e.addEvent(H.Series, 'destroy', function () {
|
||||
return keyboardNavigation.onSeriesDestroy(this);
|
||||
});
|
||||
e.addEvent(chart, 'afterDrilldown', function () {
|
||||
updateChartFocusAfterDrilling(this);
|
||||
});
|
||||
e.addEvent(chart, 'drilldown', function (e) {
|
||||
var point = e.point, series = point.series;
|
||||
keyboardNavigation.lastDrilledDownPoint = {
|
||||
x: point.x,
|
||||
y: point.y,
|
||||
seriesName: series ? series.name : ''
|
||||
};
|
||||
});
|
||||
e.addEvent(chart, 'drillupall', function () {
|
||||
setTimeout(function () {
|
||||
keyboardNavigation.onDrillupAll();
|
||||
}, 10);
|
||||
});
|
||||
},
|
||||
onDrillupAll: function () {
|
||||
// After drillup we want to find the point that was drilled down to and
|
||||
// highlight it.
|
||||
var last = this.lastDrilledDownPoint, chart = this.chart, series = last && getSeriesFromName(chart, last.seriesName), point;
|
||||
if (last && series && defined(last.x) && defined(last.y)) {
|
||||
point = getPointFromXY(series, last.x, last.y);
|
||||
}
|
||||
// Container focus can be lost on drillup due to deleted elements.
|
||||
if (chart.container) {
|
||||
chart.container.focus();
|
||||
}
|
||||
if (point && point.highlight) {
|
||||
point.highlight();
|
||||
}
|
||||
if (chart.focusElement) {
|
||||
chart.focusElement.removeFocusBorder();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @return {Highcharts.KeyboardNavigationHandler}
|
||||
*/
|
||||
getKeyboardNavigationHandler: function () {
|
||||
var keyboardNavigation = this, keys = this.keyCodes, chart = this.chart, inverted = chart.inverted;
|
||||
return new KeyboardNavigationHandler(chart, {
|
||||
keyCodeMap: [
|
||||
[inverted ? [keys.up, keys.down] : [keys.left, keys.right], function (keyCode) {
|
||||
return keyboardNavigation.onKbdSideways(this, keyCode);
|
||||
}],
|
||||
[inverted ? [keys.left, keys.right] : [keys.up, keys.down], function (keyCode) {
|
||||
return keyboardNavigation.onKbdVertical(this, keyCode);
|
||||
}],
|
||||
[[keys.enter, keys.space], function () {
|
||||
if (chart.highlightedPoint) {
|
||||
chart.highlightedPoint.firePointEvent('click');
|
||||
}
|
||||
return this.response.success;
|
||||
}]
|
||||
],
|
||||
init: function (dir) {
|
||||
return keyboardNavigation.onHandlerInit(this, dir);
|
||||
},
|
||||
terminate: function () {
|
||||
return keyboardNavigation.onHandlerTerminate();
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Highcharts.KeyboardNavigationHandler} handler
|
||||
* @param {number} keyCode
|
||||
* @return {number}
|
||||
* response
|
||||
*/
|
||||
onKbdSideways: function (handler, keyCode) {
|
||||
var keys = this.keyCodes, isNext = keyCode === keys.right || keyCode === keys.down;
|
||||
return this.attemptHighlightAdjacentPoint(handler, isNext);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Highcharts.KeyboardNavigationHandler} handler
|
||||
* @param {number} keyCode
|
||||
* @return {number}
|
||||
* response
|
||||
*/
|
||||
onKbdVertical: function (handler, keyCode) {
|
||||
var chart = this.chart, keys = this.keyCodes, isNext = keyCode === keys.down || keyCode === keys.right, navOptions = chart.options.accessibility.keyboardNavigation
|
||||
.seriesNavigation;
|
||||
// Handle serialized mode, act like left/right
|
||||
if (navOptions.mode && navOptions.mode === 'serialize') {
|
||||
return this.attemptHighlightAdjacentPoint(handler, isNext);
|
||||
}
|
||||
// Normal mode, move between series
|
||||
var highlightMethod = (chart.highlightedPoint &&
|
||||
chart.highlightedPoint.series.keyboardMoveVertical) ?
|
||||
'highlightAdjacentPointVertical' :
|
||||
'highlightAdjacentSeries';
|
||||
chart[highlightMethod](isNext);
|
||||
return handler.response.success;
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Highcharts.KeyboardNavigationHandler} handler
|
||||
* @param {number} initDirection
|
||||
* @return {number}
|
||||
* response
|
||||
*/
|
||||
onHandlerInit: function (handler, initDirection) {
|
||||
var chart = this.chart;
|
||||
if (initDirection > 0) {
|
||||
highlightFirstValidPointInChart(chart);
|
||||
}
|
||||
else {
|
||||
highlightLastValidPointInChart(chart);
|
||||
}
|
||||
return handler.response.success;
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
onHandlerTerminate: function () {
|
||||
var _a, _b;
|
||||
var chart = this.chart;
|
||||
var curPoint = chart.highlightedPoint;
|
||||
(_a = chart.tooltip) === null || _a === void 0 ? void 0 : _a.hide(0);
|
||||
(_b = curPoint === null || curPoint === void 0 ? void 0 : curPoint.onMouseOut) === null || _b === void 0 ? void 0 : _b.call(curPoint);
|
||||
delete chart.highlightedPoint;
|
||||
},
|
||||
/**
|
||||
* Function that attempts to highlight next/prev point. Handles wrap around.
|
||||
* @private
|
||||
* @param {Highcharts.KeyboardNavigationHandler} handler
|
||||
* @param {boolean} directionIsNext
|
||||
* @return {number}
|
||||
* response
|
||||
*/
|
||||
attemptHighlightAdjacentPoint: function (handler, directionIsNext) {
|
||||
var chart = this.chart, wrapAround = chart.options.accessibility.keyboardNavigation
|
||||
.wrapAround, highlightSuccessful = chart.highlightAdjacentPoint(directionIsNext);
|
||||
if (!highlightSuccessful) {
|
||||
if (wrapAround) {
|
||||
return handler.init(directionIsNext ? 1 : -1);
|
||||
}
|
||||
return handler.response[directionIsNext ? 'next' : 'prev'];
|
||||
}
|
||||
return handler.response.success;
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
onSeriesDestroy: function (series) {
|
||||
var chart = this.chart, currentHighlightedPointDestroyed = chart.highlightedPoint &&
|
||||
chart.highlightedPoint.series === series;
|
||||
if (currentHighlightedPointDestroyed) {
|
||||
delete chart.highlightedPoint;
|
||||
if (chart.focusElement) {
|
||||
chart.focusElement.removeFocusBorder();
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
destroy: function () {
|
||||
this.eventProvider.removeAddedEvents();
|
||||
}
|
||||
});
|
||||
export default SeriesKeyboardNavigation;
|
||||
@@ -0,0 +1,288 @@
|
||||
/* *
|
||||
*
|
||||
* (c) 2009-2020 Øystein Moseng
|
||||
*
|
||||
* Accessibility component for chart zoom.
|
||||
*
|
||||
* License: www.highcharts.com/license
|
||||
*
|
||||
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
|
||||
*
|
||||
* */
|
||||
'use strict';
|
||||
import H from '../../Core/Globals.js';
|
||||
import U from '../../Core/Utilities.js';
|
||||
var extend = U.extend, pick = U.pick;
|
||||
import AccessibilityComponent from '../AccessibilityComponent.js';
|
||||
import KeyboardNavigationHandler from '../KeyboardNavigationHandler.js';
|
||||
import ChartUtilities from '../Utils/ChartUtilities.js';
|
||||
var unhideChartElementFromAT = ChartUtilities.unhideChartElementFromAT;
|
||||
import HTMLUtilities from '../Utils/HTMLUtilities.js';
|
||||
var setElAttrs = HTMLUtilities.setElAttrs, removeElement = HTMLUtilities.removeElement;
|
||||
/* eslint-disable no-invalid-this, valid-jsdoc */
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
function chartHasMapZoom(chart) {
|
||||
return !!(chart.mapZoom &&
|
||||
chart.mapNavButtons &&
|
||||
chart.mapNavButtons.length);
|
||||
}
|
||||
/**
|
||||
* Pan along axis in a direction (1 or -1), optionally with a defined
|
||||
* granularity (number of steps it takes to walk across current view)
|
||||
*
|
||||
* @private
|
||||
* @function Highcharts.Axis#panStep
|
||||
*
|
||||
* @param {number} direction
|
||||
* @param {number} [granularity]
|
||||
*/
|
||||
H.Axis.prototype.panStep = function (direction, granularity) {
|
||||
var gran = granularity || 3, extremes = this.getExtremes(), step = (extremes.max - extremes.min) / gran * direction, newMax = extremes.max + step, newMin = extremes.min + step, size = newMax - newMin;
|
||||
if (direction < 0 && newMin < extremes.dataMin) {
|
||||
newMin = extremes.dataMin;
|
||||
newMax = newMin + size;
|
||||
}
|
||||
else if (direction > 0 && newMax > extremes.dataMax) {
|
||||
newMax = extremes.dataMax;
|
||||
newMin = newMax - size;
|
||||
}
|
||||
this.setExtremes(newMin, newMax);
|
||||
};
|
||||
/**
|
||||
* The ZoomComponent class
|
||||
*
|
||||
* @private
|
||||
* @class
|
||||
* @name Highcharts.ZoomComponent
|
||||
*/
|
||||
var ZoomComponent = function () { };
|
||||
ZoomComponent.prototype = new AccessibilityComponent();
|
||||
extend(ZoomComponent.prototype, /** @lends Highcharts.ZoomComponent */ {
|
||||
/**
|
||||
* Initialize the component
|
||||
*/
|
||||
init: function () {
|
||||
var component = this, chart = this.chart;
|
||||
[
|
||||
'afterShowResetZoom', 'afterDrilldown', 'drillupall'
|
||||
].forEach(function (eventType) {
|
||||
component.addEvent(chart, eventType, function () {
|
||||
component.updateProxyOverlays();
|
||||
});
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Called when chart is updated
|
||||
*/
|
||||
onChartUpdate: function () {
|
||||
var chart = this.chart, component = this;
|
||||
// Make map zoom buttons accessible
|
||||
if (chart.mapNavButtons) {
|
||||
chart.mapNavButtons.forEach(function (button, i) {
|
||||
unhideChartElementFromAT(chart, button.element);
|
||||
component.setMapNavButtonAttrs(button.element, 'accessibility.zoom.mapZoom' + (i ? 'Out' : 'In'));
|
||||
});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Highcharts.HTMLDOMElement|Highcharts.SVGDOMElement} button
|
||||
* @param {string} labelFormatKey
|
||||
*/
|
||||
setMapNavButtonAttrs: function (button, labelFormatKey) {
|
||||
var chart = this.chart, label = chart.langFormat(labelFormatKey, { chart: chart });
|
||||
setElAttrs(button, {
|
||||
tabindex: -1,
|
||||
role: 'button',
|
||||
'aria-label': label
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Update the proxy overlays on every new render to ensure positions are
|
||||
* correct.
|
||||
*/
|
||||
onChartRender: function () {
|
||||
this.updateProxyOverlays();
|
||||
},
|
||||
/**
|
||||
* Update proxy overlays, recreating the buttons.
|
||||
*/
|
||||
updateProxyOverlays: function () {
|
||||
var chart = this.chart;
|
||||
// Always start with a clean slate
|
||||
removeElement(this.drillUpProxyGroup);
|
||||
removeElement(this.resetZoomProxyGroup);
|
||||
if (chart.resetZoomButton) {
|
||||
this.recreateProxyButtonAndGroup(chart.resetZoomButton, 'resetZoomProxyButton', 'resetZoomProxyGroup', chart.langFormat('accessibility.zoom.resetZoomButton', { chart: chart }));
|
||||
}
|
||||
if (chart.drillUpButton) {
|
||||
this.recreateProxyButtonAndGroup(chart.drillUpButton, 'drillUpProxyButton', 'drillUpProxyGroup', chart.langFormat('accessibility.drillUpButton', {
|
||||
chart: chart,
|
||||
buttonText: chart.getDrilldownBackText()
|
||||
}));
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Highcharts.SVGElement} buttonEl
|
||||
* @param {string} buttonProp
|
||||
* @param {string} groupProp
|
||||
* @param {string} label
|
||||
*/
|
||||
recreateProxyButtonAndGroup: function (buttonEl, buttonProp, groupProp, label) {
|
||||
removeElement(this[groupProp]);
|
||||
this[groupProp] = this.addProxyGroup();
|
||||
this[buttonProp] = this.createProxyButton(buttonEl, this[groupProp], { 'aria-label': label, tabindex: -1 });
|
||||
},
|
||||
/**
|
||||
* Get keyboard navigation handler for map zoom.
|
||||
* @private
|
||||
* @return {Highcharts.KeyboardNavigationHandler} The module object
|
||||
*/
|
||||
getMapZoomNavigation: function () {
|
||||
var keys = this.keyCodes, chart = this.chart, component = this;
|
||||
return new KeyboardNavigationHandler(chart, {
|
||||
keyCodeMap: [
|
||||
[
|
||||
[keys.up, keys.down, keys.left, keys.right],
|
||||
function (keyCode) {
|
||||
return component.onMapKbdArrow(this, keyCode);
|
||||
}
|
||||
],
|
||||
[
|
||||
[keys.tab],
|
||||
function (_keyCode, e) {
|
||||
return component.onMapKbdTab(this, e);
|
||||
}
|
||||
],
|
||||
[
|
||||
[keys.space, keys.enter],
|
||||
function () {
|
||||
return component.onMapKbdClick(this);
|
||||
}
|
||||
]
|
||||
],
|
||||
validate: function () {
|
||||
return chartHasMapZoom(chart);
|
||||
},
|
||||
init: function (direction) {
|
||||
return component.onMapNavInit(direction);
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Highcharts.KeyboardNavigationHandler} keyboardNavigationHandler
|
||||
* @param {number} keyCode
|
||||
* @return {number} Response code
|
||||
*/
|
||||
onMapKbdArrow: function (keyboardNavigationHandler, keyCode) {
|
||||
var keys = this.keyCodes, panAxis = (keyCode === keys.up || keyCode === keys.down) ?
|
||||
'yAxis' : 'xAxis', stepDirection = (keyCode === keys.left || keyCode === keys.up) ?
|
||||
-1 : 1;
|
||||
this.chart[panAxis][0].panStep(stepDirection);
|
||||
return keyboardNavigationHandler.response.success;
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Highcharts.KeyboardNavigationHandler} keyboardNavigationHandler
|
||||
* @param {global.KeyboardEvent} event
|
||||
* @return {number} Response code
|
||||
*/
|
||||
onMapKbdTab: function (keyboardNavigationHandler, event) {
|
||||
var button, chart = this.chart, response = keyboardNavigationHandler.response, isBackwards = event.shiftKey, isMoveOutOfRange = isBackwards && !this.focusedMapNavButtonIx ||
|
||||
!isBackwards && this.focusedMapNavButtonIx;
|
||||
// Deselect old
|
||||
chart.mapNavButtons[this.focusedMapNavButtonIx].setState(0);
|
||||
if (isMoveOutOfRange) {
|
||||
chart.mapZoom(); // Reset zoom
|
||||
return response[isBackwards ? 'prev' : 'next'];
|
||||
}
|
||||
// Select other button
|
||||
this.focusedMapNavButtonIx += isBackwards ? -1 : 1;
|
||||
button = chart.mapNavButtons[this.focusedMapNavButtonIx];
|
||||
chart.setFocusToElement(button.box, button.element);
|
||||
button.setState(2);
|
||||
return response.success;
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Highcharts.KeyboardNavigationHandler} keyboardNavigationHandler
|
||||
* @return {number} Response code
|
||||
*/
|
||||
onMapKbdClick: function (keyboardNavigationHandler) {
|
||||
this.fakeClickEvent(this.chart.mapNavButtons[this.focusedMapNavButtonIx]
|
||||
.element);
|
||||
return keyboardNavigationHandler.response.success;
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {number} direction
|
||||
*/
|
||||
onMapNavInit: function (direction) {
|
||||
var chart = this.chart, zoomIn = chart.mapNavButtons[0], zoomOut = chart.mapNavButtons[1], initialButton = direction > 0 ? zoomIn : zoomOut;
|
||||
chart.setFocusToElement(initialButton.box, initialButton.element);
|
||||
initialButton.setState(2);
|
||||
this.focusedMapNavButtonIx = direction > 0 ? 0 : 1;
|
||||
},
|
||||
/**
|
||||
* Get keyboard navigation handler for a simple chart button. Provide the
|
||||
* button reference for the chart, and a function to call on click.
|
||||
*
|
||||
* @private
|
||||
* @param {string} buttonProp The property on chart referencing the button.
|
||||
* @return {Highcharts.KeyboardNavigationHandler} The module object
|
||||
*/
|
||||
simpleButtonNavigation: function (buttonProp, proxyProp, onClick) {
|
||||
var keys = this.keyCodes, component = this, chart = this.chart;
|
||||
return new KeyboardNavigationHandler(chart, {
|
||||
keyCodeMap: [
|
||||
[
|
||||
[keys.tab, keys.up, keys.down, keys.left, keys.right],
|
||||
function (keyCode, e) {
|
||||
var isBackwards = keyCode === keys.tab && e.shiftKey ||
|
||||
keyCode === keys.left || keyCode === keys.up;
|
||||
// Arrow/tab => just move
|
||||
return this.response[isBackwards ? 'prev' : 'next'];
|
||||
}
|
||||
],
|
||||
[
|
||||
[keys.space, keys.enter],
|
||||
function () {
|
||||
var res = onClick(this, chart);
|
||||
return pick(res, this.response.success);
|
||||
}
|
||||
]
|
||||
],
|
||||
validate: function () {
|
||||
var hasButton = (chart[buttonProp] &&
|
||||
chart[buttonProp].box &&
|
||||
component[proxyProp]);
|
||||
return hasButton;
|
||||
},
|
||||
init: function () {
|
||||
chart.setFocusToElement(chart[buttonProp].box, component[proxyProp]);
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Get keyboard navigation handlers for this component.
|
||||
* @return {Array<Highcharts.KeyboardNavigationHandler>}
|
||||
* List of module objects
|
||||
*/
|
||||
getKeyboardNavigation: function () {
|
||||
return [
|
||||
this.simpleButtonNavigation('resetZoomButton', 'resetZoomProxyButton', function (_handler, chart) {
|
||||
chart.zoomOut();
|
||||
}),
|
||||
this.simpleButtonNavigation('drillUpButton', 'drillUpProxyButton', function (handler, chart) {
|
||||
chart.drillUp();
|
||||
return handler.response.prev;
|
||||
}),
|
||||
this.getMapZoomNavigation()
|
||||
];
|
||||
}
|
||||
});
|
||||
export default ZoomComponent;
|
||||
Reference in New Issue
Block a user