/* * * * (c) 2009-2021 Øystein Moseng * * Accessibility component for exporting menu. * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ 'use strict'; var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { if (typeof b !== "function" && b !== null) throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); import Chart from '../../Core/Chart/Chart.js'; import U from '../../Core/Utilities.js'; var attr = U.attr; import AccessibilityComponent from '../AccessibilityComponent.js'; import KeyboardNavigationHandler from '../KeyboardNavigationHandler.js'; import ChartUtilities from '../Utils/ChartUtilities.js'; var getChartTitle = ChartUtilities.getChartTitle, unhideChartElementFromAT = ChartUtilities.unhideChartElementFromAT; import HTMLUtilities from '../Utils/HTMLUtilities.js'; var getFakeMouseEvent = HTMLUtilities.getFakeMouseEvent; /* * * * Functions * * */ /* eslint-disable valid-jsdoc */ /** * Get the wrapped export button element of a chart. * @private */ function getExportMenuButtonElement(chart) { return chart.exportSVGElements && chart.exportSVGElements[0]; } /** * @private */ function exportingShouldHaveA11y(chart) { var exportingOpts = chart.options.exporting, exportButton = getExportMenuButtonElement(chart); return !!(exportingOpts && exportingOpts.enabled !== false && exportingOpts.accessibility && exportingOpts.accessibility.enabled && exportButton && exportButton.element); } /* * * * Class * * */ /** * The MenuComponent class * * @private * @class * @name Highcharts.MenuComponent */ var MenuComponent = /** @class */ (function (_super) { __extends(MenuComponent, _super); function MenuComponent() { return _super !== null && _super.apply(this, arguments) || this; } /* * * * Functions * * */ /* eslint-disable valid-jsdoc */ /** * Init the component */ MenuComponent.prototype.init = function () { var chart = this.chart, component = this; this.addEvent(chart, 'exportMenuShown', function () { component.onMenuShown(); }); this.addEvent(chart, 'exportMenuHidden', function () { component.onMenuHidden(); }); this.createProxyGroup(); }; /** * @private */ MenuComponent.prototype.onMenuHidden = function () { var menu = this.chart.exportContextMenu; if (menu) { menu.setAttribute('aria-hidden', 'true'); } this.setExportButtonExpandedState('false'); }; /** * @private */ MenuComponent.prototype.onMenuShown = function () { var chart = this.chart, menu = chart.exportContextMenu; if (menu) { this.addAccessibleContextMenuAttribs(); unhideChartElementFromAT(chart, menu); } this.setExportButtonExpandedState('true'); }; /** * @private * @param {string} stateStr */ MenuComponent.prototype.setExportButtonExpandedState = function (stateStr) { if (this.exportButtonProxy) { this.exportButtonProxy.buttonElement.setAttribute('aria-expanded', stateStr); } }; /** * Called on each render of the chart. We need to update positioning of the * proxy overlay. */ MenuComponent.prototype.onChartRender = function () { var chart = this.chart, focusEl = chart.focusElement, a11y = chart.accessibility; this.proxyProvider.clearGroup('chartMenu'); this.proxyMenuButton(); if (this.exportButtonProxy && focusEl && focusEl === chart.exportingGroup) { if (focusEl.focusBorder) { chart.setFocusToElement(focusEl, this.exportButtonProxy.buttonElement); } else if (a11y) { a11y.keyboardNavigation.tabindexContainer.focus(); } } }; /** * @private */ MenuComponent.prototype.proxyMenuButton = function () { var chart = this.chart; var proxyProvider = this.proxyProvider; var buttonEl = getExportMenuButtonElement(chart); if (exportingShouldHaveA11y(chart) && buttonEl) { this.exportButtonProxy = proxyProvider.addProxyElement('chartMenu', { click: buttonEl }, { 'aria-label': chart.langFormat('accessibility.exporting.menuButtonLabel', { chart: chart, chartTitle: getChartTitle(chart) }), 'aria-expanded': false, title: chart.options.lang.contextButtonTitle || null }); } }; /** * @private */ MenuComponent.prototype.createProxyGroup = function () { var chart = this.chart; if (chart && this.proxyProvider) { this.proxyProvider.addGroup('chartMenu', 'div'); } }; /** * @private */ MenuComponent.prototype.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) { 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] && exportList[0].parentNode); if (parentDiv) { attr(parentDiv, { 'aria-hidden': void 0, 'aria-label': chart.langFormat('accessibility.exporting.chartMenuLabel', { chart: chart }), role: 'list' // Needed for webkit/VO }); } } }; /** * Get keyboard navigation handler for this component. * @private */ MenuComponent.prototype.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); } ] ], // Only run exporting navigation if exporting support exists and is // enabled on chart validate: function () { return !!chart.exporting && chart.options.exporting.enabled !== false && chart.options.exporting.accessibility.enabled !== false; }, // Focus export menu button init: function () { var proxy = component.exportButtonProxy; var svgEl = component.chart.exportingGroup; if (proxy && svgEl) { chart.setFocusToElement(svgEl, proxy.buttonElement); } }, // Hide the menu terminate: function () { chart.hideExportMenu(); } }); }; /** * @private * @param {Highcharts.KeyboardNavigationHandler} keyboardNavigationHandler * @return {number} Response code */ MenuComponent.prototype.onKbdPrevious = function (keyboardNavigationHandler) { var chart = this.chart; var a11yOptions = chart.options.accessibility; var response = keyboardNavigationHandler.response; // Try to highlight prev item in list. Highlighting e.g. // separators will fail. var i = chart.highlightedExportItemIx || 0; 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 */ MenuComponent.prototype.onKbdNext = function (keyboardNavigationHandler) { var chart = this.chart; var a11yOptions = chart.options.accessibility; var response = keyboardNavigationHandler.response; // Try to highlight next item in list. Highlighting e.g. // separators will fail. for (var i = (chart.highlightedExportItemIx || 0) + 1; 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 */ MenuComponent.prototype.onKbdClick = function (keyboardNavigationHandler) { var chart = this.chart; var curHighlightedItem = chart.exportDivElements[chart.highlightedExportItemIx]; var exportButtonElement = getExportMenuButtonElement(chart).element; if (chart.openMenu) { this.fakeClickEvent(curHighlightedItem); } else { this.fakeClickEvent(exportButtonElement); chart.highlightExportItem(0); } return keyboardNavigationHandler.response.success; }; return MenuComponent; }(AccessibilityComponent)); /* * * * Class Namespace * * */ (function (MenuComponent) { /* * * * Declarations * * */ /* * * * Constants * * */ var composedClasses = []; /* * * * Functions * * */ /* eslint-disable valid-jsdoc */ /** * @private */ function compose(ChartClass) { if (composedClasses.indexOf(ChartClass) === -1) { composedClasses.push(ChartClass); var chartProto = Chart.prototype; chartProto.hideExportMenu = chartHideExportMenu; chartProto.highlightExportItem = chartHighlightExportItem; chartProto.highlightLastExportItem = chartHighlightLastExportItem; chartProto.showExportMenu = chartShowExportMenu; } } MenuComponent.compose = compose; /** * Show the export menu and focus the first item (if exists). * * @private * @function Highcharts.Chart#showExportMenu */ function chartShowExportMenu() { var exportButton = getExportMenuButtonElement(this); if (exportButton) { var el = exportButton.element; if (el.onclick) { el.onclick(getFakeMouseEvent('click')); } } } /** * @private * @function Highcharts.Chart#hideExportMenu */ function chartHideExportMenu() { var chart = this, exportList = chart.exportDivElements; if (exportList && chart.exportContextMenu && chart.openMenu) { // Reset hover states etc. exportList.forEach(function (el) { if (el && 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 */ function chartHighlightExportItem(ix) { var listItem = this.exportDivElements && this.exportDivElements[ix]; var curHighlighted = this.exportDivElements && this.exportDivElements[this.highlightedExportItemIx]; if (listItem && listItem.tagName === 'LI' && !(listItem.children && listItem.children.length)) { // Test if we have focus support for SVG elements var 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 */ function chartHighlightLastExportItem() { var chart = this; if (chart.exportDivElements) { var i = chart.exportDivElements.length; while (i--) { if (chart.highlightExportItem(i)) { return true; } } } return false; } })(MenuComponent || (MenuComponent = {})); /* * * * Default Export * * */ export default MenuComponent;