FocusBorder.js 8.9 KB

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