highcharts-rails 6.0.2 → 6.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/CCBYNC-LICENSE +103 -0
  3. data/CHANGELOG.markdown +31 -0
  4. data/Highsoft-LICENSE +1 -0
  5. data/{LICENSE → MIT-LICENSE} +0 -0
  6. data/README.markdown +6 -6
  7. data/app/assets/javascripts/highcharts.js +1635 -498
  8. data/app/assets/javascripts/highcharts/highcharts-3d.js +1 -1
  9. data/app/assets/javascripts/highcharts/highcharts-more.js +2 -2
  10. data/app/assets/javascripts/highcharts/modules/accessibility.js +1072 -824
  11. data/app/assets/javascripts/highcharts/modules/annotations.js +1 -1
  12. data/app/assets/javascripts/highcharts/modules/boost-canvas.js +3 -13
  13. data/app/assets/javascripts/highcharts/modules/boost.js +29 -13
  14. data/app/assets/javascripts/highcharts/modules/broken-axis.js +1 -1
  15. data/app/assets/javascripts/highcharts/modules/bullet.js +1 -1
  16. data/app/assets/javascripts/highcharts/modules/data.js +6 -6
  17. data/app/assets/javascripts/highcharts/modules/drag-panes.js +1 -1
  18. data/app/assets/javascripts/highcharts/modules/drilldown.js +1 -1
  19. data/app/assets/javascripts/highcharts/modules/export-data.js +10 -12
  20. data/app/assets/javascripts/highcharts/modules/exporting.js +1 -1
  21. data/app/assets/javascripts/highcharts/modules/funnel.js +1 -1
  22. data/app/assets/javascripts/highcharts/modules/gantt.js +26 -78
  23. data/app/assets/javascripts/highcharts/modules/grid-axis.js +1 -1
  24. data/app/assets/javascripts/highcharts/modules/heatmap.js +1 -1
  25. data/app/assets/javascripts/highcharts/modules/histogram-bellcurve.js +1 -1
  26. data/app/assets/javascripts/highcharts/modules/item-series.js +1 -1
  27. data/app/assets/javascripts/highcharts/modules/no-data-to-display.js +6 -15
  28. data/app/assets/javascripts/highcharts/modules/offline-exporting.js +2 -2
  29. data/app/assets/javascripts/highcharts/modules/oldie.js +2 -2
  30. data/app/assets/javascripts/highcharts/modules/overlapping-datalabels.js +41 -47
  31. data/app/assets/javascripts/highcharts/modules/parallel-coordinates.js +10 -6
  32. data/app/assets/javascripts/highcharts/modules/pareto.js +9 -1
  33. data/app/assets/javascripts/highcharts/modules/sankey.js +1 -1
  34. data/app/assets/javascripts/highcharts/modules/series-label.js +1 -1
  35. data/app/assets/javascripts/highcharts/modules/solid-gauge.js +1 -1
  36. data/app/assets/javascripts/highcharts/modules/static-scale.js +2 -6
  37. data/app/assets/javascripts/highcharts/modules/stock.js +96 -30
  38. data/app/assets/javascripts/highcharts/modules/streamgraph.js +1 -1
  39. data/app/assets/javascripts/highcharts/modules/sunburst.js +82 -50
  40. data/app/assets/javascripts/highcharts/modules/tilemap.js +1 -1
  41. data/app/assets/javascripts/highcharts/modules/treemap.js +10 -2
  42. data/app/assets/javascripts/highcharts/modules/variable-pie.js +1 -1
  43. data/app/assets/javascripts/highcharts/modules/variwide.js +1 -1
  44. data/app/assets/javascripts/highcharts/modules/vector.js +1 -1
  45. data/app/assets/javascripts/highcharts/modules/windbarb.js +1 -1
  46. data/app/assets/javascripts/highcharts/modules/wordcloud.js +7 -3
  47. data/app/assets/javascripts/highcharts/modules/xrange.js +24 -76
  48. data/app/assets/javascripts/highcharts/themes/avocado.js +1 -1
  49. data/app/assets/javascripts/highcharts/themes/dark-blue.js +1 -1
  50. data/app/assets/javascripts/highcharts/themes/dark-green.js +1 -1
  51. data/app/assets/javascripts/highcharts/themes/dark-unica.js +1 -1
  52. data/app/assets/javascripts/highcharts/themes/gray.js +1 -1
  53. data/app/assets/javascripts/highcharts/themes/grid-light.js +1 -1
  54. data/app/assets/javascripts/highcharts/themes/grid.js +1 -1
  55. data/app/assets/javascripts/highcharts/themes/sand-signika.js +1 -1
  56. data/app/assets/javascripts/highcharts/themes/skies.js +1 -1
  57. data/app/assets/javascripts/highcharts/themes/sunset.js +1 -1
  58. data/highcharts-rails.gemspec +1 -0
  59. data/lib/highcharts/version.rb +1 -1
  60. metadata +9 -4
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @license Highcharts JS v6.0.2 (2017-10-20)
2
+ * @license Highcharts JS v6.0.3 (2017-11-14)
3
3
  *
4
4
  * 3D features for Highcharts JS
5
5
  *
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @license Highcharts JS v6.0.2 (2017-10-20)
2
+ * @license Highcharts JS v6.0.3 (2017-11-14)
3
3
  *
4
4
  * (c) 2009-2016 Torstein Honsi
5
5
  *
@@ -1321,7 +1321,7 @@
1321
1321
  }
1322
1322
 
1323
1323
  this.graphPath = linePath;
1324
- this.areaPath = this.areaPath.concat(lowerPath, higherAreaPath);
1324
+ this.areaPath = lowerPath.concat(higherAreaPath);
1325
1325
 
1326
1326
  // Prepare for sideways animation
1327
1327
  linePath.isArea = true;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @license Highcharts JS v6.0.2 (2017-10-20)
2
+ * @license Highcharts JS v6.0.3 (2017-11-14)
3
3
  * Accessibility module
4
4
  *
5
5
  * (c) 2010-2017 Highsoft AS
@@ -17,7 +17,7 @@
17
17
  }(function(Highcharts) {
18
18
  (function(H) {
19
19
  /**
20
- * Accessibility module
20
+ * Accessibility module - Screen Reader support
21
21
  *
22
22
  * (c) 2010-2017 Highsoft AS
23
23
  * Author: Oystein Moseng
@@ -31,8 +31,6 @@
31
31
  each = H.each,
32
32
  erase = H.erase,
33
33
  addEvent = H.addEvent,
34
- removeEvent = H.removeEvent,
35
- fireEvent = H.fireEvent,
36
34
  dateFormat = H.dateFormat,
37
35
  merge = H.merge,
38
36
  // CSS style to hide element from visual users while still exposing it to
@@ -99,27 +97,18 @@
99
97
  'contributes towards a total end value. '
100
98
  };
101
99
 
100
+
102
101
  // If a point has one of the special keys defined, we expose all keys to the
103
102
  // screen reader.
104
103
  H.Series.prototype.commonKeys = ['name', 'id', 'category', 'x', 'value', 'y'];
105
104
  H.Series.prototype.specialKeys = [
106
105
  'z', 'open', 'high', 'q3', 'median', 'q1', 'low', 'close'
107
106
  ];
108
-
109
- // A pie is always simple. Don't quote me on that.
110
107
  if (H.seriesTypes.pie) {
108
+ // A pie is always simple. Don't quote me on that.
111
109
  H.seriesTypes.pie.prototype.specialKeys = [];
112
110
  }
113
111
 
114
- // Set for which series types it makes sense to move to the closest point with
115
- // up/down arrows, and which series types should just move to next series.
116
- H.Series.prototype.keyboardMoveVertical = true;
117
- each(['column', 'pie'], function(type) {
118
- if (H.seriesTypes[type]) {
119
- H.seriesTypes[type].prototype.keyboardMoveVertical = false;
120
- }
121
- });
122
-
123
112
 
124
113
  /**
125
114
  * Accessibility options
@@ -158,35 +147,7 @@
158
147
  * @default 30
159
148
  * @since 5.0.0
160
149
  */
161
- pointDescriptionThreshold: 30, // set to false to disable
162
-
163
- /**
164
- * Options for keyboard navigation.
165
- *
166
- * @type {Object}
167
- * @since 5.0.0
168
- */
169
- keyboardNavigation: {
170
-
171
- /**
172
- * Enable keyboard navigation for the chart.
173
- *
174
- * @type {Boolean}
175
- * @default true
176
- * @since 5.0.0
177
- */
178
- enabled: true
179
-
180
- /**
181
- * Skip null points when navigating through points with the
182
- * keyboard.
183
- *
184
- * @type {Boolean}
185
- * @default false
186
- * @since 5.0.0
187
- * @apioption accessibility.keyboardNavigation.skipNullPoints
188
- */
189
- }
150
+ pointDescriptionThreshold: 30 // set to false to disable
190
151
 
191
152
  /**
192
153
  * Whether or not to add series descriptions to charts with a single
@@ -311,23 +272,6 @@
311
272
  * @apioption chart.typeDescription
312
273
  */
313
274
 
314
- /**
315
- * Keyboard navigation for the legend. Requires the Accessibility module.
316
- * @since 5.0.14
317
- * @apioption legend.keyboardNavigation
318
- */
319
-
320
- /**
321
- * Enable/disable keyboard navigation for the legend. Requires the Accessibility
322
- * module.
323
- *
324
- * @type {Boolean}
325
- * @see [accessibility.keyboardNavigation](#accessibility.keyboardNavigation.
326
- * enabled)
327
- * @default true
328
- * @since 5.0.13
329
- * @apioption legend.keyboardNavigation.enabled
330
- */
331
275
 
332
276
  /**
333
277
  * HTML encode some characters vulnerable for XSS.
@@ -344,6 +288,17 @@
344
288
  .replace(/\//g, '/');
345
289
  }
346
290
 
291
+ /**
292
+ * Strip HTML tags away from a string. Used for aria-label attributes, painting
293
+ * on a canvas will fail if the text contains tags.
294
+ * @param {String} s The input string
295
+ * @return {String} The filtered string
296
+ */
297
+ function stripTags(s) {
298
+ return typeof s === 'string' ? s.replace(/<\/?[^>]+(>|$)/g, '') : s;
299
+ }
300
+
301
+
347
302
  // Utility function. Reverses child nodes of a DOM element
348
303
  function reverseChildNodes(node) {
349
304
  var i = node.childNodes.length;
@@ -352,52 +307,6 @@
352
307
  }
353
308
  }
354
309
 
355
- // Utility function to attempt to fake a click event on an element
356
- function fakeClickEvent(element) {
357
- var fakeEvent;
358
- if (element && element.onclick && doc.createEvent) {
359
- fakeEvent = doc.createEvent('Events');
360
- fakeEvent.initEvent('click', true, false);
361
- element.onclick(fakeEvent);
362
- }
363
- }
364
-
365
- // Determine if a point should be skipped
366
- function isSkipPoint(point) {
367
- return point.isNull &&
368
- point.series.chart.options.accessibility
369
- .keyboardNavigation.skipNullPoints ||
370
- point.series.options.skipKeyboardNavigation ||
371
- !point.series.visible;
372
- }
373
-
374
- // Get the point in a series that is closest to a reference point
375
- // Optionally supply weight factors for x and y directions
376
- function getClosestPoint(point, series, xWeight, yWeight) {
377
- var minDistance = Infinity,
378
- dPoint,
379
- minIx,
380
- distance,
381
- i = series.points.length;
382
- if (point.plotX === undefined || point.plotY === undefined) {
383
- return;
384
- }
385
- while (i--) {
386
- dPoint = series.points[i];
387
- if (dPoint.plotX === undefined || dPoint.plotY === undefined) {
388
- return;
389
- }
390
- distance = (point.plotX - dPoint.plotX) *
391
- (point.plotX - dPoint.plotX) * (xWeight || 1) +
392
- (point.plotY - dPoint.plotY) *
393
- (point.plotY - dPoint.plotY) * (yWeight || 1);
394
- if (distance < minDistance) {
395
- minDistance = distance;
396
- minIx = i;
397
- }
398
- }
399
- return series.points[minIx || 0];
400
- }
401
310
 
402
311
  // Whenever drawing series, put info on DOM elements
403
312
  H.wrap(H.Series.prototype, 'render', function(proceed) {
@@ -407,6 +316,7 @@
407
316
  }
408
317
  });
409
318
 
319
+
410
320
  // Put accessible info on series and points of a series
411
321
  H.Series.prototype.setA11yDescription = function() {
412
322
  var a11yOptions = this.chart.options.accessibility,
@@ -443,12 +353,13 @@
443
353
  if (point.graphic) {
444
354
  point.graphic.element.setAttribute('role', 'img');
445
355
  point.graphic.element.setAttribute('tabindex', '-1');
446
- point.graphic.element.setAttribute('aria-label',
356
+ point.graphic.element.setAttribute('aria-label', stripTags(
447
357
  point.series.options.pointDescriptionFormatter &&
448
358
  point.series.options.pointDescriptionFormatter(point) ||
449
359
  a11yOptions.pointDescriptionFormatter &&
450
360
  a11yOptions.pointDescriptionFormatter(point) ||
451
- point.buildPointInfoString());
361
+ point.buildPointInfoString()
362
+ ));
452
363
  }
453
364
  });
454
365
  }
