FocusBorder.js 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. /* *
  2. *
  3. * (c) 2009-2021 Øystein Moseng
  4. *
  5. * Extend SVG and Chart classes with focus border capabilities.
  6. *
  7. * License: www.highcharts.com/license
  8. *
  9. * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
  10. *
  11. * */
  12. 'use strict';
  13. import H from '../Core/Globals.js';
  14. import SVGElement from '../Core/Renderer/SVG/SVGElement.js';
  15. import SVGLabel from '../Core/Renderer/SVG/SVGLabel.js';
  16. import U from '../Core/Utilities.js';
  17. var addEvent = U.addEvent, extend = U.extend, pick = U.pick;
  18. /* eslint-disable no-invalid-this, valid-jsdoc */
  19. // Attributes that trigger a focus border update
  20. var svgElementBorderUpdateTriggers = [
  21. 'x', 'y', 'transform', 'width', 'height', 'r', 'd', 'stroke-width'
  22. ];
  23. /**
  24. * Add hook to destroy focus border if SVG element is destroyed, unless
  25. * hook already exists.
  26. * @private
  27. * @param el Element to add destroy hook to
  28. */
  29. function addDestroyFocusBorderHook(el) {
  30. if (el.focusBorderDestroyHook) {
  31. return;
  32. }
  33. var origDestroy = el.destroy;
  34. el.destroy = function () {
  35. var _a, _b;
  36. (_b = (_a = el.focusBorder) === null || _a === void 0 ? void 0 : _a.destroy) === null || _b === void 0 ? void 0 : _b.call(_a);
  37. return origDestroy.apply(el, arguments);
  38. };
  39. el.focusBorderDestroyHook = origDestroy;
  40. }
  41. /**
  42. * Remove hook from SVG element added by addDestroyFocusBorderHook, if
  43. * existing.
  44. * @private
  45. * @param el Element to remove destroy hook from
  46. */
  47. function removeDestroyFocusBorderHook(el) {
  48. if (!el.focusBorderDestroyHook) {
  49. return;
  50. }
  51. el.destroy = el.focusBorderDestroyHook;
  52. delete el.focusBorderDestroyHook;
  53. }
  54. /**
  55. * Add hooks to update the focus border of an element when the element
  56. * size/position is updated, unless already added.
  57. * @private
  58. * @param el Element to add update hooks to
  59. * @param updateParams Parameters to pass through to addFocusBorder when updating.
  60. */
  61. function addUpdateFocusBorderHooks(el) {
  62. var updateParams = [];
  63. for (var _i = 1; _i < arguments.length; _i++) {
  64. updateParams[_i - 1] = arguments[_i];
  65. }
  66. if (el.focusBorderUpdateHooks) {
  67. return;
  68. }
  69. el.focusBorderUpdateHooks = {};
  70. svgElementBorderUpdateTriggers.forEach(function (trigger) {
  71. var setterKey = trigger + 'Setter';
  72. var origSetter = el[setterKey] || el._defaultSetter;
  73. el.focusBorderUpdateHooks[setterKey] = origSetter;
  74. el[setterKey] = function () {
  75. var ret = origSetter.apply(el, arguments);
  76. el.addFocusBorder.apply(el, updateParams);
  77. return ret;
  78. };
  79. });
  80. }
  81. /**
  82. * Remove hooks from SVG element added by addUpdateFocusBorderHooks, if
  83. * existing.
  84. * @private
  85. * @param el Element to remove update hooks from
  86. */
  87. function removeUpdateFocusBorderHooks(el) {
  88. if (!el.focusBorderUpdateHooks) {
  89. return;
  90. }
  91. Object.keys(el.focusBorderUpdateHooks).forEach(function (setterKey) {
  92. var origSetter = el.focusBorderUpdateHooks[setterKey];
  93. if (origSetter === el._defaultSetter) {
  94. delete el[setterKey];
  95. }
  96. else {
  97. el[setterKey] = origSetter;
  98. }
  99. });
  100. delete el.focusBorderUpdateHooks;
  101. }
  102. /*
  103. * Add focus border functionality to SVGElements. Draws a new rect on top of
  104. * element around its bounding box. This is used by multiple components.
  105. */
  106. extend(SVGElement.prototype, {
  107. /**
  108. * @private
  109. * @function Highcharts.SVGElement#addFocusBorder
  110. *
  111. * @param {number} margin
  112. *
  113. * @param {Highcharts.CSSObject} style
  114. */
  115. addFocusBorder: function (margin, style) {
  116. // Allow updating by just adding new border
  117. if (this.focusBorder) {
  118. this.removeFocusBorder();
  119. }
  120. // Add the border rect
  121. var bb = this.getBBox(), pad = pick(margin, 3);
  122. bb.x += this.translateX ? this.translateX : 0;
  123. bb.y += this.translateY ? this.translateY : 0;
  124. var borderPosX = bb.x - pad, borderPosY = bb.y - pad, borderWidth = bb.width + 2 * pad, borderHeight = bb.height + 2 * pad;
  125. // For text elements, apply x and y offset, #11397.
  126. /**
  127. * @private
  128. * @function
  129. *
  130. * @param {Highcharts.SVGElement} text
  131. *
  132. * @return {TextAnchorCorrectionObject}
  133. */
  134. function getTextAnchorCorrection(text) {
  135. var posXCorrection = 0, posYCorrection = 0;
  136. if (text.attr('text-anchor') === 'middle') {
  137. posXCorrection = H.isFirefox && text.rotation ? 0.25 : 0.5;
  138. posYCorrection = H.isFirefox && !text.rotation ? 0.75 : 0.5;
  139. }
  140. else if (!text.rotation) {
  141. posYCorrection = 0.75;
  142. }
  143. else {
  144. posXCorrection = 0.25;
  145. }
  146. return {
  147. x: posXCorrection,
  148. y: posYCorrection
  149. };
  150. }
  151. var isLabel = this instanceof SVGLabel;
  152. if (this.element.nodeName === 'text' || isLabel) {
  153. var isRotated = !!this.rotation;
  154. var correction = !isLabel ? getTextAnchorCorrection(this) :
  155. {
  156. x: isRotated ? 1 : 0,
  157. y: 0
  158. };
  159. var attrX = +this.attr('x');
  160. var attrY = +this.attr('y');
  161. if (!isNaN(attrX)) {
  162. borderPosX = attrX - (bb.width * correction.x) - pad;
  163. }
  164. if (!isNaN(attrY)) {
  165. borderPosY = attrY - (bb.height * correction.y) - pad;
  166. }
  167. if (isLabel && isRotated) {
  168. var temp = borderWidth;
  169. borderWidth = borderHeight;
  170. borderHeight = temp;
  171. if (!isNaN(attrX)) {
  172. borderPosX = attrX - (bb.height * correction.x) - pad;
  173. }
  174. if (!isNaN(attrY)) {
  175. borderPosY = attrY - (bb.width * correction.y) - pad;
  176. }
  177. }
  178. }
  179. this.focusBorder = this.renderer.rect(borderPosX, borderPosY, borderWidth, borderHeight, parseInt((style && style.borderRadius || 0).toString(), 10))
  180. .addClass('highcharts-focus-border')
  181. .attr({
  182. zIndex: 99
  183. })
  184. .add(this.parentGroup);
  185. if (!this.renderer.styledMode) {
  186. this.focusBorder.attr({
  187. stroke: style && style.stroke,
  188. 'stroke-width': style && style.strokeWidth
  189. });
  190. }
  191. addUpdateFocusBorderHooks(this, margin, style);
  192. addDestroyFocusBorderHook(this);
  193. },
  194. /**
  195. * @private
  196. * @function Highcharts.SVGElement#removeFocusBorder
  197. */
  198. removeFocusBorder: function () {
  199. removeUpdateFocusBorderHooks(this);
  200. removeDestroyFocusBorderHook(this);
  201. if (this.focusBorder) {
  202. this.focusBorder.destroy();
  203. delete this.focusBorder;
  204. }
  205. }
  206. });
  207. /**
  208. * Redraws the focus border on the currently focused element.
  209. *
  210. * @private
  211. * @function Highcharts.Chart#renderFocusBorder
  212. */
  213. H.Chart.prototype.renderFocusBorder = function () {
  214. var focusElement = this.focusElement, focusBorderOptions = this.options.accessibility.keyboardNavigation.focusBorder;
  215. if (focusElement) {
  216. focusElement.removeFocusBorder();
  217. if (focusBorderOptions.enabled) {
  218. focusElement.addFocusBorder(focusBorderOptions.margin, {
  219. stroke: focusBorderOptions.style.color,
  220. strokeWidth: focusBorderOptions.style.lineWidth,
  221. borderRadius: focusBorderOptions.style.borderRadius
  222. });
  223. }
  224. }
  225. };
  226. /**
  227. * Set chart's focus to an SVGElement. Calls focus() on it, and draws the focus
  228. * border. This is used by multiple components.
  229. *
  230. * @private
  231. * @function Highcharts.Chart#setFocusToElement
  232. *
  233. * @param {Highcharts.SVGElement} svgElement
  234. * Element to draw the border around.
  235. *
  236. * @param {SVGDOMElement|HTMLDOMElement} [focusElement]
  237. * If supplied, it draws the border around svgElement and sets the focus
  238. * to focusElement.
  239. */
  240. H.Chart.prototype.setFocusToElement = function (svgElement, focusElement) {
  241. var focusBorderOptions = this.options.accessibility.keyboardNavigation.focusBorder, browserFocusElement = focusElement || svgElement.element;
  242. // Set browser focus if possible
  243. if (browserFocusElement &&
  244. browserFocusElement.focus) {
  245. // If there is no focusin-listener, add one to work around Edge issue
  246. // where Narrator is not reading out points despite calling focus().
  247. if (!(browserFocusElement.hcEvents &&
  248. browserFocusElement.hcEvents.focusin)) {
  249. addEvent(browserFocusElement, 'focusin', function () { });
  250. }
  251. browserFocusElement.focus();
  252. // Hide default focus ring
  253. if (focusBorderOptions.hideBrowserFocusOutline) {
  254. browserFocusElement.style.outline = 'none';
  255. }
  256. }
  257. if (this.focusElement) {
  258. this.focusElement.removeFocusBorder();
  259. }
  260. this.focusElement = svgElement;
  261. this.renderFocusBorder();
  262. };