KeyboardNavigation.js 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. /**
  2. * KeyboardNavigation.js
  3. *
  4. * Copyright, Moxiecode Systems AB
  5. * Released under LGPL License.
  6. *
  7. * License: http://www.tinymce.com/license
  8. * Contributing: http://www.tinymce.com/contributing
  9. */
  10. /**
  11. * This class handles keyboard navigation of controls and elements.
  12. *
  13. * @class tinymce.ui.KeyboardNavigation
  14. */
  15. define("tinymce/ui/KeyboardNavigation", [
  16. ], function() {
  17. "use strict";
  18. /**
  19. * This class handles all keyboard navigation for WAI-ARIA support. Each root container
  20. * gets an instance of this class.
  21. *
  22. * @constructor
  23. */
  24. return function(settings) {
  25. var root = settings.root, focusedElement, focusedControl;
  26. focusedElement = document.activeElement;
  27. focusedControl = root.getParentCtrl(focusedElement);
  28. /**
  29. * Returns the currently focused elements wai aria role of the currently
  30. * focused element or specified element.
  31. *
  32. * @private
  33. * @param {Element} elm Optional element to get role from.
  34. * @return {String} Role of specified element.
  35. */
  36. function getRole(elm) {
  37. elm = elm || focusedElement;
  38. return elm && elm.getAttribute('role');
  39. }
  40. /**
  41. * Returns the wai role of the parent element of the currently
  42. * focused element or specified element.
  43. *
  44. * @private
  45. * @param {Element} elm Optional element to get parent role from.
  46. * @return {String} Role of the first parent that has a role.
  47. */
  48. function getParentRole(elm) {
  49. var role, parent = elm || focusedElement;
  50. while ((parent = parent.parentNode)) {
  51. if ((role = getRole(parent))) {
  52. return role;
  53. }
  54. }
  55. }
  56. /**
  57. * Returns a wai aria property by name for example aria-selected.
  58. *
  59. * @private
  60. * @param {String} name Name of the aria property to get for example "disabled".
  61. * @return {String} Aria property value.
  62. */
  63. function getAriaProp(name) {
  64. var elm = focusedElement;
  65. if (elm) {
  66. return elm.getAttribute('aria-' + name);
  67. }
  68. }
  69. /**
  70. * Is the element a text input element or not.
  71. *
  72. * @private
  73. * @param {Element} elm Element to check if it's an text input element or not.
  74. * @return {Boolean} True/false if the element is a text element or not.
  75. */
  76. function isTextInputElement(elm) {
  77. var tagName = elm.tagName.toUpperCase();
  78. // Notice: since type can be "email" etc we don't check the type
  79. // So all input elements gets treated as text input elements
  80. return tagName == "INPUT" || tagName == "TEXTAREA";
  81. }
  82. /**
  83. * Returns true/false if the specified element can be focused or not.
  84. *
  85. * @private
  86. * @param {Element} elm DOM element to check if it can be focused or not.
  87. * @return {Boolean} True/false if the element can have focus.
  88. */
  89. function canFocus(elm) {
  90. if (isTextInputElement(elm) && !elm.hidden) {
  91. return true;
  92. }
  93. if (/^(button|menuitem|checkbox|tab|menuitemcheckbox|option|gridcell)$/.test(getRole(elm))) {
  94. return true;
  95. }
  96. return false;
  97. }
  98. /**
  99. * Returns an array of focusable visible elements within the specified container element.
  100. *
  101. * @private
  102. * @param {Element} elm DOM element to find focusable elements within.
  103. * @return {Array} Array of focusable elements.
  104. */
  105. function getFocusElements(elm) {
  106. var elements = [];
  107. function collect(elm) {
  108. if (elm.nodeType != 1 || elm.style.display == 'none') {
  109. return;
  110. }
  111. if (canFocus(elm)) {
  112. elements.push(elm);
  113. }
  114. for (var i = 0; i < elm.childNodes.length; i++) {
  115. collect(elm.childNodes[i]);
  116. }
  117. }
  118. collect(elm || root.getEl());
  119. return elements;
  120. }
  121. /**
  122. * Returns the navigation root control for the specified control. The navigation root
  123. * is the control that the keyboard navigation gets scoped to for example a menubar or toolbar group.
  124. * It will look for parents of the specified target control or the currenty focused control if this option is omitted.
  125. *
  126. * @private
  127. * @param {tinymce.ui.Control} targetControl Optional target control to find root of.
  128. * @return {tinymce.ui.Control} Navigation root control.
  129. */
  130. function getNavigationRoot(targetControl) {
  131. var navigationRoot, controls;
  132. targetControl = targetControl || focusedControl;
  133. controls = targetControl.parents().toArray();
  134. controls.unshift(targetControl);
  135. for (var i = 0; i < controls.length; i++) {
  136. navigationRoot = controls[i];
  137. if (navigationRoot.settings.ariaRoot) {
  138. break;
  139. }
  140. }
  141. return navigationRoot;
  142. }
  143. /**
  144. * Focuses the first item in the specified targetControl element or the last aria index if the
  145. * navigation root has the ariaRemember option enabled.
  146. *
  147. * @private
  148. * @param {tinymce.ui.Control} targetControl Target control to focus the first item in.
  149. */
  150. function focusFirst(targetControl) {
  151. var navigationRoot = getNavigationRoot(targetControl);
  152. var focusElements = getFocusElements(navigationRoot.getEl());
  153. if (navigationRoot.settings.ariaRemember && "lastAriaIndex" in navigationRoot) {
  154. moveFocusToIndex(navigationRoot.lastAriaIndex, focusElements);
  155. } else {
  156. moveFocusToIndex(0, focusElements);
  157. }
  158. }
  159. /**
  160. * Moves the focus to the specified index within the elements list.
  161. * This will scope the index to the size of the element list if it changed.
  162. *
  163. * @private
  164. * @param {Number} idx Specified index to move to.
  165. * @param {Array} elements Array with dom elements to move focus within.
  166. * @return {Number} Input index or a changed index if it was out of range.
  167. */
  168. function moveFocusToIndex(idx, elements) {
  169. if (idx < 0) {
  170. idx = elements.length - 1;
  171. } else if (idx >= elements.length) {
  172. idx = 0;
  173. }
  174. if (elements[idx]) {
  175. elements[idx].focus();
  176. }
  177. return idx;
  178. }
  179. /**
  180. * Moves the focus forwards or backwards.
  181. *
  182. * @private
  183. * @param {Number} dir Direction to move in positive means forward, negative means backwards.
  184. * @param {Array} elements Optional array of elements to move within defaults to the current navigation roots elements.
  185. */
  186. function moveFocus(dir, elements) {
  187. var idx = -1, navigationRoot = getNavigationRoot();
  188. elements = elements || getFocusElements(navigationRoot.getEl());
  189. for (var i = 0; i < elements.length; i++) {
  190. if (elements[i] === focusedElement) {
  191. idx = i;
  192. }
  193. }
  194. idx += dir;
  195. navigationRoot.lastAriaIndex = moveFocusToIndex(idx, elements);
  196. }
  197. /**
  198. * Moves the focus to the left this is called by the left key.
  199. *
  200. * @private
  201. */
  202. function left() {
  203. var parentRole = getParentRole();
  204. if (parentRole == "tablist") {
  205. moveFocus(-1, getFocusElements(focusedElement.parentNode));
  206. } else if (focusedControl.parent().submenu) {
  207. cancel();
  208. } else {
  209. moveFocus(-1);
  210. }
  211. }
  212. /**
  213. * Moves the focus to the right this is called by the right key.
  214. *
  215. * @private
  216. */
  217. function right() {
  218. var role = getRole(), parentRole = getParentRole();
  219. if (parentRole == "tablist") {
  220. moveFocus(1, getFocusElements(focusedElement.parentNode));
  221. } else if (role == "menuitem" && parentRole == "menu" && getAriaProp('haspopup')) {
  222. enter();
  223. } else {
  224. moveFocus(1);
  225. }
  226. }
  227. /**
  228. * Moves the focus to the up this is called by the up key.
  229. *
  230. * @private
  231. */
  232. function up() {
  233. moveFocus(-1);
  234. }
  235. /**
  236. * Moves the focus to the up this is called by the down key.
  237. *
  238. * @private
  239. */
  240. function down() {
  241. var role = getRole(), parentRole = getParentRole();
  242. if (role == "menuitem" && parentRole == "menubar") {
  243. enter();
  244. } else if (role == "button" && getAriaProp('haspopup')) {
  245. enter({key: 'down'});
  246. } else {
  247. moveFocus(1);
  248. }
  249. }
  250. /**
  251. * Moves the focus to the next item or previous item depending on shift key.
  252. *
  253. * @private
  254. * @param {DOMEvent} e DOM event object.
  255. */
  256. function tab(e) {
  257. var parentRole = getParentRole();
  258. if (parentRole == "tablist") {
  259. var elm = getFocusElements(focusedControl.getEl('body'))[0];
  260. if (elm) {
  261. elm.focus();
  262. }
  263. } else {
  264. moveFocus(e.shiftKey ? -1 : 1);
  265. }
  266. }
  267. /**
  268. * Calls the cancel event on the currently focused control. This is normally done using the Esc key.
  269. *
  270. * @private
  271. */
  272. function cancel() {
  273. focusedControl.fire('cancel');
  274. }
  275. /**
  276. * Calls the click event on the currently focused control. This is normally done using the Enter/Space keys.
  277. *
  278. * @private
  279. * @param {Object} aria Optional aria data to pass along with the enter event.
  280. */
  281. function enter(aria) {
  282. aria = aria || {};
  283. focusedControl.fire('click', {target: focusedElement, aria: aria});
  284. }
  285. root.on('keydown', function(e) {
  286. function handleNonTabOrEscEvent(e, handler) {
  287. // Ignore non tab keys for text elements
  288. if (isTextInputElement(focusedElement)) {
  289. return;
  290. }
  291. if (handler(e) !== false) {
  292. e.preventDefault();
  293. }
  294. }
  295. if (e.isDefaultPrevented()) {
  296. return;
  297. }
  298. switch (e.keyCode) {
  299. case 37: // DOM_VK_LEFT
  300. handleNonTabOrEscEvent(e, left);
  301. break;
  302. case 39: // DOM_VK_RIGHT
  303. handleNonTabOrEscEvent(e, right);
  304. break;
  305. case 38: // DOM_VK_UP
  306. handleNonTabOrEscEvent(e, up);
  307. break;
  308. case 40: // DOM_VK_DOWN
  309. handleNonTabOrEscEvent(e, down);
  310. break;
  311. case 27: // DOM_VK_ESCAPE
  312. cancel();
  313. break;
  314. case 14: // DOM_VK_ENTER
  315. case 13: // DOM_VK_RETURN
  316. case 32: // DOM_VK_SPACE
  317. handleNonTabOrEscEvent(e, enter);
  318. break;
  319. case 9: // DOM_VK_TAB
  320. if (tab(e) !== false) {
  321. e.preventDefault();
  322. }
  323. break;
  324. }
  325. });
  326. root.on('focusin', function(e) {
  327. focusedElement = e.target;
  328. focusedControl = e.control;
  329. });
  330. return {
  331. focusFirst: focusFirst
  332. };
  333. };
  334. });