@@ -461,14 +372,17 @@
461
372
  seriesEl.setAttribute('tabindex', '-1');
462
373
  seriesEl.setAttribute(
463
374
  'aria-label',
464
- a11yOptions.seriesDescriptionFormatter &&
465
- a11yOptions.seriesDescriptionFormatter(this) ||
466
- this.buildSeriesInfoString()
375
+ stripTags(
376
+ a11yOptions.seriesDescriptionFormatter &&
377
+ a11yOptions.seriesDescriptionFormatter(this) ||
378
+ this.buildSeriesInfoString()
379
+ )
467
380
  );
468
381
  }
469
382
  }
470
383
  };
471
384
 
385
+
472
386
  // Return string with information about series
473
387
  H.Series.prototype.buildSeriesInfoString = function() {
474
388
  var typeInfo = (
@@ -501,6 +415,7 @@
501
415
  );
502
416
  };
503
417
 
418
+
504
419
  // Return string with information about point
505
420
  H.Point.prototype.buildPointInfoString = function() {
506
421
  var point = this,
@@ -555,6 +470,7 @@
555
470
  (this.description ? ' ' + this.description : '');
556
471
  };
557
472
 
473
+
558
474
  // Get descriptive label for axis
559
475
  H.Axis.prototype.getDescription = function() {
560
476
  return (
@@ -566,24 +482,6 @@
566
482
  );
567
483
  };
568
484
 
569
- // Pan along axis in a direction (1 or -1), optionally with a defined
570
- // granularity (number of steps it takes to walk across current view)
571
- H.Axis.prototype.panStep = function(direction, granularity) {
572
- var gran = granularity || 3,
573
- extremes = this.getExtremes(),
574
- step = (extremes.max - extremes.min) / gran * direction,
575
- newMax = extremes.max + step,
576
- newMin = extremes.min + step,
577
- size = newMax - newMin;
578
- if (direction < 0 && newMin < extremes.dataMin) {
579
- newMin = extremes.dataMin;
580
- newMax = newMin + size;
581
- } else if (direction > 0 && newMax > extremes.dataMax) {
582
- newMax = extremes.dataMax;
583
- newMin = newMax - size;
584
- }
585
- this.setExtremes(newMin, newMax);
586
- };
587
485
 
588
486
  // Whenever adding or removing series, keep track of types present in chart
589
487
  H.wrap(H.Series.prototype, 'init', function(proceed) {
@@ -618,6 +516,7 @@
618
516
  }
619
517
  });
620
518
 
519
+
621
520
  // Return simplified description of chart type. Some types will not be familiar
622
521
  // to most screen reader users, but we try.
623
522
  H.Chart.prototype.getTypeDescription = function() {
@@ -635,6 +534,7 @@
635
534
  return firstType + ' chart.' + (typeDescriptionMap[firstType] || '');
636
535
  };
637
536
 
537
+
638
538
  // Return object with text description of each of the chart's axes
639
539
  H.Chart.prototype.getAxesDescription = function() {
640
540
  var numXAxes = this.xAxis.length,
@@ -671,6 +571,7 @@
671
571
  return desc;
672
572
  };
673
573
 
574
+
674
575
  // Set a11y attribs on exporting menu
675
576
  H.Chart.prototype.addAccessibleContextMenuAttribs = function() {
676
577
  var exportList = this.exportDivElements;
@@ -690,84 +591,713 @@
690
591
  }
691
592
  };
692
593
 
693
- // Highlight a point (show tooltip and display hover state). Returns the
694
- // highlighted point.
695
- H.Point.prototype.highlight = function() {
696
- var chart = this.series.chart;
697
- if (this.graphic && this.graphic.element.focus) {
698
- this.graphic.element.focus();
699
- }
700
- if (!this.isNull) {
701
- this.onMouseOver(); // Show the hover marker
702
- // Show the tooltip
703
- if (chart.tooltip) {
704
- chart.tooltip.refresh(chart.tooltip.shared ? [this] : this);
705
- }
706
- } else {
707
- if (chart.tooltip) {
708
- chart.tooltip.hide(0);
709
- }
710
- // Don't call blur on the element, as it messes up the chart div's focus
711
- }
712
- chart.highlightedPoint = this;
713
- return this;
714
- };
715
594
 
