/* * * * (c) 2009-2019 Ø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 '../../../../parts/Globals.js'; var doc = H.win.document, format = H.format; import U from '../../../../parts/Utilities.js'; var extend = U.extend, pick = U.pick; import AccessibilityComponent from '../../AccessibilityComponent.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, '') .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} 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, 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); }); }, /** * @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.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 chart = this.chart, format = chart.options.accessibility .screenReaderSection.beforeChartFormat, axesDesc = this.getAxesDescription(), dataTableButtonId = 'hc-linkto-highcharts-data-table-' + chart.index, 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, viewTableButton: chart.getCSV ? this.getDataTableButtonText(dataTableButtonId) : '', annotationsTitle: annotationsTitleStr, annotationsList: getAnnotationsInfoHTML(chart) }, formattedString = H.i18nFormat(format, context, chart); this.dataTableButtonId = dataTableButtonId; 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 '' + buttonText + ''; }, /** * @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 '
' + markerText + '
'; }, /** * @private * @param {Highcharts.Dictionary} 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('} */ 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.isDatetimeAxis && (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.isDatetimeAxis ? chart.time.dateFormat(dateRangeFormat, axis[axisKey]) : axis[axisKey]; }; return chart.langFormat('accessibility.axis.rangeFromTo', { chart: chart, axis: axis, rangeFrom: format('min'), rangeTo: format('max') }); } }); export default InfoRegionsComponent;