357 lines
12 KiB
JavaScript
357 lines
12 KiB
JavaScript
|
/* *
|
||
|
*
|
||
|
* (c) 2009-2020 Øystein Moseng
|
||
|
*
|
||
|
* Accessibility component for exporting menu.
|
||
|
*
|
||
|
* License: www.highcharts.com/license
|
||
|
*
|
||
|
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
|
||
|
*
|
||
|
* */
|
||
|
'use strict';
|
||
|
import H from '../../../parts/Globals.js';
|
||
|
import U from '../../../parts/Utilities.js';
|
||
|
var extend = U.extend;
|
||
|
import AccessibilityComponent from '../AccessibilityComponent.js';
|
||
|
import KeyboardNavigationHandler from '../KeyboardNavigationHandler.js';
|
||
|
import ChartUtilities from '../utils/chartUtilities.js';
|
||
|
var unhideChartElementFromAT = ChartUtilities.unhideChartElementFromAT;
|
||
|
import HTMLUtilities from '../utils/htmlUtilities.js';
|
||
|
var removeElement = HTMLUtilities.removeElement, getFakeMouseEvent = HTMLUtilities.getFakeMouseEvent;
|
||
|
/* eslint-disable no-invalid-this, valid-jsdoc */
|
||
|
/**
|
||
|
* Get the wrapped export button element of a chart.
|
||
|
*
|
||
|
* @private
|
||
|
* @param {Highcharts.Chart} chart
|
||
|
* @returns {Highcharts.SVGElement}
|
||
|
*/
|
||
|
function getExportMenuButtonElement(chart) {
|
||
|
return chart.exportSVGElements && chart.exportSVGElements[0];
|
||
|
}
|
||
|
/**
|
||
|
* Show the export menu and focus the first item (if exists).
|
||
|
*
|
||
|
* @private
|
||
|
* @function Highcharts.Chart#showExportMenu
|
||
|
*/
|
||
|
H.Chart.prototype.showExportMenu = function () {
|
||
|
var exportButton = getExportMenuButtonElement(this);
|
||
|
if (exportButton) {
|
||
|
var el = exportButton.element;
|
||
|
if (el.onclick) {
|
||
|
el.onclick(getFakeMouseEvent('click'));
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
/**
|
||
|
* @private
|
||
|
* @function Highcharts.Chart#hideExportMenu
|
||
|
*/
|
||
|
H.Chart.prototype.hideExportMenu = function () {
|
||
|
var chart = this, exportList = chart.exportDivElements;
|
||
|
if (exportList && chart.exportContextMenu) {
|
||
|
// Reset hover states etc.
|
||
|
exportList.forEach(function (el) {
|
||
|
if (el.className === 'highcharts-menu-item' && el.onmouseout) {
|
||
|
el.onmouseout(getFakeMouseEvent('mouseout'));
|
||
|
}
|
||
|
});
|
||
|
chart.highlightedExportItemIx = 0;
|
||
|
// Hide the menu div
|
||
|
chart.exportContextMenu.hideMenu();
|
||
|
// Make sure the chart has focus and can capture keyboard events
|
||
|
chart.container.focus();
|
||
|
}
|
||
|
};
|
||
|
/**
|
||
|
* Highlight export menu item by index.
|
||
|
*
|
||
|
* @private
|
||
|
* @function Highcharts.Chart#highlightExportItem
|
||
|
*
|
||
|
* @param {number} ix
|
||
|
*
|
||
|
* @return {boolean}
|
||
|
*/
|
||
|
H.Chart.prototype.highlightExportItem = function (ix) {
|
||
|
var listItem = this.exportDivElements && this.exportDivElements[ix], curHighlighted = this.exportDivElements &&
|
||
|
this.exportDivElements[this.highlightedExportItemIx], hasSVGFocusSupport;
|
||
|
if (listItem &&
|
||
|
listItem.tagName === 'LI' &&
|
||
|
!(listItem.children && listItem.children.length)) {
|
||
|
// Test if we have focus support for SVG elements
|
||
|
hasSVGFocusSupport = !!(this.renderTo.getElementsByTagName('g')[0] || {}).focus;
|
||
|
// Only focus if we can set focus back to the elements after
|
||
|
// destroying the menu (#7422)
|
||
|
if (listItem.focus && hasSVGFocusSupport) {
|
||
|
listItem.focus();
|
||
|
}
|
||
|
if (curHighlighted && curHighlighted.onmouseout) {
|
||
|
curHighlighted.onmouseout(getFakeMouseEvent('mouseout'));
|
||
|
}
|
||
|
if (listItem.onmouseover) {
|
||
|
listItem.onmouseover(getFakeMouseEvent('mouseover'));
|
||
|
}
|
||
|
this.highlightedExportItemIx = ix;
|
||
|
return true;
|
||
|
}
|
||
|
return false;
|
||
|
};
|
||
|
/**
|
||
|
* Try to highlight the last valid export menu item.
|
||
|
*
|
||
|
* @private
|
||
|
* @function Highcharts.Chart#highlightLastExportItem
|
||
|
* @return {boolean}
|
||
|
*/
|
||
|
H.Chart.prototype.highlightLastExportItem = function () {
|
||
|
var chart = this, i;
|
||
|
if (chart.exportDivElements) {
|
||
|
i = chart.exportDivElements.length;
|
||
|
while (i--) {
|
||
|
if (chart.highlightExportItem(i)) {
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return false;
|
||
|
};
|
||
|
/**
|
||
|
* @private
|
||
|
* @param {Highcharts.Chart} chart
|
||
|
*/
|
||
|
function exportingShouldHaveA11y(chart) {
|
||
|
var exportingOpts = chart.options.exporting, exportButton = getExportMenuButtonElement(chart);
|
||
|
return !!(exportingOpts &&
|
||
|
exportingOpts.enabled !== false &&
|
||
|
exportingOpts.accessibility &&
|
||
|
exportingOpts.accessibility.enabled &&
|
||
|
exportButton &&
|
||
|
exportButton.element);
|
||
|
}
|
||
|
/**
|
||
|
* The MenuComponent class
|
||
|
*
|
||
|
* @private
|
||
|
* @class
|
||
|
* @name Highcharts.MenuComponent
|
||
|
*/
|
||
|
var MenuComponent = function () { };
|
||
|
MenuComponent.prototype = new AccessibilityComponent();
|
||
|
extend(MenuComponent.prototype, /** @lends Highcharts.MenuComponent */ {
|
||
|
/**
|
||
|
* Init the component
|
||
|
*/
|
||
|
init: function () {
|
||
|
var chart = this.chart, component = this;
|
||
|
this.addEvent(chart, 'exportMenuShown', function () {
|
||
|
component.onMenuShown();
|
||
|
});
|
||
|
this.addEvent(chart, 'exportMenuHidden', function () {
|
||
|
component.onMenuHidden();
|
||
|
});
|
||
|
},
|
||
|
/**
|
||
|
* @private
|
||
|
*/
|
||
|
onMenuHidden: function () {
|
||
|
var menu = this.chart.exportContextMenu;
|
||
|
if (menu) {
|
||
|
menu.setAttribute('aria-hidden', 'true');
|
||
|
}
|
||
|
this.isExportMenuShown = false;
|
||
|
this.setExportButtonExpandedState('false');
|
||
|
},
|
||
|
/**
|
||
|
* @private
|
||
|
*/
|
||
|
onMenuShown: function () {
|
||
|
var chart = this.chart, menu = chart.exportContextMenu;
|
||
|
if (menu) {
|
||
|
this.addAccessibleContextMenuAttribs();
|
||
|
unhideChartElementFromAT(chart, menu);
|
||
|
}
|
||
|
this.isExportMenuShown = true;
|
||
|
this.setExportButtonExpandedState('true');
|
||
|
},
|
||
|
/**
|
||
|
* @private
|
||
|
* @param {string} stateStr
|
||
|
*/
|
||
|
setExportButtonExpandedState: function (stateStr) {
|
||
|
var button = this.exportButtonProxy;
|
||
|
if (button) {
|
||
|
button.setAttribute('aria-expanded', stateStr);
|
||
|
}
|
||
|
},
|
||
|
/**
|
||
|
* Called on each render of the chart. We need to update positioning of the
|
||
|
* proxy overlay.
|
||
|
*/
|
||
|
onChartRender: function () {
|
||
|
var chart = this.chart, a11yOptions = chart.options.accessibility;
|
||
|
// Always start with a clean slate
|
||
|
removeElement(this.exportProxyGroup);
|
||
|
// Set screen reader properties on export menu
|
||
|
if (exportingShouldHaveA11y(chart)) {
|
||
|
// Proxy button and group
|
||
|
this.exportProxyGroup = this.addProxyGroup(
|
||
|
// Wrap in a region div if verbosity is high
|
||
|
a11yOptions.landmarkVerbosity === 'all' ? {
|
||
|
'aria-label': chart.langFormat('accessibility.exporting.exportRegionLabel', { chart: chart }),
|
||
|
'role': 'region'
|
||
|
} : {});
|
||
|
var button = getExportMenuButtonElement(this.chart);
|
||
|
this.exportButtonProxy = this.createProxyButton(button, this.exportProxyGroup, {
|
||
|
'aria-label': chart.langFormat('accessibility.exporting.menuButtonLabel', { chart: chart }),
|
||
|
'aria-expanded': 'false'
|
||
|
});
|
||
|
}
|
||
|
},
|
||
|
/**
|
||
|
* @private
|
||
|
*/
|
||
|
addAccessibleContextMenuAttribs: function () {
|
||
|
var chart = this.chart, exportList = chart.exportDivElements;
|
||
|
if (exportList && exportList.length) {
|
||
|
// Set tabindex on the menu items to allow focusing by script
|
||
|
// Set role to give screen readers a chance to pick up the contents
|
||
|
exportList.forEach(function (item) {
|
||
|
if (item.tagName === 'LI' &&
|
||
|
!(item.children && item.children.length)) {
|
||
|
item.setAttribute('tabindex', -1);
|
||
|
}
|
||
|
else {
|
||
|
item.setAttribute('aria-hidden', 'true');
|
||
|
}
|
||
|
});
|
||
|
// Set accessibility properties on parent div
|
||
|
var parentDiv = exportList[0].parentNode;
|
||
|
parentDiv.removeAttribute('aria-hidden');
|
||
|
parentDiv.setAttribute('aria-label', chart.langFormat('accessibility.exporting.chartMenuLabel', { chart: chart }));
|
||
|
}
|
||
|
},
|
||
|
/**
|
||
|
* Get keyboard navigation handler for this component.
|
||
|
* @return {Highcharts.KeyboardNavigationHandler}
|
||
|
*/
|
||
|
getKeyboardNavigation: function () {
|
||
|
var keys = this.keyCodes, chart = this.chart, component = this;
|
||
|
return new KeyboardNavigationHandler(chart, {
|
||
|
keyCodeMap: [
|
||
|
// Arrow prev handler
|
||
|
[
|
||
|
[keys.left, keys.up],
|
||
|
function () {
|
||
|
return component.onKbdPrevious(this);
|
||
|
}
|
||
|
],
|
||
|
// Arrow next handler
|
||
|
[
|
||
|
[keys.right, keys.down],
|
||
|
function () {
|
||
|
return component.onKbdNext(this);
|
||
|
}
|
||
|
],
|
||
|
// Click handler
|
||
|
[
|
||
|
[keys.enter, keys.space],
|
||
|
function () {
|
||
|
return component.onKbdClick(this);
|
||
|
}
|
||
|
],
|
||
|
// ESC handler
|
||
|
[
|
||
|
[keys.esc],
|
||
|
function () {
|
||
|
return this.response.prev;
|
||
|
}
|
||
|
]
|
||
|
],
|
||
|
// Only run exporting navigation if exporting support exists and is
|
||
|
// enabled on chart
|
||
|
validate: function () {
|
||
|
return chart.exportChart &&
|
||
|
chart.options.exporting.enabled !== false &&
|
||
|
chart.options.exporting.accessibility.enabled !==
|
||
|
false;
|
||
|
},
|
||
|
// Focus export menu button
|
||
|
init: function () {
|
||
|
var exportBtn = component.exportButtonProxy, exportGroup = chart.exportingGroup;
|
||
|
if (exportGroup && exportBtn) {
|
||
|
chart.setFocusToElement(exportGroup, exportBtn);
|
||
|
}
|
||
|
},
|
||
|
// Hide the menu
|
||
|
terminate: function () {
|
||
|
chart.hideExportMenu();
|
||
|
}
|
||
|
});
|
||
|
},
|
||
|
/**
|
||
|
* @private
|
||
|
* @param {Highcharts.KeyboardNavigationHandler} keyboardNavigationHandler
|
||
|
* @return {number}
|
||
|
* Response code
|
||
|
*/
|
||
|
onKbdPrevious: function (keyboardNavigationHandler) {
|
||
|
var chart = this.chart, a11yOptions = chart.options.accessibility, response = keyboardNavigationHandler.response, i = chart.highlightedExportItemIx || 0;
|
||
|
// Try to highlight prev item in list. Highlighting e.g.
|
||
|
// separators will fail.
|
||
|
while (i--) {
|
||
|
if (chart.highlightExportItem(i)) {
|
||
|
return response.success;
|
||
|
}
|
||
|
}
|
||
|
// We failed, so wrap around or move to prev module
|
||
|
if (a11yOptions.keyboardNavigation.wrapAround) {
|
||
|
chart.highlightLastExportItem();
|
||
|
return response.success;
|
||
|
}
|
||
|
return response.prev;
|
||
|
},
|
||
|
/**
|
||
|
* @private
|
||
|
* @param {Highcharts.KeyboardNavigationHandler} keyboardNavigationHandler
|
||
|
* @return {number}
|
||
|
* Response code
|
||
|
*/
|
||
|
onKbdNext: function (keyboardNavigationHandler) {
|
||
|
var chart = this.chart, a11yOptions = chart.options.accessibility, response = keyboardNavigationHandler.response, i = (chart.highlightedExportItemIx || 0) + 1;
|
||
|
// Try to highlight next item in list. Highlighting e.g.
|
||
|
// separators will fail.
|
||
|
for (; i < chart.exportDivElements.length; ++i) {
|
||
|
if (chart.highlightExportItem(i)) {
|
||
|
return response.success;
|
||
|
}
|
||
|
}
|
||
|
// We failed, so wrap around or move to next module
|
||
|
if (a11yOptions.keyboardNavigation.wrapAround) {
|
||
|
chart.highlightExportItem(0);
|
||
|
return response.success;
|
||
|
}
|
||
|
return response.next;
|
||
|
},
|
||
|
/**
|
||
|
* @private
|
||
|
* @param {Highcharts.KeyboardNavigationHandler} keyboardNavigationHandler
|
||
|
* @return {number}
|
||
|
* Response code
|
||
|
*/
|
||
|
onKbdClick: function (keyboardNavigationHandler) {
|
||
|
var chart = this.chart, curHighlightedItem = chart.exportDivElements[chart.highlightedExportItemIx], exportButtonElement = getExportMenuButtonElement(chart).element;
|
||
|
if (this.isExportMenuShown) {
|
||
|
this.fakeClickEvent(curHighlightedItem);
|
||
|
}
|
||
|
else {
|
||
|
this.fakeClickEvent(exportButtonElement);
|
||
|
chart.highlightExportItem(0);
|
||
|
}
|
||
|
return keyboardNavigationHandler.response.success;
|
||
|
}
|
||
|
});
|
||
|
export default MenuComponent;
|