716
- // Function to highlight next/previous point in chart
717
- // Returns highlighted point on success, false on failure (no adjacent point to
718
- // highlight in chosen direction)
719
- H.Chart.prototype.highlightAdjacentPoint = function(next) {
595
+ // Add screen reader region to chart.
596
+ // tableId is the HTML id of the table to focus when clicking the table anchor
597
+ // in the screen reader region.
598
+ H.Chart.prototype.addScreenReaderRegion = function(id, tableId) {
720
599
  var chart = this,
721
600
  series = chart.series,
722
- curPoint = chart.highlightedPoint,
723
- curPointIndex = curPoint && curPoint.index || 0,
724
- curPoints = curPoint && curPoint.series.points,
725
- lastSeries = chart.series && chart.series[chart.series.length - 1],
726
- lastPoint = lastSeries && lastSeries.points &&
727
- lastSeries.points[lastSeries.points.length - 1],
728
- newSeries,
729
- newPoint,
730
- // Handle connecting ends - where the points array has an extra last
731
- // point that is a reference to the first one. We skip this.
732
- forwardSkipAmount = curPoint && curPoint.series.connectEnds &&
733
- curPointIndex > curPoints.length - 3 ? 2 : 1;
734
-
735
- // If no points, return false
736
- if (!series[0] || !series[0].points) {
737
- return false;
738
- }
739
-
740
- if (!curPoint) {
741
- // No point is highlighted yet. Try first/last point depending on move
742
- // direction
743
- newPoint = next ? series[0].points[0] : lastPoint;
744
- } else {
745
- // We have a highlighted point.
746
- // Find index of current point in series.points array. Necessary for
747
- // dataGrouping (and maybe zoom?)
748
- if (curPoints[curPointIndex] !== curPoint) {
749
- for (var i = 0; i < curPoints.length; ++i) {
750
- if (curPoints[i] === curPoint) {
751
- curPointIndex = i;
752
- break;
753
- }
754
- }
755
- }
601
+ options = chart.options,
602
+ a11yOptions = options.accessibility,
603
+ hiddenSection = chart.screenReaderRegion = doc.createElement('div'),
604
+ tableShortcut = doc.createElement('h4'),
605
+ tableShortcutAnchor = doc.createElement('a'),
606
+ chartHeading = doc.createElement('h4'),
607
+ chartTypes = chart.types || [],
608
+ // Build axis info - but not for pies and maps. Consider not adding for
609
+ // certain other types as well (funnel, pyramid?)
610
+ axesDesc = (
611
+ chartTypes.length === 1 && chartTypes[0] === 'pie' ||
612
+ chartTypes[0] === 'map'
613
+ ) && {} || chart.getAxesDescription(),
614
+ chartTypeInfo = series[0] && typeToSeriesMap[series[0].type] ||
615
+ typeToSeriesMap['default']; // eslint-disable-line dot-notation
756
616
 
757
- // Grab next/prev point & series
758
- newSeries = series[curPoint.series.index + (next ? 1 : -1)];
759
- newPoint = curPoints[curPointIndex + (next ? forwardSkipAmount : -1)] ||
760
- // Done with this series, try next one
761
- newSeries &&
762
- newSeries.points[next ? 0 : newSeries.points.length - (
763
- newSeries.connectEnds ? 2 : 1
764
- )];
617
+ hiddenSection.setAttribute('id', id);
618
+ hiddenSection.setAttribute('role', 'region');
619
+ hiddenSection.setAttribute(
620
+ 'aria-label',
621
+ 'Chart screen reader information.'
622
+ );
765
623
 
766
- // If there is no adjacent point, we return false
767
- if (newPoint === undefined) {
768
- return false;
769
- }
770
- }
624
+ hiddenSection.innerHTML =
625
+ a11yOptions.screenReaderSectionFormatter &&
626
+ a11yOptions.screenReaderSectionFormatter(chart) ||
627
+ '<div>Use regions/landmarks to skip ahead to chart' +
628
+ (series.length > 1 ? ' and navigate between data series' : '') +
629
+ '.</div><h3>' +
630
+ (options.title.text ? htmlencode(options.title.text) : 'Chart') +
631
+ (
632
+ options.subtitle && options.subtitle.text ?
633
+ '. ' + htmlencode(options.subtitle.text) :
634
+ ''
635
+ ) +
636
+ '</h3><h4>Long description.</h4><div>' +
637
+ (options.chart.description || 'No description available.') +
638
+ '</div><h4>Structure.</h4><div>Chart type: ' +
639
+ (options.chart.typeDescription || chart.getTypeDescription()) +
640
+ '</div>' +
641
+ (
642
+ series.length === 1 ?
643
+ (
644
+ '<div>' + chartTypeInfo[0] + ' with ' +
645
+ series[0].points.length + ' ' +
646
+ (
647
+ series[0].points.length === 1 ?
648
+ chartTypeInfo[1] :
649
+ chartTypeInfo[2]
650
+ ) +
651
+ '.</div>'
652
+ ) : ''
653
+ ) +
654
+ (axesDesc.xAxis ? ('<div>' + axesDesc.xAxis + '</div>') : '') +
655
+ (axesDesc.yAxis ? ('<div>' + axesDesc.yAxis + '</div>') : '');
656
+
657
+ // Add shortcut to data table if export-data is loaded
658
+ if (chart.getCSV) {
659
+ tableShortcutAnchor.innerHTML = 'View as data table.';
660
+ tableShortcutAnchor.href = '#' + tableId;
661
+ // Make this unreachable by user tabbing
662
+ tableShortcutAnchor.setAttribute('tabindex', '-1');
663
+ tableShortcutAnchor.onclick =
664
+ a11yOptions.onTableAnchorClick || function() {
665
+ chart.viewData();
666
+ doc.getElementById(tableId).focus();
667
+ };
668
+ tableShortcut.appendChild(tableShortcutAnchor);
669
+ hiddenSection.appendChild(tableShortcut);
670
+ }
671
+
672
+ // Note: JAWS seems to refuse to read aria-label on the container, so add an
673
+ // h4 element as title for the chart.
674
+ chartHeading.innerHTML = 'Chart graphic.';
675
+ chart.renderTo.insertBefore(chartHeading, chart.renderTo.firstChild);
676
+ chart.renderTo.insertBefore(hiddenSection, chart.renderTo.firstChild);
677
+
678
+ // Hide the section and the chart heading
679
+ merge(true, chartHeading.style, hiddenStyle);
680
+ merge(true, hiddenSection.style, hiddenStyle);
681
+ };
682
+
683
+
684
+ // Make chart container accessible, and wrap table functionality
685
+ H.Chart.prototype.callbacks.push(function(chart) {
686
+ var options = chart.options,
687
+ a11yOptions = options.accessibility;
688
+
689
+ if (!a11yOptions.enabled) {
690
+ return;
691
+ }
692
+
693
+ var titleElement = doc.createElementNS(
694
+ 'http://www.w3.org/2000/svg',
695
+ 'title'
696
+ ),
697
+ exportGroupElement = doc.createElementNS(
698
+ 'http://www.w3.org/2000/svg',
699
+ 'g'
700
+ ),
701
+ descElement = chart.container.getElementsByTagName('desc')[0],
702
+ textElements = chart.container.getElementsByTagName('text'),
703
+ titleId = 'highcharts-title-' + chart.index,
704
+ tableId = 'highcharts-data-table-' + chart.index,
705
+ hiddenSectionId = 'highcharts-information-region-' + chart.index,
706
+ chartTitle = options.title.text || 'Chart',
707
+ oldColumnHeaderFormatter = (
708
+ options.exporting &&
709
+ options.exporting.csv &&
710
+ options.exporting.csv.columnHeaderFormatter
711
+ ),
712
+ topLevelColumns = [];
713
+
714
+ // Add SVG title/desc tags
715
+ titleElement.textContent = htmlencode(chartTitle);
716
+ titleElement.id = titleId;
717
+ descElement.parentNode.insertBefore(titleElement, descElement);
718
+ chart.renderTo.setAttribute('role', 'region');
719
+ chart.renderTo.setAttribute(
720
+ 'aria-label',
721
+ stripTags(
722
+ 'Interactive chart. ' + chartTitle +
723
+ '. Use up and down arrows to navigate with most screen readers.'
724
+ )
725
+ );
726
+
727
+ // Set screen reader properties on export menu
728
+ if (
729
+ chart.exportSVGElements &&
730
+ chart.exportSVGElements[0] &&
731
+ chart.exportSVGElements[0].element
732
+ ) {
733
+ var oldExportCallback = chart.exportSVGElements[0].element.onclick,
734
+ parent = chart.exportSVGElements[0].element.parentNode;
735
+ chart.exportSVGElements[0].element.onclick = function() {
736
+ oldExportCallback.apply(
737
+ this,
738
+ Array.prototype.slice.call(arguments)
739
+ );
740
+ chart.addAccessibleContextMenuAttribs();
741
+ chart.highlightExportItem(0);
742
+ };
743
+ chart.exportSVGElements[0].element.setAttribute('role', 'button');
744
+ chart.exportSVGElements[0].element.setAttribute(
745
+ 'aria-label',
746
+ 'View export menu'
747
+ );
748
+ exportGroupElement.appendChild(chart.exportSVGElements[0].element);
749
+ exportGroupElement.setAttribute('role', 'region');
750
+ exportGroupElement.setAttribute('aria-label', 'Chart export menu');
751
+ parent.appendChild(exportGroupElement);
752
+ }
753
+
754
+ // Set screen reader properties on input boxes for range selector. We need
755
+ // to do this regardless of whether or not these are visible, as they are
756
+ // by default part of the page's tabindex unless we set them to -1.
757
+ if (chart.rangeSelector) {
758
+ each(['minInput', 'maxInput'], function(key, i) {
759
+ if (chart.rangeSelector[key]) {
760
+ chart.rangeSelector[key].setAttribute('tabindex', '-1');
761
+ chart.rangeSelector[key].setAttribute('role', 'textbox');
762
+ chart.rangeSelector[key].setAttribute(
763
+ 'aria-label',
764
+ 'Select ' + (i ? 'end' : 'start') + ' date.'
765
+ );
766
+ }
767
+ });
768
+ }
769
+
770
+ // Hide text elements from screen readers
771
+ each(textElements, function(el) {
772
+ el.setAttribute('aria-hidden', 'true');
773
+ });
774
+
775
+ // Add top-secret screen reader region
776
+ chart.addScreenReaderRegion(hiddenSectionId, tableId);
777
+
778
+
779
+ /* Wrap table functionality from export-data */
780
+ /* TODO: Can't we just do this in export-data? */
781
+
782
+ // Keep track of columns
783
+ merge(true, options.exporting, {
784
+ csv: {
785
+ columnHeaderFormatter: function(item, key, keyLength) {
786
+ if (!item) {
787
+ return 'Category';
788
+ }
789
+ if (item instanceof H.Axis) {
790
+ return (item.options.title && item.options.title.text) ||
791
+ (item.isDatetimeAxis ? 'DateTime' : 'Category');
792
+ }
793
+ var prevCol = topLevelColumns[topLevelColumns.length - 1];
794
+ if (keyLength > 1) {
795
+ // We need multiple levels of column headers
796
+ // Populate a list of column headers to add in addition to
797
+ // the ones added by export-data
798
+ if ((prevCol && prevCol.text) !== item.name) {
799
+ topLevelColumns.push({
800
+ text: item.name,
801
+ span: keyLength
802
+ });
803
+ }
804
+ }
805
+ if (oldColumnHeaderFormatter) {
806
+ return oldColumnHeaderFormatter.call(
807
+ this,
808
+ item,
809
+ key,
810
+ keyLength
811
+ );
812
+ }
813
+ return keyLength > 1 ? key : item.name;
814
+ }
815
+ }
816
+ });
817
+
818
+ // Add ID and title/caption to table HTML
819
+ H.wrap(chart, 'getTable', function(proceed) {
820
+ return proceed.apply(this, Array.prototype.slice.call(arguments, 1))
821
+ .replace(
822
+ '<table>',
823
+ '<table id="' + tableId + '" summary="Table representation ' +
824
+ 'of chart"><caption>' + chartTitle + '</caption>'
825
+ );
826
+ });
827
+
828
+ // Add accessibility attributes and top level columns
829
+ H.wrap(chart, 'viewData', function(proceed) {
830
+ if (!this.dataTableDiv) {
831
+ proceed.apply(this, Array.prototype.slice.call(arguments, 1));
832
+
833
+ var table = doc.getElementById(tableId),
834
+ head = table.getElementsByTagName('thead')[0],
835
+ body = table.getElementsByTagName('tbody')[0],
836
+ firstRow = head.firstChild.children,
837
+ columnHeaderRow = '<tr><td></td>',
838
+ cell,
839
+ newCell;
840
+
841
+ // Make table focusable by script
842
+ table.setAttribute('tabindex', '-1');
843
+
844
+ // Create row headers
845
+ each(body.children, function(el) {
846
+ cell = el.firstChild;
847
+ newCell = doc.createElement('th');
848
+ newCell.setAttribute('scope', 'row');
849
+ newCell.innerHTML = cell.innerHTML;
850
+ cell.parentNode.replaceChild(newCell, cell);
851
+ });
852
+
853
+ // Set scope for column headers
854
+ each(firstRow, function(el) {
855
+ if (el.tagName === 'TH') {
856
+ el.setAttribute('scope', 'col');
857
+ }
858
+ });
859
+
860
+ // Add top level columns
861
+ if (topLevelColumns.length) {
862
+ each(topLevelColumns, function(col) {
863
+ columnHeaderRow += '<th scope="col" colspan="' + col.span +
864
+ '">' + col.text + '</th>';
865
+ });
866
+ head.insertAdjacentHTML('afterbegin', columnHeaderRow);
867
+ }
868
+ }
869
+ });
870
+ });
871
+
872
+ }(Highcharts));
873
+ (function(H) {
874
+ /**
875
+ * Accessibility module - Keyboard navigation
876
+ *
877
+ * (c) 2010-2017 Highsoft AS
878
+ * Author: Oystein Moseng
879
+ *
880
+ * License: www.highcharts.com/license
881
+ */
882
+ /* eslint max-len: ["warn", 80, 4] */
883
+
884
+ var win = H.win,
885
+ doc = win.document,
886
+ each = H.each,
887
+ addEvent = H.addEvent,
888
+ fireEvent = H.fireEvent,
889
+ merge = H.merge,
890
+ pick = H.pick;
891
+
892
+ // Add focus border functionality to SVGElements.
893
+ // Draws a new rect on top of element around its bounding box.
894
+ H.extend(H.SVGElement.prototype, {
895
+ addFocusBorder: function(margin, style) {
896
+ // Allow updating by just adding new border
897
+ if (this.focusBorder) {
898
+ this.removeFocusBorder();
899
+ }
900
+ // Add the border rect
901
+ var bb = this.getBBox(),
902
+ pad = pick(margin, 3);
903
+ this.focusBorder = this.renderer.rect(
904
+ bb.x - pad,
905
+ bb.y - pad,
906
+ bb.width + 2 * pad,
907
+ bb.height + 2 * pad,
908
+ style && style.borderRadius
909
+ )
910
+ .addClass('highcharts-focus-border')
911
+
912
+ .attr({
913
+ stroke: style && style.stroke,
914
+ 'stroke-width': style && style.strokeWidth
915
+ })
916
+
917
+ .attr({
918
+ zIndex: 99
919
+ })
920
+ .add(this.parentGroup);
921
+ },
922
+
923
+ removeFocusBorder: function() {
924
+ if (this.focusBorder) {
925
+ this.focusBorder.destroy();
926
+ delete this.focusBorder;
927
+ }
928
+ }
929
+ });
930
+
931
+
932
+ // Set for which series types it makes sense to move to the closest point with
933
+ // up/down arrows, and which series types should just move to next series.
934
+ H.Series.prototype.keyboardMoveVertical = true;
935
+ each(['column', 'pie'], function(type) {
936
+ if (H.seriesTypes[type]) {
937
+ H.seriesTypes[type].prototype.keyboardMoveVertical = false;
938
+ }
939
+ });
940
+
941
+ /**
942
+ * Strip HTML tags away from a string. Used for aria-label attributes, painting
943
+ * on a canvas will fail if the text contains tags.
944
+ * @param {String} s The input string
945
+ * @return {String} The filtered string
946
+ */
947
+ function stripTags(s) {
948
+ return typeof s === 'string' ? s.replace(/<\/?[^>]+(>|$)/g, '') : s;
949
+ }
950
+
951
+
952
+ H.setOptions({
953
+ accessibility: {
954
+
955
+ /**
956
+ * Options for keyboard navigation.
957
+ *
958
+ * @type {Object}
959
+ * @since 5.0.0
960
+ */
961
+ keyboardNavigation: {
962
+
963
+ /**
964
+ * Enable keyboard navigation for the chart.
965
+ *
966
+ * @type {Boolean}
967
+ * @default true
968
+ * @since 5.0.0
969
+ */
970
+ enabled: true,
971
+
972
+ /**
973
+ * Options for the focus border drawn around elements while
974
+ * navigating through them.
975
+ *
976
+ * @since 6.0.3
977
+ */
978
+ focusBorder: {
979
+ /**
980
+ * Enable/disable focus border for chart.
981
+ */
982
+ enabled: true,
983
+
984
+ /**
985
+ * Style options for the focus border drawn around elements
986
+ * while navigating through them. Note that some browsers in
987
+ * addition draw their own borders for focused elements. These
988
+ * automatic borders can not be styled by Highcharts.
989
+ *
990
+ * In styled mode, the border is given the
991
+ * `.highcharts-focus-border` class.
992
+ */
993
+ style: {
994
+ color: '#000000',
995
+ lineWidth: 1,
996
+ borderRadius: 2
997
+ },
998
+
999
+ /**
1000
+ * Focus border margin around the elements.
1001
+ */
1002
+ margin: 2
1003
+ }
1004
+
1005
+ /**
1006
+ * Skip null points when navigating through points with the
1007
+ * keyboard.
1008
+ *
1009
+ * @type {Boolean}
1010
+ * @default false
1011
+ * @since 5.0.0
1012
+ * @apioption accessibility.keyboardNavigation.skipNullPoints
1013
+ */
1014
+ }
1015
+ }
1016
+ });
1017
+
1018
+ /**
1019
+ * Keyboard navigation for the legend. Requires the Accessibility module.
1020
+ * @since 5.0.14
1021
+ * @apioption legend.keyboardNavigation
1022
+ */
1023
+
1024
+ /**
1025
+ * Enable/disable keyboard navigation for the legend. Requires the Accessibility
1026
+ * module.
1027
+ *
1028
+ * @type {Boolean}
1029
+ * @see [accessibility.keyboardNavigation](#accessibility.keyboardNavigation.
1030
+ * enabled)
1031
+ * @default true
1032
+ * @since 5.0.13
1033
+ * @apioption legend.keyboardNavigation.enabled
1034
+ */
1035
+
1036
+
1037
+ // Abstraction layer for keyboard navigation. Keep a map of keyCodes to
1038
+ // handler functions, and a next/prev move handler for tab order. The
1039
+ // module's keyCode handlers determine when to move to another module.
1040
+ // Validate holds a function to determine if there are prerequisites for
1041
+ // this module to run that are not met. Init holds a function to run once
1042
+ // before any keyCodes are interpreted. Terminate holds a function to run
1043
+ // once before moving to next/prev module.
1044
+ // The chart object keeps track of a list of KeyboardNavigationModules.
1045
+ function KeyboardNavigationModule(chart, options) {
1046
+ this.chart = chart;
1047
+ this.id = options.id;
1048
+ this.keyCodeMap = options.keyCodeMap;
1049
+ this.validate = options.validate;
1050
+ this.init = options.init;
1051
+ this.terminate = options.terminate;
1052
+ }
1053
+ KeyboardNavigationModule.prototype = {
1054
+ // Find handler function(s) for key code in the keyCodeMap and run it.
1055
+ run: function(e) {
1056
+ var navModule = this,
1057
+ keyCode = e.which || e.keyCode,
1058
+ found = false,
1059
+ handled = false;
1060
+ each(this.keyCodeMap, function(codeSet) {
1061
+ if (codeSet[0].indexOf(keyCode) > -1) {
1062
+ found = true;
1063
+ handled = codeSet[1].call(navModule, keyCode, e) === false ?
1064
+ // If explicitly returning false, we haven't handled it
1065
+ false :
1066
+ true;
1067
+ }
1068
+ });
1069
+ // Default tab handler, move to next/prev module
1070
+ if (!found && keyCode === 9) {
1071
+ handled = this.move(e.shiftKey ? -1 : 1);
1072
+ }
1073
+ return handled;
1074
+ },
1075
+
1076
+ // Move to next/prev valid module, or undefined if none, and init
1077
+ // it. Returns true on success and false if there is no valid module
1078
+ // to move to.
1079
+ move: function(direction) {
1080
+ var chart = this.chart;
1081
+ if (this.terminate) {
1082
+ this.terminate(direction);
1083
+ }
1084
+ chart.keyboardNavigationModuleIndex += direction;
1085
+ var newModule = chart.keyboardNavigationModules[
1086
+ chart.keyboardNavigationModuleIndex
1087
+ ];
1088
+
1089
+ // Remove existing focus border if any
1090
+ if (chart.focusElement) {
1091
+ chart.focusElement.removeFocusBorder();
1092
+ }
1093
+
1094
+ // Verify new module
1095
+ if (newModule) {
1096
+ if (newModule.validate && !newModule.validate()) {
1097
+ return this.move(direction); // Invalid module, recurse
1098
+ }
1099
+ if (newModule.init) {
1100
+ newModule.init(direction); // Valid module, init it
1101
+ return true;
1102
+ }
1103
+ }
1104
+ // No module
1105
+ chart.keyboardNavigationModuleIndex = 0; // Reset counter
1106
+
1107
+ // Set focus to chart or exit anchor depending on direction
1108
+ if (direction > 0) {
1109
+ this.chart.exiting = true;
1110
+ this.chart.tabExitAnchor.focus();
1111
+ } else {
1112
+ this.chart.renderTo.focus();
1113
+ }
1114
+
1115
+ return false;
1116
+ }
1117
+ };
1118
+
1119
+
1120
+ // Utility function to attempt to fake a click event on an element
1121
+ function fakeClickEvent(element) {
1122
+ var fakeEvent;
1123
+ if (element && element.onclick && doc.createEvent) {
1124
+ fakeEvent = doc.createEvent('Events');
1125
+ fakeEvent.initEvent('click', true, false);
1126
+ element.onclick(fakeEvent);
1127
+ }
1128
+ }
1129
+
1130
+
1131
+ // Determine if a point should be skipped
1132
+ function isSkipPoint(point) {
1133
+ return point.isNull &&
1134
+ point.series.chart.options.accessibility
1135
+ .keyboardNavigation.skipNullPoints ||
1136
+ point.series.options.skipKeyboardNavigation ||
1137
+ !point.series.visible;
1138
+ }
1139
+
1140
+
1141
+ // Get the point in a series that is closest (in distance) to a reference point
1142
+ // Optionally supply weight factors for x and y directions
1143
+ function getClosestPoint(point, series, xWeight, yWeight) {
1144
+ var minDistance = Infinity,
1145
+ dPoint,
1146
+ minIx,
1147
+ distance,
1148
+ i = series.points.length;
1149
+ if (point.plotX === undefined || point.plotY === undefined) {
1150
+ return;
1151
+ }
1152
+ while (i--) {
1153
+ dPoint = series.points[i];
1154
+ if (dPoint.plotX === undefined || dPoint.plotY === undefined) {
1155
+ return;
1156
+ }
1157
+ distance = (point.plotX - dPoint.plotX) *
1158
+ (point.plotX - dPoint.plotX) * (xWeight || 1) +
1159
+ (point.plotY - dPoint.plotY) *
1160
+ (point.plotY - dPoint.plotY) * (yWeight || 1);
1161
+ if (distance < minDistance) {
1162
+ minDistance = distance;
1163
+ minIx = i;
1164
+ }
1165
+ }
1166
+ return series.points[minIx || 0];
1167
+ }
1168
+
1169
+
1170
+ // Pan along axis in a direction (1 or -1), optionally with a defined
1171
+ // granularity (number of steps it takes to walk across current view)
1172
+ H.Axis.prototype.panStep = function(direction, granularity) {
1173
+ var gran = granularity || 3,
1174
+ extremes = this.getExtremes(),
1175
+ step = (extremes.max - extremes.min) / gran * direction,
1176
+ newMax = extremes.max + step,
1177
+ newMin = extremes.min + step,
1178
+ size = newMax - newMin;
1179
+ if (direction < 0 && newMin < extremes.dataMin) {
1180
+ newMin = extremes.dataMin;
1181
+ newMax = newMin + size;
1182
+ } else if (direction > 0 && newMax > extremes.dataMax) {
1183
+ newMax = extremes.dataMax;
1184
+ newMin = newMax - size;
1185
+ }
1186
+ this.setExtremes(newMin, newMax);
1187
+ };
1188
+
1189
+
1190
+ // Set chart's focus to an SVGElement. Calls focus() on it, and draws the focus
1191
+ // border. If the focusElement argument is supplied, it draws the border around
1192
+ // svgElement and sets the focus to focusElement.
1193
+ H.Chart.prototype.setFocusToElement = function(svgElement, focusElement) {
1194
+ var focusBorderOptions = this.options.accessibility
1195
+ .keyboardNavigation.focusBorder;
1196
+ if (focusBorderOptions.enabled && svgElement !== this.focusElement) {
1197
+ // Remove old focus border
1198
+ if (this.focusElement) {
1199
+ this.focusElement.removeFocusBorder();
1200
+ }
1201
+ // Set browser focus if possible
1202
+ if (
1203
+ focusElement &&
1204
+ focusElement.element &&
1205
+ focusElement.element.focus
1206
+ ) {
1207
+ focusElement.element.focus();
1208
+ } else if (svgElement.element.focus) {
1209
+ svgElement.element.focus();
1210
+ }
1211
+ // Draw focus border (since some browsers don't do it automatically)
1212
+ svgElement.addFocusBorder(focusBorderOptions.margin, {
1213
+ stroke: focusBorderOptions.style.color,
1214
+ strokeWidth: focusBorderOptions.style.lineWidth,
1215
+ borderRadius: focusBorderOptions.style.borderRadius
1216
+ });
1217
+ this.focusElement = svgElement;
1218
+ }
1219
+ };
1220
+
1221
+
1222
+ // Highlight a point (show tooltip and display hover state). Returns the
1223
+ // highlighted point.
1224
+ H.Point.prototype.highlight = function() {
1225
+ var chart = this.series.chart;
1226
+ if (!this.isNull) {
1227
+ this.onMouseOver(); // Show the hover marker and tooltip
1228
+ } else {
1229
+ if (chart.tooltip) {
1230
+ chart.tooltip.hide(0);
1231
+ }
1232
+ // Don't call blur on the element, as it messes up the chart div's focus
1233
+ }
1234
+
1235
+ // We focus only after calling onMouseOver because the state change can
1236
+ // change z-index and mess up the element.
1237
+ if (this.graphic) {
1238
+ chart.setFocusToElement(this.graphic);
1239
+ }
1240
+
1241
+ chart.highlightedPoint = this;
1242
+ return this;
1243
+ };
1244
+
1245
+
1246
+ // Function to highlight next/previous point in chart
1247
+ // Returns highlighted point on success, false on failure (no adjacent point to
1248
+ // highlight in chosen direction)
1249
+ H.Chart.prototype.highlightAdjacentPoint = function(next) {
1250
+ var chart = this,
1251
+ series = chart.series,
1252
+ curPoint = chart.highlightedPoint,
1253
+ curPointIndex = curPoint && curPoint.index || 0,
1254
+ curPoints = curPoint && curPoint.series.points,
1255
+ lastSeries = chart.series && chart.series[chart.series.length - 1],
1256
+ lastPoint = lastSeries && lastSeries.points &&
1257
+ lastSeries.points[lastSeries.points.length - 1],
1258
+ newSeries,
1259
+ newPoint,
1260
+ // Handle connecting ends - where the points array has an extra last
1261
+ // point that is a reference to the first one. We skip this.
1262
+ forwardSkipAmount = curPoint && curPoint.series.connectEnds &&
1263
+ curPointIndex > curPoints.length - 3 ? 2 : 1;
1264
+
1265
+ // If no points, return false
1266
+ if (!series[0] || !series[0].points) {
1267
+ return false;
1268
+ }
1269
+
1270
+ if (!curPoint) {
1271
+ // No point is highlighted yet. Try first/last point depending on move
1272
+ // direction
1273
+ newPoint = next ? series[0].points[0] : lastPoint;
1274
+ } else {
1275
+ // We have a highlighted point.
1276
+ // Find index of current point in series.points array. Necessary for
1277
+ // dataGrouping (and maybe zoom?)
1278
+ if (curPoints[curPointIndex] !== curPoint) {
1279
+ for (var i = 0; i < curPoints.length; ++i) {
1280
+ if (curPoints[i] === curPoint) {
1281
+ curPointIndex = i;
1282
+ break;
1283
+ }
1284
+ }
1285
+ }
1286
+
1287
+ // Grab next/prev point & series
1288
+ newSeries = series[curPoint.series.index + (next ? 1 : -1)];
1289
+ newPoint = curPoints[curPointIndex + (next ? forwardSkipAmount : -1)] ||
1290
+ // Done with this series, try next one
1291
+ newSeries &&
1292
+ newSeries.points[next ? 0 : newSeries.points.length - (
1293
+ newSeries.connectEnds ? 2 : 1
1294
+ )];
1295
+
1296
+ // If there is no adjacent point, we return false
1297
+ if (newPoint === undefined) {
1298
+ return false;
1299
+ }
1300
+ }
771
1301
 
772
1302
  // Recursively skip null points or points in series that should be skipped
773
1303
  if (isSkipPoint(newPoint)) {
@@ -779,6 +1309,7 @@
779
1309
  return newPoint.highlight();
780
1310
  };
781
1311
 
1312
+
782
1313
  // Highlight first valid point in a series. Returns the point if successfully
783
1314
  // highlighted, otherwise false. If there is a highlighted point in the series,
784
1315
  // use that as starting point.
@@ -800,6 +1331,7 @@
800
1331
  return false;
801
1332
  };
802
1333
 
1334
+
803
1335
  // Highlight next/previous series in chart. Returns false if no adjacent series
804
1336
  // in the direction, otherwise returns new highlighted point.
805
1337
  H.Chart.prototype.highlightAdjacentSeries = function(down) {
@@ -853,6 +1385,7 @@
853
1385
  return newPoint.series.highlightFirstValidPoint();
854
1386
  };
855
1387
 
1388
+
856
1389
  // Highlight the closest point vertically
857
1390
  H.Chart.prototype.highlightAdjacentPointVertical = function(down) {
858
1391
  var curPoint = this.highlightedPoint,
@@ -896,6 +1429,7 @@
896
1429
  return bestPoint ? bestPoint.highlight() : false;
897
1430
  };
898
1431
 
1432
+
899
1433
  // Show the export menu and focus the first item (if exists)
900
1434
  H.Chart.prototype.showExportMenu = function() {
901
1435
  if (this.exportSVGElements && this.exportSVGElements[0]) {
@@ -904,6 +1438,26 @@
904
1438
  }
905
1439
  };
906
1440
 
1441
+
1442
+ // Hide export menu
1443
+ H.Chart.prototype.hideExportMenu = function() {
1444
+ var exportList = this.exportDivElements;
1445
+ if (exportList) {
1446
+ each(exportList, function(el) {
1447
+ fireEvent(el, 'mouseleave');
1448
+ });
1449
+ if (
1450
+ exportList[this.highlightedExportItem] &&
1451
+ exportList[this.highlightedExportItem].onmouseout
1452
+ ) {
1453
+ exportList[this.highlightedExportItem].onmouseout();
1454
+ }
1455
+ this.highlightedExportItem = 0;
1456
+ this.renderTo.focus();
1457
+ }
1458
+ };
1459
+
1460
+
907
1461
  // Highlight export menu item by index
908
1462
  H.Chart.prototype.highlightExportItem = function(ix) {
909
1463
  var listItem = this.exportDivElements && this.exportDivElements[ix],
@@ -930,6 +1484,7 @@
930
1484
  }
931
1485
  };
932
1486
 
1487
+
933
1488
  // Highlight range selector button by index
934
1489
  H.Chart.prototype.highlightRangeSelectorButton = function(ix) {
935
1490
  var buttons = this.rangeSelector.buttons;
@@ -942,9 +1497,7 @@
942
1497
  // Select new
943
1498
  this.highlightedRangeSelectorItemIx = ix;
944
1499
  if (buttons[ix]) {
945
- if (buttons[ix].element.focus) {
946
- buttons[ix].element.focus();
947
- }
1500
+ this.setFocusToElement(buttons[ix].box, buttons[ix]);
948
1501
  this.oldRangeSelectorItemState = buttons[ix].state;
949
1502
  buttons[ix].setState(2);
950
1503
  return true;
@@ -952,146 +1505,39 @@
952
1505
  return false;
953
1506
  };
954
1507
 
1508
+
955
1509
  // Highlight legend item by index
956
1510
  H.Chart.prototype.highlightLegendItem = function(ix) {
957
- var items = this.legend.allItems;
958
- if (items[this.highlightedLegendItemIx]) {
959
- fireEvent(
960
- items[this.highlightedLegendItemIx].legendGroup.element,
961
- 'mouseout'
962
- );
963
- }
964
- this.highlightedLegendItemIx = ix;
1511
+ var items = this.legend.allItems,
1512
+ oldIx = this.highlightedLegendItemIx;
965
1513
  if (items[ix]) {
966
- if (items[ix].legendGroup.element.focus) {
967
- items[ix].legendGroup.element.focus();
1514
+ if (items[oldIx]) {
1515
+ fireEvent(
1516
+ items[oldIx].legendGroup.element,
1517
+ 'mouseout'
1518
+ );
968
1519
  }
1520
+ this.highlightedLegendItemIx = ix;
1521
+ this.setFocusToElement(items[ix].legendItem, items[ix].legendGroup);
969
1522
  fireEvent(items[ix].legendGroup.element, 'mouseover');
970
1523
  return true;
971
1524
  }
972
1525
  return false;
973
1526
  };
974
1527
 
975
- // Hide export menu
976
- H.Chart.prototype.hideExportMenu = function() {
977
- var exportList = this.exportDivElements;
978
- if (exportList) {
979
- each(exportList, function(el) {
980
- fireEvent(el, 'mouseleave');
981
- });
982
- if (
983
- exportList[this.highlightedExportItem] &&
984
- exportList[this.highlightedExportItem].onmouseout
985
- ) {
986
- exportList[this.highlightedExportItem].onmouseout();
987
- }
988
- this.highlightedExportItem = 0;
989
- this.renderTo.focus();
990
- }
991
- };
992
1528
 
993
- // Add keyboard navigation handling to chart
994
- H.Chart.prototype.addKeyboardNavEvents = function() {
1529
+ // Add keyboard navigation handling modules to chart
1530
+ H.Chart.prototype.addKeyboardNavigationModules = function() {
995
1531
  var chart = this;
996
1532
 
997
- // Abstraction layer for keyboard navigation. Keep a map of keyCodes to
998
- // handler functions, and a next/prev move handler for tab order. The
999
- // module's keyCode handlers determine when to move to another module.
1000
- // Validate holds a function to determine if there are prerequisites for
1001
- // this module to run that are not met. Init holds a function to run once
1002
- // before any keyCodes are interpreted. Terminate holds a function to run
1003
- // once before moving to next/prev module.
1004
- function KeyboardNavigationModule(options) {
1005
- this.id = options.id;
1006
- this.keyCodeMap = options.keyCodeMap;
1007
- this.move = options.move;
1008
- this.validate = options.validate;
1009
- this.init = options.init;
1010
- this.terminate = options.terminate;
1011
- }
1012
- KeyboardNavigationModule.prototype = {
1013
- // Find handler function(s) for key code in the keyCodeMap and run it.
1014
- run: function(e) {
1015
- var navModule = this,
1016
- keyCode = e.which || e.keyCode,
1017
- found = false,
1018
- handled = false;
1019
- each(this.keyCodeMap, function(codeSet) {
1020
- if (codeSet[0].indexOf(keyCode) > -1) {
1021
- found = true;
1022
- handled = codeSet[1].call(navModule, keyCode, e) === false ?
1023
- // If explicitly returning false, we haven't handled it
1024
- false :
1025
- true;
1026
- }
1027
- });
1028
- // Default tab handler, move to next/prev module
1029
- if (!found && keyCode === 9) {
1030
- handled = this.move(e.shiftKey ? -1 : 1);
1031
- }
1032
- return handled;
1033
- }
1034
- };
1035
- // Maintain abstraction between KeyboardNavigationModule and Highcharts.
1036
- // The chart object keeps track of a list of KeyboardNavigationModules that
1037
- // we move through.
1038
1533
  function navModuleFactory(id, keyMap, options) {
1039
- return new KeyboardNavigationModule(merge({
1040
- keyCodeMap: keyMap,
1041
- // Move to next/prev valid module, or undefined if none, and init
1042
- // it. Returns true on success and false if there is no valid module
1043
- // to move to.
1044
- move: function(direction) {
1045
- if (this.terminate) {
1046
- this.terminate(direction);
1047
- }
1048
- chart.keyboardNavigationModuleIndex += direction;
1049
- var newModule = chart.keyboardNavigationModules[
1050
- chart.keyboardNavigationModuleIndex
1051
- ];
1052
- if (newModule) {
1053
- if (newModule.validate && !newModule.validate()) {
1054
- return this.move(direction); // Invalid module
1055
- }
1056
- if (newModule.init) {
1057
- newModule.init(direction); // Valid module, init it
1058
- return true;
1059
- }
1060
- }
1061
- // No module
1062
- chart.keyboardNavigationModuleIndex = 0; // Reset counter
1063
-
1064
- // Set focus to chart or exit anchor depending on direction
1065
- if (direction > 0) {
1066
- chart.tabExitAnchor.focus();
1067
- } else {
1068
- chart.renderTo.focus();
1069
- }
1070
-
1071
- return false;
1072
- }
1534
+ return new KeyboardNavigationModule(chart, merge({
1535
+ keyCodeMap: keyMap
1073
1536
  }, {
1074
1537
  id: id
1075
1538
  }, options));
1076
1539
  }
1077
1540
 
1078
- // Route keydown events
1079
- function keydownHandler(ev) {
1080
- var e = ev || win.event,
1081
- curNavModule = chart.keyboardNavigationModules[
1082
- chart.keyboardNavigationModuleIndex
1083
- ];
1084
-
1085
- // If there is a navigation module for the current index, run it.
1086
- // Otherwise, we are outside of the chart in some direction.
1087
- if (curNavModule) {
1088
- if (curNavModule.run(e)) {
1089
- // Successfully handled this key event, stop default handling
1090
- e.preventDefault();
1091
- }
1092
- }
1093
- }
1094
-
1095
1541
  // List of the different keyboard handling modes we use depending on where
1096
1542
  // we are in the chart. Each mode has a set of handling functions mapped to
1097
1543
  // key codes. Each mode determines when to move to the next/prev mode.
@@ -1240,550 +1686,352 @@
1240
1686
  chart.hideExportMenu();
1241
1687
  }
1242
1688
  }),
1243
-
1244
- // Map zoom
1245
- navModuleFactory('mapZoom', [
1246
- // Up/down/left/right
1247
- [
1248
- [38, 40, 37, 39],
1249
- function(keyCode) {
1250
- chart[keyCode === 38 || keyCode === 40 ? 'yAxis' : 'xAxis'][0]
1251
- .panStep(keyCode < 39 ? -1 : 1);
1252
- }
1253
- ],
1254
-
1255
- // Tabs
1256
- [
1257
- [9],
1258
- function(keyCode, e) {
1259
- var button;
1260
- // Deselect old
1261
- chart.mapNavButtons[chart.focusedMapNavButtonIx].setState(0);
1262
- if (
1263
- e.shiftKey && !chart.focusedMapNavButtonIx ||
1264
- !e.shiftKey && chart.focusedMapNavButtonIx
1265
- ) { // trying to go somewhere we can't?
1266
- chart.mapZoom(); // Reset zoom
1267
- // Nowhere to go, go to prev/next module
1268
- return this.move(e.shiftKey ? -1 : 1);
1269
- }
1270
- chart.focusedMapNavButtonIx += e.shiftKey ? -1 : 1;
1271
- button = chart.mapNavButtons[chart.focusedMapNavButtonIx];
1272
- if (button.element.focus) {
1273
- button.element.focus();
1274
- }
1275
- button.setState(2);
1276
- }
1277
- ],
1278
-
1279
- // Enter/Spacebar
1280
- [
1281
- [13, 32],
1282
- function() {
1283
- fakeClickEvent(
1284
- chart.mapNavButtons[chart.focusedMapNavButtonIx].element
1285
- );
1286
- }
1287
- ]
1288
- ], {
1289
- // Only run this module if we have map zoom on the chart
1290
- validate: function() {
1291
- return (
1292
- chart.mapZoom &&
1293
- chart.mapNavButtons &&
1294
- chart.mapNavButtons.length === 2
1295
- );
1296
- },
1297
-
1298
- // Make zoom buttons do their magic
1299
- init: function(direction) {
1300
- var zoomIn = chart.mapNavButtons[0],
1301
- zoomOut = chart.mapNavButtons[1],
1302
- initialButton = direction > 0 ? zoomIn : zoomOut;
1303
-
1304
- each(chart.mapNavButtons, function(button, i) {
1305
- button.element.setAttribute('tabindex', -1);
1306
- button.element.setAttribute('role', 'button');
1307
- button.element.setAttribute(
1308
- 'aria-label',
1309
- 'Zoom ' + (i ? 'out' : '') + 'chart'
1310
- );
1311
- });
1312
-
1313
- if (initialButton.element.focus) {
1314
- initialButton.element.focus();
1315
- }
1316
- initialButton.setState(2);
1317
- chart.focusedMapNavButtonIx = direction > 0 ? 0 : 1;
1318
- }
1319
- }),
1320
-
1321
- // Highstock range selector (minus input boxes)
1322
- navModuleFactory('rangeSelector', [
1323
- // Left/Right/Up/Down
1324
- [
1325
- [37, 39, 38, 40],
1326
- function(keyCode) {
1327
- var direction = (keyCode === 37 || keyCode === 38) ? -1 : 1;
1328
- // Try to highlight next/prev button
1329
- if (!chart.highlightRangeSelectorButton(
1330
- chart.highlightedRangeSelectorItemIx + direction
1331
- )) {
1332
- return this.move(direction);
1333
- }
1334
- }
1335
- ],
1336
- // Enter/Spacebar
1337
- [
1338
- [13, 32],
1339
- function() {
1340
- // Don't allow click if button used to be disabled
1341
- if (chart.oldRangeSelectorItemState !== 3) {
1342
- fakeClickEvent(
1343
- chart.rangeSelector.buttons[
1344
- chart.highlightedRangeSelectorItemIx
1345
- ].element
1346
- );
1347
- }
1348
- }
1349
- ]
1350
- ], {
1351
- // Only run this module if we have range selector
1352
- validate: function() {
1353
- return (
1354
- chart.rangeSelector &&
1355
- chart.rangeSelector.buttons &&
1356
- chart.rangeSelector.buttons.length
1357
- );
1358
- },
1359
-
1360
- // Make elements focusable and accessible
1361
- init: function(direction) {
1362
- each(chart.rangeSelector.buttons, function(button) {
1363
- button.element.setAttribute('tabindex', '-1');
1364
- button.element.setAttribute('role', 'button');
1365
- button.element.setAttribute(
1366
- 'aria-label',
1367
- 'Select range ' + (button.text && button.text.textStr)
1368
- );
1369
- });
1370
- // Focus first/last button
1371
- chart.highlightRangeSelectorButton(
1372
- direction > 0 ? 0 : chart.rangeSelector.buttons.length - 1
1373
- );
1374
- }
1375
- }),
1376
-
1377
- // Highstock range selector, input boxes
1378
- navModuleFactory('rangeSelectorInput', [
1379
- // Tab/Up/Down
1380
- [
1381
- [9, 38, 40],
1382
- function(keyCode, e) {
1383
- var direction =
1384
- (keyCode === 9 && e.shiftKey || keyCode === 38) ? -1 : 1,
1385
-
1386
- newIx = chart.highlightedInputRangeIx =
1387
- chart.highlightedInputRangeIx + direction;
1388
-
1389
- // Try to highlight next/prev item in list.
1390
- if (newIx > 1 || newIx < 0) { // Out of range
1391
- return this.move(direction);
1392
- }
1393
- chart.rangeSelector[newIx ? 'maxInput' : 'minInput'].focus();
1394
- }
1395
- ]
1396
- ], {
1397
- // Only run if we have range selector with input boxes
1398
- validate: function() {
1399
- var inputVisible = (
1400
- chart.rangeSelector &&
1401
- chart.rangeSelector.inputGroup &&
1402
- chart.rangeSelector.inputGroup.element
1403
- .getAttribute('visibility') !== 'hidden'
1404
- );
1405
- return (
1406
- inputVisible &&
1407
- chart.options.rangeSelector.inputEnabled !== false &&
1408
- chart.rangeSelector.minInput &&
1409
- chart.rangeSelector.maxInput
1410
- );
1411
- },
1412
-
1413
- // Highlight first/last input box
1414
- init: function(direction) {
1415
- chart.highlightedInputRangeIx = direction > 0 ? 0 : 1;
1416
- chart.rangeSelector[
1417
- chart.highlightedInputRangeIx ? 'maxInput' : 'minInput'
1418
- ].focus();
1419
- }
1420
- }),
1421
-
1422
- // Legend navigation
1423
- navModuleFactory('legend', [
1424
- // Left/Right/Up/Down
1689
+
1690
+ // Map zoom
1691
+ navModuleFactory('mapZoom', [
1692
+ // Up/down/left/right
1425
1693
  [
1426
- [37, 39, 38, 40],
1694
+ [38, 40, 37, 39],
1427
1695
  function(keyCode) {
1428
- var direction = (keyCode === 37 || keyCode === 38) ? -1 : 1;
1429
- // Try to highlight next/prev legend item
1430
- if (!chart.highlightLegendItem(
1431
- chart.highlightedLegendItemIx + direction
1432
- )) {
1433
- return this.move(direction);
1696
+ chart[keyCode === 38 || keyCode === 40 ? 'yAxis' : 'xAxis'][0]
1697
+ .panStep(keyCode < 39 ? -1 : 1);
1698
+ }
1699
+ ],
1700
+
1701
+ // Tabs
1702
+ [
1703
+ [9],
1704
+ function(keyCode, e) {
1705
+ var button;
1706
+ // Deselect old
1707
+ chart.mapNavButtons[chart.focusedMapNavButtonIx].setState(0);
1708
+ if (
1709
+ e.shiftKey && !chart.focusedMapNavButtonIx ||
1710
+ !e.shiftKey && chart.focusedMapNavButtonIx
1711
+ ) { // trying to go somewhere we can't?
1712
+ chart.mapZoom(); // Reset zoom
1713
+ // Nowhere to go, go to prev/next module
1714
+ return this.move(e.shiftKey ? -1 : 1);
1434
1715
  }
1716
+ chart.focusedMapNavButtonIx += e.shiftKey ? -1 : 1;
1717
+ button = chart.mapNavButtons[chart.focusedMapNavButtonIx];
1718
+ chart.setFocusToElement(button.box, button);
1719
+ button.setState(2);
1435
1720
  }
1436
1721
  ],
1722
+
1437
1723
  // Enter/Spacebar
1438
1724
  [
1439
1725
  [13, 32],
1440
1726
  function() {
1441
1727
  fakeClickEvent(
1442
- chart.legend.allItems[
1443
- chart.highlightedLegendItemIx
1444
- ].legendItem.element.parentNode
1728
+ chart.mapNavButtons[chart.focusedMapNavButtonIx].element
1445
1729
  );
1446
1730
  }
1447
1731
  ]
1448
1732
  ], {
1449
- // Only run this module if we have at least one legend - wait for
1450
- // it - item. Don't run if the legend is populated by a colorAxis.
1451
- // Don't run if legend navigation is disabled.
1733
+ // Only run this module if we have map zoom on the chart
1452
1734
  validate: function() {
1453
- return chart.legend && chart.legend.allItems &&
1454
- !(chart.colorAxis && chart.colorAxis.length) &&
1455
- (chart.options.legend &&
1456
- chart.options.legend.keyboardNavigation &&
1457
- chart.options.legend.keyboardNavigation.enabled) !== false;
1735
+ return (
1736
+ chart.mapZoom &&
1737
+ chart.mapNavButtons &&
1738
+ chart.mapNavButtons.length === 2
1739
+ );
1458
1740
  },
1459
1741
 
1460
- // Make elements focusable and accessible
1742
+ // Make zoom buttons do their magic
1461
1743
  init: function(direction) {
1462
- each(chart.legend.allItems, function(item) {
1463
- item.legendGroup.element.setAttribute('tabindex', '-1');
1464
- item.legendGroup.element.setAttribute('role', 'button');
1465
- item.legendGroup.element.setAttribute(
1744
+ var zoomIn = chart.mapNavButtons[0],
1745
+ zoomOut = chart.mapNavButtons[1],
1746
+ initialButton = direction > 0 ? zoomIn : zoomOut;
1747
+
1748
+ each(chart.mapNavButtons, function(button, i) {
1749
+ button.element.setAttribute('tabindex', -1);
1750
+ button.element.setAttribute('role', 'button');
1751
+ button.element.setAttribute(
1466
1752
  'aria-label',
1467
- 'Toggle visibility of series ' + item.name
1753
+ 'Zoom ' + (i ? 'out ' : '') + 'chart'
1468
1754
  );
1469
1755
  });
1470
- // Focus first/last item
1471
- chart.highlightLegendItem(
1472
- direction > 0 ? 0 : chart.legend.allItems.length - 1
1473
- );
1474
- }
1475
- })
1476
- ];
1477
-
1478
- // Init nav module index. We start at the first module, and as the user
1479
- // navigates through the chart the index will increase to use different
1480
- // handler modules.
1481
- chart.keyboardNavigationModuleIndex = 0;
1482
-
1483
- // Make chart reachable by tab
1484
- if (
1485
- chart.container.hasAttribute &&
1486
- !chart.container.hasAttribute('tabIndex')
1487
- ) {
1488
- chart.container.setAttribute('tabindex', '0');
1489
- }
1490
-
1491
- // Add tab exit anchor
1492
- // We use this to move focus out of chart whenever we want, by setting focus
1493
- // to this and not preventing the default tab action.
1494
- if (!chart.tabExitAnchor) {
1495
- chart.tabExitAnchor = doc.createElement('div');
1496
- // Not reachable by user
1497
- chart.tabExitAnchor.setAttribute('tabindex', '-1');
1498
- merge(true, chart.tabExitAnchor.style, hiddenStyle);
1499
- chart.renderTo.appendChild(chart.tabExitAnchor);
1500
- }
1501
-
1502
- // Handle keyboard events
1503
- addEvent(chart.renderTo, 'keydown', keydownHandler);
1504
- addEvent(chart, 'destroy', function() {
1505
- if (chart.renderTo) {
1506
- removeEvent(chart.renderTo, 'keydown', keydownHandler);
1507
- }
1508
- });
1509
- };
1510
-
1511
- // Add screen reader region to chart.
1512
- // tableId is the HTML id of the table to focus when clicking the table anchor
1513
- // in the screen reader region.
1514
- H.Chart.prototype.addScreenReaderRegion = function(id, tableId) {
1515
- var chart = this,
1516
- series = chart.series,
1517
- options = chart.options,
1518
- a11yOptions = options.accessibility,
1519
- hiddenSection = chart.screenReaderRegion = doc.createElement('div'),
1520
- tableShortcut = doc.createElement('h4'),
1521
- tableShortcutAnchor = doc.createElement('a'),
1522
- chartHeading = doc.createElement('h4'),
1523
- chartTypes = chart.types || [],
1524
- // Build axis info - but not for pies and maps. Consider not adding for
1525
- // certain other types as well (funnel, pyramid?)
1526
- axesDesc = (
1527
- chartTypes.length === 1 && chartTypes[0] === 'pie' ||
1528
- chartTypes[0] === 'map'
1529
- ) && {} || chart.getAxesDescription(),
1530
- chartTypeInfo = series[0] && typeToSeriesMap[series[0].type] ||
1531
- typeToSeriesMap['default']; // eslint-disable-line dot-notation
1532
-
1533
- hiddenSection.setAttribute('id', id);
1534
- hiddenSection.setAttribute('role', 'region');
1535
- hiddenSection.setAttribute(
1536
- 'aria-label',
1537
- 'Chart screen reader information.'
1538
- );
1539
-
1540
- hiddenSection.innerHTML =
1541
- a11yOptions.screenReaderSectionFormatter &&
1542
- a11yOptions.screenReaderSectionFormatter(chart) ||
1543
- '<div>Use regions/landmarks to skip ahead to chart' +
1544
- (series.length > 1 ? ' and navigate between data series' : '') +
1545
- '.</div><h3>' +
1546
- (options.title.text ? htmlencode(options.title.text) : 'Chart') +
1547
- (
1548
- options.subtitle && options.subtitle.text ?
1549
- '. ' + htmlencode(options.subtitle.text) :
1550
- ''
1551
- ) +
1552
- '</h3><h4>Long description.</h4><div>' +
1553
- (options.chart.description || 'No description available.') +
1554
- '</div><h4>Structure.</h4><div>Chart type: ' +
1555
- (options.chart.typeDescription || chart.getTypeDescription()) +
1556
- '</div>' +
1557
- (
1558
- series.length === 1 ?
1559
- (
1560
- '<div>' + chartTypeInfo[0] + ' with ' +
1561
- series[0].points.length + ' ' +
1562
- (
1563
- series[0].points.length === 1 ?
1564
- chartTypeInfo[1] :
1565
- chartTypeInfo[2]
1566
- ) +
1567
- '.</div>'
1568
- ) : ''
1569
- ) +
1570
- (axesDesc.xAxis ? ('<div>' + axesDesc.xAxis + '</div>') : '') +
1571
- (axesDesc.yAxis ? ('<div>' + axesDesc.yAxis + '</div>') : '');
1572
-
1573
- // Add shortcut to data table if export-data is loaded
1574
- if (chart.getCSV) {
1575
- tableShortcutAnchor.innerHTML = 'View as data table.';
1576
- tableShortcutAnchor.href = '#' + tableId;
1577
- // Make this unreachable by user tabbing
1578
- tableShortcutAnchor.setAttribute('tabindex', '-1');
1579
- tableShortcutAnchor.onclick =
1580
- a11yOptions.onTableAnchorClick || function() {
1581
- chart.viewData();
1582
- doc.getElementById(tableId).focus();
1583
- };
1584
- tableShortcut.appendChild(tableShortcutAnchor);
1585
- hiddenSection.appendChild(tableShortcut);
1586
- }
1587
-
1588
- // Note: JAWS seems to refuse to read aria-label on the container, so add an
1589
- // h4 element as title for the chart.
1590
- chartHeading.innerHTML = 'Chart graphic.';
1591
- chart.renderTo.insertBefore(chartHeading, chart.renderTo.firstChild);
1592
- chart.renderTo.insertBefore(hiddenSection, chart.renderTo.firstChild);
1593
-
1594
- // Hide the section and the chart heading
1595
- merge(true, chartHeading.style, hiddenStyle);
1596
- merge(true, hiddenSection.style, hiddenStyle);
1597
- };
1598
-
1599
-
1600
- // Make chart container accessible, and wrap table functionality
1601
- H.Chart.prototype.callbacks.push(function(chart) {
1602
- var options = chart.options,
1603
- a11yOptions = options.accessibility;
1604
-
1605
- if (!a11yOptions.enabled) {
1606
- return;
1607
- }
1608
-
1609
- var titleElement = doc.createElementNS(
1610
- 'http://www.w3.org/2000/svg',
1611
- 'title'
1612
- ),
1613
- exportGroupElement = doc.createElementNS(
1614
- 'http://www.w3.org/2000/svg',
1615
- 'g'
1616
- ),
1617
- descElement = chart.container.getElementsByTagName('desc')[0],
1618
- textElements = chart.container.getElementsByTagName('text'),
1619
- titleId = 'highcharts-title-' + chart.index,
1620
- tableId = 'highcharts-data-table-' + chart.index,
1621
- hiddenSectionId = 'highcharts-information-region-' + chart.index,
1622
- chartTitle = options.title.text || 'Chart',
1623
- oldColumnHeaderFormatter = (
1624
- options.exporting &&
1625
- options.exporting.csv &&
1626
- options.exporting.csv.columnHeaderFormatter
1627
- ),
1628
- topLevelColumns = [];
1629
1756
 
1630
- // Add SVG title/desc tags
1631
- titleElement.textContent = htmlencode(chartTitle);
1632
- titleElement.id = titleId;
1633
- descElement.parentNode.insertBefore(titleElement, descElement);
1634
- chart.renderTo.setAttribute('role', 'region');
1635
- chart.renderTo.setAttribute(
1636
- 'aria-label',
1637
- 'Interactive chart. ' + chartTitle +
1638
- '. Use up and down arrows to navigate with most screen readers.'
1639
- );
1757
+ chart.setFocusToElement(initialButton.box, initialButton);
1758
+ initialButton.setState(2);
1759
+ chart.focusedMapNavButtonIx = direction > 0 ? 0 : 1;
1760
+ }
1761
+ }),
1640
1762
 
1641
- // Set screen reader properties on export menu
1642
- if (
1643
- chart.exportSVGElements &&
1644
- chart.exportSVGElements[0] &&
1645
- chart.exportSVGElements[0].element
1646
- ) {
1647
- var oldExportCallback = chart.exportSVGElements[0].element.onclick,
1648
- parent = chart.exportSVGElements[0].element.parentNode;
1649
- chart.exportSVGElements[0].element.onclick = function() {
1650
- oldExportCallback.apply(
1651
- this,
1652
- Array.prototype.slice.call(arguments)
1653
- );
1654
- chart.addAccessibleContextMenuAttribs();
1655
- chart.highlightExportItem(0);
1656
- };
1657
- chart.exportSVGElements[0].element.setAttribute('role', 'button');
1658
- chart.exportSVGElements[0].element.setAttribute(
1659
- 'aria-label',
1660
- 'View export menu'
1661
- );
1662
- exportGroupElement.appendChild(chart.exportSVGElements[0].element);
1663
- exportGroupElement.setAttribute('role', 'region');
1664
- exportGroupElement.setAttribute('aria-label', 'Chart export menu');
1665
- parent.appendChild(exportGroupElement);
1666
- }
1763
+ // Highstock range selector (minus input boxes)
1764
+ navModuleFactory('rangeSelector', [
1765
+ // Left/Right/Up/Down
1766
+ [
1767
+ [37, 39, 38, 40],
1768
+ function(keyCode) {
1769
+ var direction = (keyCode === 37 || keyCode === 38) ? -1 : 1;
1770
+ // Try to highlight next/prev button
1771
+ if (!chart.highlightRangeSelectorButton(
1772
+ chart.highlightedRangeSelectorItemIx + direction
1773
+ )) {
1774
+ return this.move(direction);
1775
+ }
1776
+ }
1777
+ ],
1778
+ // Enter/Spacebar
1779
+ [
1780
+ [13, 32],
1781
+ function() {
1782
+ // Don't allow click if button used to be disabled
1783
+ if (chart.oldRangeSelectorItemState !== 3) {
1784
+ fakeClickEvent(
1785
+ chart.rangeSelector.buttons[
1786
+ chart.highlightedRangeSelectorItemIx
1787
+ ].element
1788
+ );
1789
+ }
1790
+ }
1791
+ ]
1792
+ ], {
1793
+ // Only run this module if we have range selector
1794
+ validate: function() {
1795
+ return (
1796
+ chart.rangeSelector &&
1797
+ chart.rangeSelector.buttons &&
1798
+ chart.rangeSelector.buttons.length
1799
+ );
1800
+ },
1667
1801
 
1668
- // Set screen reader properties on input boxes for range selector. We need
1669
- // to do this regardless of whether or not these are visible, as they are
1670
- // by default part of the page's tabindex unless we set them to -1.
1671
- if (chart.rangeSelector) {
1672
- each(['minInput', 'maxInput'], function(key, i) {
1673
- if (chart.rangeSelector[key]) {
1674
- chart.rangeSelector[key].setAttribute('tabindex', '-1');
1675
- chart.rangeSelector[key].setAttribute('role', 'textbox');
1676
- chart.rangeSelector[key].setAttribute(
1677
- 'aria-label',
1678
- 'Select ' + (i ? 'end' : 'start') + ' date.'
1802
+ // Make elements focusable and accessible
1803
+ init: function(direction) {
1804
+ each(chart.rangeSelector.buttons, function(button) {
1805
+ button.element.setAttribute('tabindex', '-1');
1806
+ button.element.setAttribute('role', 'button');
1807
+ button.element.setAttribute(
1808
+ 'aria-label',
1809
+ 'Select range ' + (button.text && button.text.textStr)
1810
+ );
1811
+ });
1812
+ // Focus first/last button
1813
+ chart.highlightRangeSelectorButton(
1814
+ direction > 0 ? 0 : chart.rangeSelector.buttons.length - 1
1679
1815
  );
1680
1816
  }
1681
- });
1682
- }
1817
+ }),
1683
1818
 
1684
- // Hide text elements from screen readers
1685
- each(textElements, function(el) {
1686
- el.setAttribute('aria-hidden', 'true');
1687
- });
1819
+ // Highstock range selector, input boxes
1820
+ navModuleFactory('rangeSelectorInput', [
1821
+ // Tab/Up/Down
1822
+ [
1823
+ [9, 38, 40],
1824
+ function(keyCode, e) {
1825
+ var direction =
1826
+ (keyCode === 9 && e.shiftKey || keyCode === 38) ? -1 : 1,
1688
1827
 
1689
- // Add top-secret screen reader region
1690
- chart.addScreenReaderRegion(hiddenSectionId, tableId);
1828
+ newIx = chart.highlightedInputRangeIx =
1829
+ chart.highlightedInputRangeIx + direction;
1691
1830
 
1692
- // Enable keyboard navigation
1693
- if (a11yOptions.keyboardNavigation.enabled) {
1694
- chart.addKeyboardNavEvents();
1695
- }
1831
+ // Try to highlight next/prev item in list.
1832
+ if (newIx > 1 || newIx < 0) { // Out of range
1833
+ return this.move(direction);
1834
+ }
1835
+ chart.rangeSelector[newIx ? 'maxInput' : 'minInput'].focus();
1836
+ }
1837
+ ]
1838
+ ], {
1839
+ // Only run if we have range selector with input boxes
1840
+ validate: function() {
1841
+ var inputVisible = (
1842
+ chart.rangeSelector &&
1843
+ chart.rangeSelector.inputGroup &&
1844
+ chart.rangeSelector.inputGroup.element
1845
+ .getAttribute('visibility') !== 'hidden'
1846
+ );
1847
+ return (
1848
+ inputVisible &&
1849
+ chart.options.rangeSelector.inputEnabled !== false &&
1850
+ chart.rangeSelector.minInput &&
1851
+ chart.rangeSelector.maxInput
1852
+ );
1853
+ },
1696
1854
 
1697
- /* Wrap table functionality from export-data */
1855
+ // Highlight first/last input box
1856
+ init: function(direction) {
1857
+ chart.highlightedInputRangeIx = direction > 0 ? 0 : 1;
1858
+ chart.rangeSelector[
1859
+ chart.highlightedInputRangeIx ? 'maxInput' : 'minInput'
1860
+ ].focus();
1861
+ }
1862
+ }),
1698
1863
 
1699
- // Keep track of columns
1700
- merge(true, options.exporting, {
1701
- csv: {
1702
- columnHeaderFormatter: function(item, key, keyLength) {
1703
- if (!item) {
1704
- return 'Category';
1705
- }
1706
- if (item instanceof H.Axis) {
1707
- return (item.options.title && item.options.title.text) ||
1708
- (item.isDatetimeAxis ? 'DateTime' : 'Category');
1709
- }
1710
- var prevCol = topLevelColumns[topLevelColumns.length - 1];
1711
- if (keyLength > 1) {
1712
- // We need multiple levels of column headers
1713
- // Populate a list of column headers to add in addition to
1714
- // the ones added by export-data
1715
- if ((prevCol && prevCol.text) !== item.name) {
1716
- topLevelColumns.push({
1717
- text: item.name,
1718
- span: keyLength
1719
- });
1864
+ // Legend navigation
1865
+ navModuleFactory('legend', [
1866
+ // Left/Right/Up/Down
1867
+ [
1868
+ [37, 39, 38, 40],
1869
+ function(keyCode) {
1870
+ var direction = (keyCode === 37 || keyCode === 38) ? -1 : 1;
1871
+ // Try to highlight next/prev legend item
1872
+ if (!chart.highlightLegendItem(
1873
+ chart.highlightedLegendItemIx + direction
1874
+ )) {
1875
+ return this.move(direction);
1720
1876
  }
1721
1877
  }
1722
- if (oldColumnHeaderFormatter) {
1723
- return oldColumnHeaderFormatter.call(
1724
- this,
1725
- item,
1726
- key,
1727
- keyLength
1878
+ ],
1879
+ // Enter/Spacebar
1880
+ [
1881
+ [13, 32],
1882
+ function() {
1883
+ fakeClickEvent(
1884
+ chart.legend.allItems[
1885
+ chart.highlightedLegendItemIx
1886
+ ].legendItem.element.parentNode
1728
1887
  );
1729
1888
  }
1730
- return keyLength > 1 ? key : item.name;
1889
+ ]
1890
+ ], {
1891
+ // Only run this module if we have at least one legend - wait for
1892
+ // it - item. Don't run if the legend is populated by a colorAxis.
1893
+ // Don't run if legend navigation is disabled.
1894
+ validate: function() {
1895
+ return chart.legend && chart.legend.allItems &&
1896
+ chart.legend.display &&
1897
+ !(chart.colorAxis && chart.colorAxis.length) &&
1898
+ (chart.options.legend &&
1899
+ chart.options.legend.keyboardNavigation &&
1900
+ chart.options.legend.keyboardNavigation.enabled) !== false;
1901
+ },
1902
+
1903
+ // Make elements focusable and accessible
1904
+ init: function(direction) {
1905
+ each(chart.legend.allItems, function(item) {
1906
+ item.legendGroup.element.setAttribute('tabindex', '-1');
1907
+ item.legendGroup.element.setAttribute('role', 'button');
1908
+ item.legendGroup.element.setAttribute(
1909
+ 'aria-label',
1910
+ stripTags('Toggle visibility of series ' + item.name)
1911
+ );
1912
+ });
1913
+ // Focus first/last item
1914
+ chart.highlightLegendItem(
1915
+ direction > 0 ? 0 : chart.legend.allItems.length - 1
1916
+ );
1731
1917
  }
1732
- }
1733
- });
1918
+ })
1919
+ ];
1920
+ };
1734
1921
 
1735
- // Add ID and title/caption to table HTML
1736
- H.wrap(chart, 'getTable', function(proceed) {
1737
- return proceed.apply(this, Array.prototype.slice.call(arguments, 1))
1738
- .replace(
1739
- '<table>',
1740
- '<table id="' + tableId + '" summary="Table representation ' +
1741
- 'of chart"><caption>' + chartTitle + '</caption>'
1742
- );
1922
+
1923
+ // Add exit anchor to the chart
1924
+ // We use this to move focus out of chart whenever we want, by setting focus
1925
+ // to this div and not preventing the default tab action.
1926
+ // We also use this when users come back into the chart by tabbing back, in
1927
+ // order to navigate from the end of the chart.
1928
+ // Function returns the unbind function for the exit anchor's event handler.
1929
+ H.Chart.prototype.addExitAnchor = function() {
1930
+ var chart = this;
1931
+ chart.tabExitAnchor = doc.createElement('div');
1932
+ chart.tabExitAnchor.setAttribute('tabindex', '0');
1933
+
1934
+ // Hide exit anchor
1935
+ merge(true, chart.tabExitAnchor.style, {
1936
+ position: 'absolute',
1937
+ left: '-9999px',
1938
+ top: 'auto',
1939
+ width: '1px',
1940
+ height: '1px',
1941
+ overflow: 'hidden'
1743
1942
  });
1744
1943
 
1745
- // Add accessibility attributes and top level columns
1746
- H.wrap(chart, 'viewData', function(proceed) {
1747
- if (!this.dataTableDiv) {
1748
- proceed.apply(this, Array.prototype.slice.call(arguments, 1));
1944
+ chart.renderTo.appendChild(chart.tabExitAnchor);
1945
+ return addEvent(chart.tabExitAnchor, 'focus',
1946
+ function(ev) {
1947
+ var e = ev || win.event,
1948
+ curModule;
1749
1949
 
1750
- var table = doc.getElementById(tableId),
1751
- head = table.getElementsByTagName('thead')[0],
1752
- body = table.getElementsByTagName('tbody')[0],
1753
- firstRow = head.firstChild.children,
1754
- columnHeaderRow = '<tr><td></td>',
1755
- cell,
1756
- newCell;
1950
+ // If focusing and we are exiting, do nothing once.
1951
+ if (!chart.exiting) {
1757
1952
 
1758
- // Make table focusable by script
1759
- table.setAttribute('tabindex', '-1');
1953
+ // Not exiting, means we are coming in backwards
1954
+ chart.renderTo.focus();
1955
+ e.preventDefault();
1760
1956
 
1761
- // Create row headers
1762
- each(body.children, function(el) {
1763
- cell = el.firstChild;
1764
- newCell = doc.createElement('th');
1765
- newCell.setAttribute('scope', 'row');
1766
- newCell.innerHTML = cell.innerHTML;
1767
- cell.parentNode.replaceChild(newCell, cell);
1768
- });
1957
+ // Move to last valid keyboard nav module
1958
+ // Note the we don't run it, just set the index
1959
+ chart.keyboardNavigationModuleIndex =
1960
+ chart.keyboardNavigationModules.length - 1;
1961
+ curModule = chart.keyboardNavigationModules[
1962
+ chart.keyboardNavigationModuleIndex
1963
+ ];
1769
1964
 
1770
- // Set scope for column headers
1771
- each(firstRow, function(el) {
1772
- if (el.tagName === 'TH') {
1773
- el.setAttribute('scope', 'col');
1965
+ // Validate the module
1966
+ if (curModule.validate && !curModule.validate()) {
1967
+ // Invalid.
1968
+ // Move inits next valid module in direction
1969
+ curModule.move(-1);
1970
+ } else {
1971
+ // We have a valid module, init it
1972
+ curModule.init(-1);
1774
1973
  }
1775
- });
1776
1974
 
1777
- // Add top level columns
1778
- if (topLevelColumns.length) {
1779
- each(topLevelColumns, function(col) {
1780
- columnHeaderRow += '<th scope="col" colspan="' + col.span +
1781
- '">' + col.text + '</th>';
1782
- });
1783
- head.insertAdjacentHTML('afterbegin', columnHeaderRow);
1975
+ } else {
1976
+ // Don't skip the next focus, we only skip once.
1977
+ chart.exiting = false;
1784
1978
  }
1785
1979
  }
1786
- });
1980
+ );
1981
+ };
1982
+
1983
+
1984
+ // Add keyboard navigation events on chart load
1985
+ H.Chart.prototype.callbacks.push(function(chart) {
1986
+ var a11yOptions = chart.options.accessibility;
1987
+ if (a11yOptions.enabled && a11yOptions.keyboardNavigation.enabled) {
1988
+
1989
+ // Init nav modules. We start at the first module, and as the user
1990
+ // navigates through the chart the index will increase to use different
1991
+ // handler modules.
1992
+ chart.addKeyboardNavigationModules();
1993
+ chart.keyboardNavigationModuleIndex = 0;
1994
+
1995
+ // Make chart container reachable by tab
1996
+ if (
1997
+ chart.container.hasAttribute &&
1998
+ !chart.container.hasAttribute('tabIndex')
1999
+ ) {
2000
+ chart.container.setAttribute('tabindex', '0');
2001
+ }
2002
+
2003
+ // Add tab exit anchor
2004
+ if (!chart.tabExitAnchor) {
2005
+ chart.unbindExitAnchorFocus = chart.addExitAnchor();
2006
+ }
2007
+
2008
+ // Handle keyboard events by routing them to active keyboard nav module
2009
+ chart.unbindKeydownHandler = addEvent(chart.renderTo, 'keydown',
2010
+ function(ev) {
2011
+ var e = ev || win.event,
2012
+ curNavModule = chart.keyboardNavigationModules[
2013
+ chart.keyboardNavigationModuleIndex
2014
+ ];
2015
+ // If there is a nav module for the current index, run it.
2016
+ // Otherwise, we are outside of the chart in some direction.
2017
+ if (curNavModule) {
2018
+ if (curNavModule.run(e)) {
2019
+ // Successfully handled this key event, stop default
2020
+ e.preventDefault();
2021
+ }
2022
+ }
2023
+ });
2024
+
2025
+ // Add cleanup handlers
2026
+ addEvent(chart, 'destroy', function() {
2027
+ if (chart.unbindExitAnchorFocus && chart.tabExitAnchor) {
2028
+ chart.unbindExitAnchorFocus();
2029
+ }
2030
+ if (chart.unbindKeydownHandler && chart.renderTo) {
2031
+ chart.unbindKeydownHandler();
2032
+ }
2033
+ });
2034
+ }
1787
2035
  });
1788
2036
 
1789
2037
  }(Highcharts));