NGToolsCSharp/NGTools/Scripts/Highcharts-7.1.1/code/es-modules/modules/accessibility/accessibility.js

505 lines
14 KiB
JavaScript
Raw Normal View History

2024-09-13 08:44:13 +00:00
/* *
*
* (c) 2009-2019 Øystein Moseng
*
* Accessibility module for Highcharts
*
* License: www.highcharts.com/license
*
* */
'use strict';
import H from '../../parts/Globals.js';
import KeyboardNavigationHandler from './KeyboardNavigationHandler.js';
import AccessibilityComponent from './AccessibilityComponent.js';
import KeyboardNavigation from './KeyboardNavigation.js';
import LegendComponent from './components/LegendComponent.js';
import MenuComponent from './components/MenuComponent.js';
import SeriesComponent from './components/SeriesComponent.js';
import ZoomComponent from './components/ZoomComponent.js';
import RangeSelectorComponent from './components/RangeSelectorComponent.js';
import InfoRegionComponent from './components/InfoRegionComponent.js';
import ContainerComponent from './components/ContainerComponent.js';
import defaultOptions from './options.js';
import '../../modules/accessibility/a11y-i18n.js';
var addEvent = H.addEvent,
doc = H.win.document,
pick = H.pick,
merge = H.merge,
extend = H.extend,
error = H.error;
// Add default options
merge(true, H.defaultOptions, defaultOptions);
// Expose classes on Highcharts namespace
H.KeyboardNavigationHandler = KeyboardNavigationHandler;
H.AccessibilityComponent = AccessibilityComponent;
/*
* Add focus border functionality to SVGElements. Draws a new rect on top of
* element around its bounding box. This is used by multiple components.
*/
H.extend(H.SVGElement.prototype, {
/**
* @private
* @function Highcharts.SVGElement#addFocusBorder
*
* @param {number} margin
*
* @param {Higcharts.CSSObject} style
*/
addFocusBorder: function (margin, style) {
// Allow updating by just adding new border
if (this.focusBorder) {
this.removeFocusBorder();
}
// Add the border rect
var bb = this.getBBox(),
pad = pick(margin, 3);
bb.x += this.translateX ? this.translateX : 0;
bb.y += this.translateY ? this.translateY : 0;
this.focusBorder = this.renderer.rect(
bb.x - pad,
bb.y - pad,
bb.width + 2 * pad,
bb.height + 2 * pad,
style && style.borderRadius
)
.addClass('highcharts-focus-border')
.attr({
zIndex: 99
})
.add(this.parentGroup);
if (!this.renderer.styledMode) {
this.focusBorder.attr({
stroke: style && style.stroke,
'stroke-width': style && style.strokeWidth
});
}
},
/**
* @private
* @function Highcharts.SVGElement#removeFocusBorder
*/
removeFocusBorder: function () {
if (this.focusBorder) {
this.focusBorder.destroy();
delete this.focusBorder;
}
}
});
/**
* Set chart's focus to an SVGElement. Calls focus() on it, and draws the focus
* border. This is used by multiple components.
*
* @private
* @function Highcharts.Chart#setFocusToElement
*
* @param {Highcharts.SVGElement} svgElement
* Element to draw the border around.
*
* @param {SVGDOMElement|HTMLDOMElement} [focusElement]
* If supplied, it draws the border around svgElement and sets the focus
* to focusElement.
*/
H.Chart.prototype.setFocusToElement = function (svgElement, focusElement) {
var focusBorderOptions = this.options.accessibility
.keyboardNavigation.focusBorder,
browserFocusElement = focusElement || svgElement.element;
// Set browser focus if possible
if (
browserFocusElement &&
browserFocusElement.focus
) {
// If there is no focusin-listener, add one to work around Edge issue
// where Narrator is not reading out points despite calling focus().
if (!(
browserFocusElement.hcEvents &&
browserFocusElement.hcEvents.focusin
)) {
addEvent(browserFocusElement, 'focusin', function () {});
}
browserFocusElement.focus();
// Hide default focus ring
if (focusBorderOptions.hideBrowserFocusOutline) {
browserFocusElement.style.outline = 'none';
}
}
if (focusBorderOptions.enabled) {
// Remove old focus border
if (this.focusElement) {
this.focusElement.removeFocusBorder();
}
// Draw focus border (since some browsers don't do it automatically)
svgElement.addFocusBorder(focusBorderOptions.margin, {
stroke: focusBorderOptions.style.color,
strokeWidth: focusBorderOptions.style.lineWidth,
borderRadius: focusBorderOptions.style.borderRadius
});
this.focusElement = svgElement;
}
};
/**
* Get descriptive label for axis. This is used by multiple components.
*
* @private
* @function Highcharts.Axis#getDescription
*
* @return {string}
*/
H.Axis.prototype.getDescription = function () {
return (
this.userOptions && this.userOptions.accessibility &&
this.userOptions.accessibility.description ||
this.axisTitle && this.axisTitle.textStr ||
this.options.id ||
this.categories && 'categories' ||
this.isDatetimeAxis && 'Time' ||
'values'
);
};
/**
* The Accessibility class
*
* @private
* @requires module:modules/accessibility
*
* @class
* @name Highcharts.Accessibility
*
* @param {Highcharts.Chart} chart
* Chart object
*/
function Accessibility(chart) {
this.init(chart);
}
Accessibility.prototype = {
/**
* Initialize the accessibility class
* @private
* @param {Highcharts.Chart} chart
* Chart object
*/
init: function (chart) {
var a11yOptions = chart.options.accessibility;
this.chart = chart;
// Abort on old browsers
if (!doc.addEventListener || !chart.renderer.isSVG) {
chart.renderTo.setAttribute('aria-hidden', true);
return;
}
// Copy over any deprecated options that are used. We could do this on
// every update, but it is probably not needed.
this.copyDeprecatedOptions();
// Add the components
var components = this.components = {
container: new ContainerComponent(chart),
infoRegion: new InfoRegionComponent(chart),
legend: new LegendComponent(chart),
chartMenu: new MenuComponent(chart),
rangeSelector: new RangeSelectorComponent(chart),
series: new SeriesComponent(chart),
zoom: new ZoomComponent(chart)
};
if (a11yOptions.customComponents) {
extend(this.components, a11yOptions.customComponents);
}
this.keyboardNavigation = new KeyboardNavigation(chart, components);
this.update();
},
/**
* Update all components.
*/
update: function () {
var components = this.components,
a11yOptions = this.chart.options.accessibility;
// Update the chart type list as this is used by multiple modules
this.chart.types = this.getChartTypes();
// Update markup
Object.keys(components).forEach(function (componentName) {
components[componentName].onChartUpdate();
});
// Update keyboard navigation
this.keyboardNavigation.update(
a11yOptions.keyboardNavigation.order
);
},
/**
* Destroy all elements.
*/
destroy: function () {
var chart = this.chart || {};
// Destroy components
var components = this.components;
Object.keys(components).forEach(function (componentName) {
components[componentName].destroy();
});
// Kill keyboard nav
this.keyboardNavigation.destroy();
// Hide container from screen readers if it exists
if (chart.renderTo) {
chart.renderTo.setAttribute('aria-hidden', true);
}
// Remove focus border if it exists
if (chart.focusElement) {
chart.focusElement.removeFocusBorder();
}
},
/**
* Return a list of the types of series we have in the chart.
* @private
*/
getChartTypes: function () {
var types = {};
this.chart.series.forEach(function (series) {
types[series.type] = 1;
});
return Object.keys(types);
},
/**
* Copy options that are deprecated over to new options. Logs warnings to
* console for deprecated options used. The following options are
* deprecated:
*
* chart.description -> accessibility.description
* chart.typeDescription -> accessibility.typeDescription
* series.description -> series.accessibility.description
* series.exposeElementToA11y -> series.accessibility.exposeAsGroupOnly
* series.pointDescriptionFormatter ->
* series.accessibility.pointDescriptionFormatter
* series.skipKeyboardNavigation ->
* series.accessibility.keyboardNavigation.enabled
* point.description -> point.accessibility.description
* axis.description -> axis.accessibility.description
*
* @private
*/
copyDeprecatedOptions: function () {
var chart = this.chart,
// Warn user that a deprecated option was used
warn = function (oldOption, newOption) {
error(
'Highcharts: Deprecated option ' + oldOption +
' used. Use ' + newOption + ' instead.', false, chart
);
},
// Set a new option on a root prop, where the option is defined as
// an array of suboptions.
traverseSetOption = function (val, optionAsArray, root) {
var opt = root,
prop,
i = 0;
for (;i < optionAsArray.length - 1; ++i) {
prop = optionAsArray[i];
opt = opt[prop] = pick(opt[prop], {});
}
opt[optionAsArray[optionAsArray.length - 1]] = val;
},
// Map of deprecated series options. New options are defined as
// arrays of paths under series.options.
oldToNewSeriesOptions = {
description: ['accessibility', 'description'],
exposeElementToA11y: ['accessibility', 'exposeAsGroupOnly'],
pointDescriptionFormatter: [
'accessibility', 'pointDescriptionFormatter'
],
skipKeyboardNavigation: [
'accessibility', 'keyboardNavigation', 'enabled'
]
};
// Deal with chart wide options (description, typeDescription)
var chartOptions = chart.options.chart || {},
a11yOptions = chart.options.accessibility || {};
['description', 'typeDescription'].forEach(function (prop) {
if (chartOptions[prop]) {
a11yOptions[prop] = chartOptions[prop];
warn('chart.' + prop, 'accessibility.' + prop);
}
});
// Deal with axis description
chart.axes.forEach(function (axis) {
var opts = axis.options;
if (opts && opts.description) {
opts.accessibility = opts.accessibility || {};
opts.accessibility.description = opts.description;
warn('axis.description', 'axis.accessibility.description');
}
});
// Loop through all series and handle options
if (!chart.series) {
return;
}
chart.series.forEach(function (series) {
// Handle series wide options
Object.keys(oldToNewSeriesOptions).forEach(function (oldOption) {
var optionVal = series.options[oldOption];
if (optionVal !== undefined) {
// Set the new option
traverseSetOption(
// Note that skipKeyboardNavigation has inverted option
// value, since we set enabled rather than disabled
oldOption === 'skipKeyboardNavigation' ?
!optionVal : optionVal,
oldToNewSeriesOptions[oldOption],
series.options
);
warn(
'series.' + oldOption, 'series.' +
oldToNewSeriesOptions[oldOption].join('.')
);
}
});
// Loop through the points and handle point.description
if (series.points) {
series.points.forEach(function (point) {
if (point.options && point.options.description) {
point.options.accessibility =
point.options.accessibility || {};
point.options.accessibility.description =
point.options.description;
warn('point.description',
'point.accessibility.description');
}
});
}
});
}
};
// Handle updates to the module and send render updates to components
addEvent(H.Chart, 'render', function (e) {
var a11y = this.accessibility;
// Update/destroy
if (this.a11yDirty && this.renderTo) {
delete this.a11yDirty;
var accessibilityOptions = this.options.accessibility;
if (accessibilityOptions && accessibilityOptions.enabled) {
if (a11y) {
a11y.update();
} else {
this.accessibility = a11y = new Accessibility(this);
}
} else if (a11y) {
// Destroy if after update we have a11y and it is disabled
if (a11y.destroy) {
a11y.destroy();
}
delete this.accessibility;
} else {
// Just hide container
this.renderTo.setAttribute('aria-hidden', true);
}
}
// Update markup regardless
if (a11y) {
Object.keys(a11y.components).forEach(function (componentName) {
a11y.components[componentName].onChartRender(e);
});
}
});
// Update with chart/series/point updates
addEvent(H.Chart, 'update', function (e) {
// Merge new options
var newOptions = e.options.accessibility;
if (newOptions) {
// Handle custom component updating specifically
if (newOptions.customComponents) {
this.options.accessibility.customComponents =
newOptions.customComponents;
delete newOptions.customComponents;
}
merge(true, this.options.accessibility, newOptions);
// Recreate from scratch
if (this.accessibility && this.accessibility.destroy) {
this.accessibility.destroy();
delete this.accessibility;
}
}
// Mark dirty for update
this.a11yDirty = true;
});
// Mark dirty for update
addEvent(H.Point, 'update', function () {
if (this.series.chart.accessibility) {
this.series.chart.a11yDirty = true;
}
});
['addSeries', 'init'].forEach(function (event) {
addEvent(H.Chart, event, function () {
this.a11yDirty = true;
});
});
['update', 'updatedData', 'remove'].forEach(function (event) {
addEvent(H.Series, event, function () {
if (this.chart.accessibility) {
this.chart.a11yDirty = true;
}
});
});
// Direct updates (events happen after render)
[
'afterDrilldown', 'drillupall'
].forEach(function (event) {
addEvent(H.Chart, event, function () {
if (this.accessibility) {
this.accessibility.update();
}
});
});
// Destroy with chart
addEvent(H.Chart, 'destroy', function () {
if (this.accessibility) {
this.accessibility.destroy();
}
});