UNPKG

highcharts

Version:
279 lines (278 loc) 10.6 kB
/* * * * (c) 2009-2019 Ø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 '../../../../parts/Globals.js'; import U from '../../../../parts/Utilities.js'; var extend = U.extend, defined = U.defined; import HTMLUtilities from '../../utils/htmlUtilities.js'; var visuallyHideElement = HTMLUtilities.visuallyHideElement; import ChartUtilities from '../../utils/chartUtilities.js'; var getChartTitle = ChartUtilities.getChartTitle; import SeriesDescriber from './SeriesDescriber.js'; var defaultPointDescriptionFormatter = SeriesDescriber .defaultPointDescriptionFormatter, defaultSeriesDescriptionFormatter = SeriesDescriber .defaultSeriesDescriptionFormatter; import EventProvider from '../../utils/EventProvider.js'; import DOMElementProvider from '../../utils/DOMElementProvider.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. */ init: function () { this.lastAnnouncementTime = 0; this.dirty = { allSeries: {} }; this.eventProvider = new EventProvider(); this.domElementProvider = new DOMElementProvider(); this.announceRegion = this.addAnnounceRegion(); this.addEventListeners(); }, /** * Remove traces of announcer. */ destroy: function () { this.eventProvider.removeAddedEvents(); this.domElementProvider.destroyCreatedElements(); }, /** * Add the announcement live region to the DOM. * @private */ addAnnounceRegion: function () { var chart = this.chart, div = this.domElementProvider.createElement('div'), announceOptions = (chart.options.accessibility.announceNewData); div.setAttribute('aria-hidden', false); div.setAttribute('aria-live', announceOptions.interruptUser ? 'assertive' : 'polite'); visuallyHideElement(div); chart.renderTo.insertBefore(div, chart.renderTo.firstChild); return div; }, /** * 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 chart = this.chart, annOptions = (chart.options.accessibility.announceNewData); if (annOptions.enabled) { var announcer = this, now = +new Date(), dTime = now - this.lastAnnouncementTime, time = Math.max(0, annOptions.minAnnounceInterval - dTime), // Add series from previously queued announcement. 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 announcer.queuedAnnouncementTimer = setTimeout(function () { if (announcer && announcer.announceRegion) { announcer.lastAnnouncementTime = +new Date(); announcer.liveRegionSpeak(announcer.queuedAnnouncement.message); delete announcer.queuedAnnouncement; delete announcer.queuedAnnouncementTimer; } }, time); } } }, /** * Speak a message using the announcer live region. * @private * @param {string} message */ liveRegionSpeak: function (message) { var announcer = this; this.announceRegion.innerHTML = message; // Delete contents after a little while to avoid user finding the live // region in the DOM. if (this.clearAnnouncementContainerTimer) { clearTimeout(this.clearAnnouncementContainerTimer); } this.clearAnnouncementContainerTimer = setTimeout(function () { announcer.announceRegion.innerHTML = ''; delete announcer.clearAnnouncementContainerTimer; }, 1000); }, /** * 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;