UNPKG

highcharts

Version:
428 lines (427 loc) 13.8 kB
/* * * * (c) 2009-2025 Øystein Moseng * * Accessibility component for exporting menu. * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ 'use strict'; import U from '../../Core/Utilities.js'; const { attr } = U; import AccessibilityComponent from '../AccessibilityComponent.js'; import KeyboardNavigationHandler from '../KeyboardNavigationHandler.js'; import ChartUtilities from '../Utils/ChartUtilities.js'; const { getChartTitle, unhideChartElementFromAT } = ChartUtilities; import HTMLUtilities from '../Utils/HTMLUtilities.js'; const { getFakeMouseEvent } = HTMLUtilities; /* * * * Functions * * */ /** * Get the wrapped export button element of a chart. * @private */ function getExportMenuButtonElement(chart) { return chart.exportSVGElements && chart.exportSVGElements[0]; } /** * @private */ function exportingShouldHaveA11y(chart) { const 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 */ class MenuComponent extends AccessibilityComponent { /* * * * Functions * * */ /* eslint-disable valid-jsdoc */ /** * Init the component */ init() { const chart = this.chart, component = this; this.addEvent(chart, 'exportMenuShown', function () { component.onMenuShown(); }); this.addEvent(chart, 'exportMenuHidden', function () { component.onMenuHidden(); }); this.createProxyGroup(); } /** * @private */ onMenuHidden() { const menu = this.chart.exportContextMenu; if (menu) { menu.setAttribute('aria-hidden', 'true'); } this.setExportButtonExpandedState('false'); } /** * @private */ onMenuShown() { const chart = this.chart, menu = chart.exportContextMenu; if (menu) { this.addAccessibleContextMenuAttribs(); unhideChartElementFromAT(chart, menu); } this.setExportButtonExpandedState('true'); } /** * @private * @param {string} stateStr */ setExportButtonExpandedState(stateStr) { if (this.exportButtonProxy) { this.exportButtonProxy.innerElement.setAttribute('aria-expanded', stateStr); } } /** * Called on each render of the chart. We need to update positioning of the * proxy overlay. */ onChartRender() { const 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.innerElement); } else if (a11y) { a11y.keyboardNavigation.tabindexContainer.focus(); } } } /** * @private */ proxyMenuButton() { const chart = this.chart; const proxyProvider = this.proxyProvider; const buttonEl = getExportMenuButtonElement(chart); if (exportingShouldHaveA11y(chart) && buttonEl) { this.exportButtonProxy = proxyProvider.addProxyElement('chartMenu', { click: buttonEl }, 'button', { 'aria-label': chart.langFormat('accessibility.exporting.menuButtonLabel', { chart: chart, chartTitle: getChartTitle(chart) }), 'aria-expanded': false, title: chart.options.lang.contextButtonTitle || null }); } } /** * @private */ createProxyGroup() { const chart = this.chart; if (chart && this.proxyProvider) { this.proxyProvider.addGroup('chartMenu'); } } /** * @private */ addAccessibleContextMenuAttribs() { const 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((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 const parentDiv = (exportList[0] && exportList[0].parentNode); if (parentDiv) { attr(parentDiv, { 'aria-hidden': void 0, 'aria-label': chart.langFormat('accessibility.exporting.chartMenuLabel', { chart }), role: 'list' // Needed for webkit/VO }); } } } /** * Get keyboard navigation handler for this component. * @private */ getKeyboardNavigation() { const 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 ?.buttons ?.contextButton.enabled !== false && chart.options.exporting.enabled !== false && chart.options.exporting.accessibility.enabled !== false; }, // Focus export menu button init: function () { const proxy = component.exportButtonProxy; const svgEl = component.chart.exportingGroup; if (proxy && svgEl) { chart.setFocusToElement(svgEl, proxy.innerElement); } }, // Hide the menu terminate: function () { chart.hideExportMenu(); } }); } /** * @private * @param {Highcharts.KeyboardNavigationHandler} keyboardNavigationHandler * @return {number} Response code */ onKbdPrevious(keyboardNavigationHandler) { const chart = this.chart; const a11yOptions = chart.options.accessibility; const response = keyboardNavigationHandler.response; // Try to highlight prev item in list. Highlighting e.g. // separators will fail. let 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 */ onKbdNext(keyboardNavigationHandler) { const chart = this.chart; const a11yOptions = chart.options.accessibility; const response = keyboardNavigationHandler.response; // Try to highlight next item in list. Highlighting e.g. // separators will fail. for (let 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 */ onKbdClick(keyboardNavigationHandler) { const chart = this.chart; const curHighlightedItem = chart.exportDivElements[chart.highlightedExportItemIx]; const exportButtonElement = getExportMenuButtonElement(chart).element; if (chart.openMenu) { this.fakeClickEvent(curHighlightedItem); } else { this.fakeClickEvent(exportButtonElement); chart.highlightExportItem(0); } return keyboardNavigationHandler.response.success; } } /* * * * Class Namespace * * */ (function (MenuComponent) { /* * * * Declarations * * */ /* * * * Functions * * */ /** * @private */ function compose(ChartClass) { const chartProto = ChartClass.prototype; if (!chartProto.hideExportMenu) { 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() { const exportButton = getExportMenuButtonElement(this); if (exportButton) { const el = exportButton.element; if (el.onclick) { el.onclick(getFakeMouseEvent('click')); } } } /** * @private * @function Highcharts.Chart#hideExportMenu */ function chartHideExportMenu() { const chart = this, exportList = chart.exportDivElements; if (exportList && chart.exportContextMenu && chart.openMenu) { // Reset hover states etc. exportList.forEach((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) { const listItem = this.exportDivElements && this.exportDivElements[ix]; const 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 const 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() { const chart = this; if (chart.exportDivElements) { let i = chart.exportDivElements.length; while (i--) { if (chart.highlightExportItem(i)) { return true; } } } return false; } })(MenuComponent || (MenuComponent = {})); /* * * * Default Export * * */ export default MenuComponent;