249 lines
8.5 KiB
JavaScript
249 lines
8.5 KiB
JavaScript
|
/* *
|
||
|
*
|
||
|
* (c) 2009-2020 Ø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 H from '../../parts/Globals.js';
|
||
|
import U from '../../parts/Utilities.js';
|
||
|
var addEvent = U.addEvent, extend = U.extend, pick = U.pick;
|
||
|
/* eslint-disable no-invalid-this, valid-jsdoc */
|
||
|
// Attributes that trigger a focus border update
|
||
|
var svgElementBorderUpdateTriggers = [
|
||
|
'x', 'y', 'transform', 'width', 'height', 'r', 'd', 'stroke-width'
|
||
|
];
|
||
|
/**
|
||
|
* Add hook to destroy focus border if SVG element is destroyed, unless
|
||
|
* hook already exists.
|
||
|
*
|
||
|
* @param el Element to add destroy hook to
|
||
|
*/
|
||
|
function addDestroyFocusBorderHook(el) {
|
||
|
if (el.focusBorderDestroyHook) {
|
||
|
return;
|
||
|
}
|
||
|
var origDestroy = el.destroy;
|
||
|
el.destroy = function () {
|
||
|
var _a, _b;
|
||
|
(_b = (_a = el.focusBorder) === null || _a === void 0 ? void 0 : _a.destroy) === null || _b === void 0 ? void 0 : _b.call(_a);
|
||
|
return origDestroy.apply(el, arguments);
|
||
|
};
|
||
|
el.focusBorderDestroyHook = origDestroy;
|
||
|
}
|
||
|
/**
|
||
|
* Remove hook from SVG element added by addDestroyFocusBorderHook, if
|
||
|
* existing.
|
||
|
*
|
||
|
* @param el Element to remove destroy hook from
|
||
|
*/
|
||
|
function removeDestroyFocusBorderHook(el) {
|
||
|
if (!el.focusBorderDestroyHook) {
|
||
|
return;
|
||
|
}
|
||
|
el.destroy = el.focusBorderDestroyHook;
|
||
|
delete el.focusBorderDestroyHook;
|
||
|
}
|
||
|
/**
|
||
|
* Add hooks to update the focus border of an element when the element
|
||
|
* size/position is updated, unless already added.
|
||
|
*
|
||
|
* @param el Element to add update hooks to
|
||
|
* @param updateParams Parameters to pass through to addFocusBorder when updating.
|
||
|
*/
|
||
|
function addUpdateFocusBorderHooks(el) {
|
||
|
var updateParams = [];
|
||
|
for (var _i = 1; _i < arguments.length; _i++) {
|
||
|
updateParams[_i - 1] = arguments[_i];
|
||
|
}
|
||
|
if (el.focusBorderUpdateHooks) {
|
||
|
return;
|
||
|
}
|
||
|
el.focusBorderUpdateHooks = {};
|
||
|
svgElementBorderUpdateTriggers.forEach(function (trigger) {
|
||
|
var setterKey = trigger + 'Setter';
|
||
|
var origSetter = el[setterKey] || el._defaultSetter;
|
||
|
el.focusBorderUpdateHooks[setterKey] = origSetter;
|
||
|
el[setterKey] = function () {
|
||
|
var ret = origSetter.apply(el, arguments);
|
||
|
el.addFocusBorder.apply(el, updateParams);
|
||
|
return ret;
|
||
|
};
|
||
|
});
|
||
|
}
|
||
|
/**
|
||
|
* Remove hooks from SVG element added by addUpdateFocusBorderHooks, if
|
||
|
* existing.
|
||
|
*
|
||
|
* @param el Element to remove update hooks from
|
||
|
*/
|
||
|
function removeUpdateFocusBorderHooks(el) {
|
||
|
if (!el.focusBorderUpdateHooks) {
|
||
|
return;
|
||
|
}
|
||
|
Object.keys(el.focusBorderUpdateHooks).forEach(function (setterKey) {
|
||
|
var origSetter = el.focusBorderUpdateHooks[setterKey];
|
||
|
if (origSetter === el._defaultSetter) {
|
||
|
delete el[setterKey];
|
||
|
}
|
||
|
else {
|
||
|
el[setterKey] = origSetter;
|
||
|
}
|
||
|
});
|
||
|
delete el.focusBorderUpdateHooks;
|
||
|
}
|
||
|
/*
|
||
|
* Add focus border functionality to SVGElements. Draws a new rect on top of
|
||
|
* element around its bounding box. This is used by multiple components.
|
||
|
*/
|
||
|
extend(H.SVGElement.prototype, {
|
||
|
/**
|
||
|
* @private
|
||
|
* @function Highcharts.SVGElement#addFocusBorder
|
||
|
*
|
||
|
* @param {number} margin
|
||
|
*
|
||
|
* @param {Highcharts.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;
|
||
|
var 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
|
||
|
*
|
||
|
* @param {Highcharts.SVGElement} text
|
||
|
*
|
||
|
* @return {TextAnchorCorrectionObject}
|
||
|
*/
|
||
|
function getTextAnchorCorrection(text) {
|
||
|
var posXCorrection = 0, posYCorrection = 0;
|
||
|
if (text.attr('text-anchor') === 'middle') {
|
||
|
posXCorrection = H.isFirefox && text.rotation ? 0.25 : 0.5;
|
||
|
posYCorrection = H.isFirefox && !text.rotation ? 0.75 : 0.5;
|
||
|
}
|
||
|
else if (!text.rotation) {
|
||
|
posYCorrection = 0.75;
|
||
|
}
|
||
|
else {
|
||
|
posXCorrection = 0.25;
|
||
|
}
|
||
|
return {
|
||
|
x: posXCorrection,
|
||
|
y: posYCorrection
|
||
|
};
|
||
|
}
|
||
|
if (this.element.nodeName === 'text' || this.isLabel) {
|
||
|
var isRotated = !!this.rotation, correction = !this.isLabel ? getTextAnchorCorrection(this) :
|
||
|
{
|
||
|
x: isRotated ? 1 : 0,
|
||
|
y: 0
|
||
|
};
|
||
|
borderPosX = +this.attr('x') - (bb.width * correction.x) - pad;
|
||
|
borderPosY = +this.attr('y') - (bb.height * correction.y) - pad;
|
||
|
if (this.isLabel && isRotated) {
|
||
|
var temp = borderWidth;
|
||
|
borderWidth = borderHeight;
|
||
|
borderHeight = temp;
|
||
|
borderPosX = +this.attr('x') - (bb.height * correction.x) - pad;
|
||
|
borderPosY = +this.attr('y') - (bb.width * correction.y) - pad;
|
||
|
}
|
||
|
}
|
||
|
this.focusBorder = this.renderer.rect(borderPosX, borderPosY, borderWidth, borderHeight, parseInt((style && style.borderRadius || 0).toString(), 10))
|
||
|
.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
|
||
|
});
|
||
|
}
|
||
|
addUpdateFocusBorderHooks(this, margin, style);
|
||
|
addDestroyFocusBorderHook(this);
|
||
|
},
|
||
|
/**
|
||
|
* @private
|
||
|
* @function Highcharts.SVGElement#removeFocusBorder
|
||
|
*/
|
||
|
removeFocusBorder: function () {
|
||
|
removeUpdateFocusBorderHooks(this);
|
||
|
removeDestroyFocusBorderHook(this);
|
||
|
if (this.focusBorder) {
|
||
|
this.focusBorder.destroy();
|
||
|
delete this.focusBorder;
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
/**
|
||
|
* Redraws the focus border on the currently focused element.
|
||
|
*
|
||
|
* @private
|
||
|
* @function Highcharts.Chart#renderFocusBorder
|
||
|
*/
|
||
|
H.Chart.prototype.renderFocusBorder = function () {
|
||
|
var 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,
|
||
|
borderRadius: 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.
|
||
|
*/
|
||
|
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 (this.focusElement) {
|
||
|
this.focusElement.removeFocusBorder();
|
||
|
}
|
||
|
this.focusElement = svgElement;
|
||
|
this.renderFocusBorder();
|
||
|
};
|