highcharts-rails 6.0.2 → 6.0.3

Sign up to get free protection for your applications and to get access to all the features.
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));