highcharts
Version:
JavaScript charting framework
298 lines (297 loc) • 10.8 kB
JavaScript
/* *
*
* (c) 2009-2025 Øystein Moseng
*
* Extend SVG and Chart classes with focus border capabilities.
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import U from '../Core/Utilities.js';
const { addEvent, pick } = U;
/* *
*
* Composition
*
* */
var FocusBorderComposition;
(function (FocusBorderComposition) {
/* *
*
* Declarations
*
* */
/* *
*
* Constants
*
* */
// Attributes that trigger a focus border update
const svgElementBorderUpdateTriggers = [
'x', 'y', 'transform', 'width', 'height', 'r', 'd', 'stroke-width'
];
/* *
*
* Functions
*
* */
/**
* @private
*/
function compose(ChartClass, SVGElementClass) {
const chartProto = ChartClass.prototype, svgElementProto = SVGElementClass.prototype;
if (!chartProto.renderFocusBorder) {
chartProto.renderFocusBorder = chartRenderFocusBorder;
chartProto.setFocusToElement = chartSetFocusToElement;
}
if (!svgElementProto.addFocusBorder) {
svgElementProto.addFocusBorder = svgElementAddFocusBorder;
svgElementProto.removeFocusBorder = svgElementRemoveFocusBorder;
}
}
FocusBorderComposition.compose = compose;
/**
* Redraws the focus border on the currently focused element.
*
* @private
* @function Highcharts.Chart#renderFocusBorder
*/
function chartRenderFocusBorder() {
const focusElement = this.focusElement, focusBorderOptions = this.options.accessibility.keyboardNavigation.focusBorder;
if (focusElement) {
focusElement.removeFocusBorder();
if (focusBorderOptions.enabled) {
focusElement.addFocusBorder(focusBorderOptions.margin, {
stroke: focusBorderOptions.style.color,
strokeWidth: focusBorderOptions.style.lineWidth,
r: focusBorderOptions.style.borderRadius
});
}
}
}
/**
* 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.
*/
function chartSetFocusToElement(svgElement, focusElement) {
const 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
// 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 (this.focusElement) {
this.focusElement.removeFocusBorder();
}
this.focusElement = svgElement;
this.renderFocusBorder();
}
/**
* Add hook to destroy focus border if SVG element is destroyed, unless
* hook already exists.
* @private
* @param el Element to add destroy hook to
*/
function svgElementAddDestroyFocusBorderHook(el) {
if (el.focusBorderDestroyHook) {
return;
}
const origDestroy = el.destroy;
el.destroy = function () {
if (el.focusBorder && el.focusBorder.destroy) {
el.focusBorder.destroy();
}
return origDestroy.apply(el, arguments);
};
el.focusBorderDestroyHook = origDestroy;
}
/**
* Add focus border functionality to SVGElements. Draws a new rect on top of
* element around its bounding box. This is used by multiple components.
*
* @private
* @function Highcharts.SVGElement#addFocusBorder
*
* @param {number} margin
*
* @param {SVGAttributes} attribs
*/
function svgElementAddFocusBorder(margin, attribs) {
// Allow updating by just adding new border
if (this.focusBorder) {
this.removeFocusBorder();
}
// Add the border rect
const bb = this.getBBox(), pad = pick(margin, 3), parent = this.parentGroup, scaleX = this.scaleX || parent && parent.scaleX, scaleY = this.scaleY || parent && parent.scaleY, oneDefined = scaleX ? !scaleY : scaleY, scaleBoth = oneDefined ? Math.abs(scaleX || scaleY || 1) :
(Math.abs(scaleX || 1) + Math.abs(scaleY || 1)) / 2, lineHeight = this.renderer.fontMetrics(this).h;
bb.x += this.translateX ? this.translateX : 0;
bb.y += this.translateY ? this.translateY : 0;
let borderPosX = bb.x - pad, borderPosY = bb.y - pad, borderWidth = bb.width + 2 * pad, borderHeight = bb.height + 2 * pad;
/**
* For text elements, apply x and y offset, #11397.
* @private
*/
function getTextAnchorCorrection(text) {
let posXCorrection = 0, posYCorrection = 0;
if (text.attr('text-anchor') === 'middle') {
posXCorrection = posYCorrection = 0.5;
}
else if (!text.rotation) {
posYCorrection = 0.75;
}
else {
posXCorrection = 0.25;
}
return {
x: posXCorrection,
y: posYCorrection
};
}
const isLabel = !!this.text;
if (this.element.nodeName === 'text' || isLabel) {
const isRotated = !!this.rotation;
const correction = !isLabel ? getTextAnchorCorrection(this) :
{
x: isRotated ? 1 : 0,
y: 0
};
const attrX = +this.attr('x');
const attrY = +this.attr('y');
if (!isNaN(attrX)) {
borderPosX = attrX - (bb.width * correction.x) - pad;
}
if (!isNaN(attrY)) {
// Correct by line height if "text-achor" == "start", #19335.
const dim = this.attr('text-anchor') === 'start' ?
lineHeight :
bb.height;
borderPosY = attrY - (dim * correction.y) - pad;
}
if (isLabel && isRotated) {
const temp = borderWidth;
borderWidth = borderHeight;
borderHeight = temp;
if (!isNaN(attrX)) {
borderPosX = attrX - (bb.height * correction.x) - pad;
}
if (!isNaN(attrY)) {
borderPosY = attrY - (bb.width * correction.y) - pad;
}
}
}
this.focusBorder = this.renderer.rect(borderPosX, borderPosY, borderWidth, borderHeight, parseInt((attribs && attribs.r || 0).toString(), 10) / scaleBoth)
.addClass('highcharts-focus-border')
.attr({
zIndex: 99
})
.add(parent);
if (!this.renderer.styledMode) {
this.focusBorder.attr({
stroke: attribs && attribs.stroke,
'stroke-width': (attribs && attribs.strokeWidth || 0) / scaleBoth
});
}
avgElementAddUpdateFocusBorderHooks(this, margin, attribs);
svgElementAddDestroyFocusBorderHook(this);
}
/**
* Add hooks to update the focus border of an element when the element
* size/position is updated, unless already added.
* @private
* @param el Element to add update hooks to
* @param updateParams Parameters to pass through to addFocusBorder when updating.
*/
function avgElementAddUpdateFocusBorderHooks(el, ...updateParams) {
if (el.focusBorderUpdateHooks) {
return;
}
el.focusBorderUpdateHooks = {};
svgElementBorderUpdateTriggers.forEach((trigger) => {
const setterKey = trigger + 'Setter';
const origSetter = el[setterKey] || el._defaultSetter;
el.focusBorderUpdateHooks[setterKey] = origSetter;
el[setterKey] = function () {
const ret = origSetter.apply(el, arguments);
el.addFocusBorder.apply(el, updateParams);
return ret;
};
});
}
/**
* Remove hook from SVG element added by addDestroyFocusBorderHook, if
* existing.
* @private
* @param el Element to remove destroy hook from
*/
function svgElementRemoveDestroyFocusBorderHook(el) {
if (!el.focusBorderDestroyHook) {
return;
}
el.destroy = el.focusBorderDestroyHook;
delete el.focusBorderDestroyHook;
}
/**
* Add focus border functionality to SVGElements. Draws a new rect on top of
* element around its bounding box. This is used by multiple components.
* @private
* @function Highcharts.SVGElement#removeFocusBorder
*/
function svgElementRemoveFocusBorder() {
svgElementRemoveUpdateFocusBorderHooks(this);
svgElementRemoveDestroyFocusBorderHook(this);
if (this.focusBorder) {
this.focusBorder.destroy();
delete this.focusBorder;
}
}
/**
* Remove hooks from SVG element added by addUpdateFocusBorderHooks, if
* existing.
* @private
* @param el Element to remove update hooks from
*/
function svgElementRemoveUpdateFocusBorderHooks(el) {
if (!el.focusBorderUpdateHooks) {
return;
}
Object.keys(el.focusBorderUpdateHooks).forEach((setterKey) => {
const origSetter = el.focusBorderUpdateHooks[setterKey];
if (origSetter === el._defaultSetter) {
delete el[setterKey];
}
else {
el[setterKey] = origSetter;
}
});
delete el.focusBorderUpdateHooks;
}
})(FocusBorderComposition || (FocusBorderComposition = {}));
/* *
*
* Default Export
*
* */
export default FocusBorderComposition;