Traversal.jsm 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  3. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. /* global PrefCache, Roles, Prefilters, States, Filters, Utils,
  5. TraversalRules, Components, XPCOMUtils */
  6. /* exported TraversalRules, TraversalHelper */
  7. 'use strict';
  8. const Ci = Components.interfaces;
  9. const Cu = Components.utils;
  10. this.EXPORTED_SYMBOLS = ['TraversalRules', 'TraversalHelper']; // jshint ignore:line
  11. Cu.import('resource://gre/modules/accessibility/Utils.jsm');
  12. Cu.import('resource://gre/modules/XPCOMUtils.jsm');
  13. XPCOMUtils.defineLazyModuleGetter(this, 'Roles', // jshint ignore:line
  14. 'resource://gre/modules/accessibility/Constants.jsm');
  15. XPCOMUtils.defineLazyModuleGetter(this, 'Filters', // jshint ignore:line
  16. 'resource://gre/modules/accessibility/Constants.jsm');
  17. XPCOMUtils.defineLazyModuleGetter(this, 'States', // jshint ignore:line
  18. 'resource://gre/modules/accessibility/Constants.jsm');
  19. XPCOMUtils.defineLazyModuleGetter(this, 'Prefilters', // jshint ignore:line
  20. 'resource://gre/modules/accessibility/Constants.jsm');
  21. var gSkipEmptyImages = new PrefCache('accessibility.accessfu.skip_empty_images');
  22. function BaseTraversalRule(aRoles, aMatchFunc, aPreFilter, aContainerRule) {
  23. this._explicitMatchRoles = new Set(aRoles);
  24. this._matchRoles = aRoles;
  25. if (aRoles.length) {
  26. if (aRoles.indexOf(Roles.LABEL) < 0) {
  27. this._matchRoles.push(Roles.LABEL);
  28. }
  29. if (aRoles.indexOf(Roles.INTERNAL_FRAME) < 0) {
  30. // Used for traversing in to child OOP frames.
  31. this._matchRoles.push(Roles.INTERNAL_FRAME);
  32. }
  33. }
  34. this._matchFunc = aMatchFunc || function() { return Filters.MATCH; };
  35. this.preFilter = aPreFilter || gSimplePreFilter;
  36. this.containerRule = aContainerRule;
  37. }
  38. BaseTraversalRule.prototype = {
  39. getMatchRoles: function BaseTraversalRule_getmatchRoles(aRoles) {
  40. aRoles.value = this._matchRoles;
  41. return aRoles.value.length;
  42. },
  43. match: function BaseTraversalRule_match(aAccessible)
  44. {
  45. let role = aAccessible.role;
  46. if (role == Roles.INTERNAL_FRAME) {
  47. return (Utils.getMessageManager(aAccessible.DOMNode)) ?
  48. Filters.MATCH | Filters.IGNORE_SUBTREE : Filters.IGNORE;
  49. }
  50. let matchResult =
  51. (this._explicitMatchRoles.has(role) || !this._explicitMatchRoles.size) ?
  52. this._matchFunc(aAccessible) : Filters.IGNORE;
  53. // If we are on a label that nests a checkbox/radio we should land on it.
  54. // It is a bigger touch target, and it reduces clutter.
  55. if (role == Roles.LABEL && !(matchResult & Filters.IGNORE_SUBTREE)) {
  56. let control = Utils.getEmbeddedControl(aAccessible);
  57. if (control && this._explicitMatchRoles.has(control.role)) {
  58. matchResult = this._matchFunc(control) | Filters.IGNORE_SUBTREE;
  59. }
  60. }
  61. return matchResult;
  62. },
  63. QueryInterface: XPCOMUtils.generateQI([Ci.nsIAccessibleTraversalRule])
  64. };
  65. var gSimpleTraversalRoles =
  66. [Roles.MENUITEM,
  67. Roles.LINK,
  68. Roles.PAGETAB,
  69. Roles.GRAPHIC,
  70. Roles.STATICTEXT,
  71. Roles.TEXT_LEAF,
  72. Roles.PUSHBUTTON,
  73. Roles.CHECKBUTTON,
  74. Roles.RADIOBUTTON,
  75. Roles.COMBOBOX,
  76. Roles.PROGRESSBAR,
  77. Roles.BUTTONDROPDOWN,
  78. Roles.BUTTONMENU,
  79. Roles.CHECK_MENU_ITEM,
  80. Roles.PASSWORD_TEXT,
  81. Roles.RADIO_MENU_ITEM,
  82. Roles.TOGGLE_BUTTON,
  83. Roles.ENTRY,
  84. Roles.KEY,
  85. Roles.HEADER,
  86. Roles.HEADING,
  87. Roles.SLIDER,
  88. Roles.SPINBUTTON,
  89. Roles.OPTION,
  90. Roles.LISTITEM,
  91. Roles.GRID_CELL,
  92. Roles.COLUMNHEADER,
  93. Roles.ROWHEADER,
  94. Roles.STATUSBAR,
  95. Roles.SWITCH,
  96. Roles.MATHML_MATH];
  97. var gSimpleMatchFunc = function gSimpleMatchFunc(aAccessible) {
  98. // An object is simple, if it either has a single child lineage,
  99. // or has a flat subtree.
  100. function isSingleLineage(acc) {
  101. for (let child = acc; child; child = child.firstChild) {
  102. if (Utils.visibleChildCount(child) > 1) {
  103. return false;
  104. }
  105. }
  106. return true;
  107. }
  108. function isFlatSubtree(acc) {
  109. for (let child = acc.firstChild; child; child = child.nextSibling) {
  110. // text leafs inherit the actionCount of any ancestor that has a click
  111. // listener.
  112. if ([Roles.TEXT_LEAF, Roles.STATICTEXT].indexOf(child.role) >= 0) {
  113. continue;
  114. }
  115. if (Utils.visibleChildCount(child) > 0 || child.actionCount > 0) {
  116. return false;
  117. }
  118. }
  119. return true;
  120. }
  121. switch (aAccessible.role) {
  122. case Roles.COMBOBOX:
  123. // We don't want to ignore the subtree because this is often
  124. // where the list box hangs out.
  125. return Filters.MATCH;
  126. case Roles.TEXT_LEAF:
  127. {
  128. // Nameless text leaves are boring, skip them.
  129. let name = aAccessible.name;
  130. return (name && name.trim()) ? Filters.MATCH : Filters.IGNORE;
  131. }
  132. case Roles.STATICTEXT:
  133. // Ignore prefix static text in list items. They are typically bullets or numbers.
  134. return Utils.isListItemDecorator(aAccessible) ?
  135. Filters.IGNORE : Filters.MATCH;
  136. case Roles.GRAPHIC:
  137. return TraversalRules._shouldSkipImage(aAccessible);
  138. case Roles.HEADER:
  139. case Roles.HEADING:
  140. case Roles.COLUMNHEADER:
  141. case Roles.ROWHEADER:
  142. case Roles.STATUSBAR:
  143. if ((aAccessible.childCount > 0 || aAccessible.name) &&
  144. (isSingleLineage(aAccessible) || isFlatSubtree(aAccessible))) {
  145. return Filters.MATCH | Filters.IGNORE_SUBTREE;
  146. }
  147. return Filters.IGNORE;
  148. case Roles.GRID_CELL:
  149. return isSingleLineage(aAccessible) || isFlatSubtree(aAccessible) ?
  150. Filters.MATCH | Filters.IGNORE_SUBTREE : Filters.IGNORE;
  151. case Roles.LISTITEM:
  152. {
  153. let item = aAccessible.childCount === 2 &&
  154. aAccessible.firstChild.role === Roles.STATICTEXT ?
  155. aAccessible.lastChild : aAccessible;
  156. return isSingleLineage(item) || isFlatSubtree(item) ?
  157. Filters.MATCH | Filters.IGNORE_SUBTREE : Filters.IGNORE;
  158. }
  159. default:
  160. // Ignore the subtree, if there is one. So that we don't land on
  161. // the same content that was already presented by its parent.
  162. return Filters.MATCH |
  163. Filters.IGNORE_SUBTREE;
  164. }
  165. };
  166. var gSimplePreFilter = Prefilters.DEFUNCT |
  167. Prefilters.INVISIBLE |
  168. Prefilters.ARIA_HIDDEN |
  169. Prefilters.TRANSPARENT;
  170. this.TraversalRules = { // jshint ignore:line
  171. Simple: new BaseTraversalRule(gSimpleTraversalRoles, gSimpleMatchFunc),
  172. SimpleOnScreen: new BaseTraversalRule(
  173. gSimpleTraversalRoles, gSimpleMatchFunc,
  174. Prefilters.DEFUNCT | Prefilters.INVISIBLE | Prefilters.ARIA_HIDDEN |
  175. Prefilters.TRANSPARENT | Prefilters.OFFSCREEN),
  176. Anchor: new BaseTraversalRule(
  177. [Roles.LINK],
  178. function Anchor_match(aAccessible)
  179. {
  180. // We want to ignore links, only focus named anchors.
  181. if (Utils.getState(aAccessible).contains(States.LINKED)) {
  182. return Filters.IGNORE;
  183. } else {
  184. return Filters.MATCH;
  185. }
  186. }),
  187. Button: new BaseTraversalRule(
  188. [Roles.PUSHBUTTON,
  189. Roles.SPINBUTTON,
  190. Roles.TOGGLE_BUTTON,
  191. Roles.BUTTONDROPDOWN,
  192. Roles.BUTTONDROPDOWNGRID]),
  193. Combobox: new BaseTraversalRule(
  194. [Roles.COMBOBOX,
  195. Roles.LISTBOX]),
  196. Landmark: new BaseTraversalRule(
  197. [],
  198. function Landmark_match(aAccessible) {
  199. return Utils.getLandmarkName(aAccessible) ? Filters.MATCH :
  200. Filters.IGNORE;
  201. }, null, true),
  202. /* A rule for Android's section navigation, lands on landmarks, regions, and
  203. on headings to aid navigation of traditionally structured documents */
  204. Section: new BaseTraversalRule(
  205. [],
  206. function Section_match(aAccessible) {
  207. if (aAccessible.role === Roles.HEADING) {
  208. return Filters.MATCH;
  209. }
  210. let matchedRole = Utils.matchRoles(aAccessible, [
  211. 'banner',
  212. 'complementary',
  213. 'contentinfo',
  214. 'main',
  215. 'navigation',
  216. 'search',
  217. 'region'
  218. ]);
  219. return matchedRole ? Filters.MATCH : Filters.IGNORE;
  220. }, null, true),
  221. Entry: new BaseTraversalRule(
  222. [Roles.ENTRY,
  223. Roles.PASSWORD_TEXT]),
  224. FormElement: new BaseTraversalRule(
  225. [Roles.PUSHBUTTON,
  226. Roles.SPINBUTTON,
  227. Roles.TOGGLE_BUTTON,
  228. Roles.BUTTONDROPDOWN,
  229. Roles.BUTTONDROPDOWNGRID,
  230. Roles.COMBOBOX,
  231. Roles.LISTBOX,
  232. Roles.ENTRY,
  233. Roles.PASSWORD_TEXT,
  234. Roles.PAGETAB,
  235. Roles.RADIOBUTTON,
  236. Roles.RADIO_MENU_ITEM,
  237. Roles.SLIDER,
  238. Roles.CHECKBUTTON,
  239. Roles.CHECK_MENU_ITEM,
  240. Roles.SWITCH]),
  241. Graphic: new BaseTraversalRule(
  242. [Roles.GRAPHIC],
  243. function Graphic_match(aAccessible) {
  244. return TraversalRules._shouldSkipImage(aAccessible);
  245. }),
  246. Heading: new BaseTraversalRule(
  247. [Roles.HEADING],
  248. function Heading_match(aAccessible) {
  249. return aAccessible.childCount > 0 ? Filters.MATCH : Filters.IGNORE;
  250. }),
  251. ListItem: new BaseTraversalRule(
  252. [Roles.LISTITEM,
  253. Roles.TERM]),
  254. Link: new BaseTraversalRule(
  255. [Roles.LINK],
  256. function Link_match(aAccessible)
  257. {
  258. // We want to ignore anchors, only focus real links.
  259. if (Utils.getState(aAccessible).contains(States.LINKED)) {
  260. return Filters.MATCH;
  261. } else {
  262. return Filters.IGNORE;
  263. }
  264. }),
  265. /* For TalkBack's "Control" granularity. Form conrols and links */
  266. Control: new BaseTraversalRule(
  267. [Roles.PUSHBUTTON,
  268. Roles.SPINBUTTON,
  269. Roles.TOGGLE_BUTTON,
  270. Roles.BUTTONDROPDOWN,
  271. Roles.BUTTONDROPDOWNGRID,
  272. Roles.COMBOBOX,
  273. Roles.LISTBOX,
  274. Roles.ENTRY,
  275. Roles.PASSWORD_TEXT,
  276. Roles.PAGETAB,
  277. Roles.RADIOBUTTON,
  278. Roles.RADIO_MENU_ITEM,
  279. Roles.SLIDER,
  280. Roles.CHECKBUTTON,
  281. Roles.CHECK_MENU_ITEM,
  282. Roles.SWITCH,
  283. Roles.LINK,
  284. Roles.MENUITEM],
  285. function Control_match(aAccessible)
  286. {
  287. // We want to ignore anchors, only focus real links.
  288. if (aAccessible.role == Roles.LINK &&
  289. !Utils.getState(aAccessible).contains(States.LINKED)) {
  290. return Filters.IGNORE;
  291. }
  292. return Filters.MATCH;
  293. }),
  294. List: new BaseTraversalRule(
  295. [Roles.LIST,
  296. Roles.DEFINITION_LIST],
  297. null, null, true),
  298. PageTab: new BaseTraversalRule(
  299. [Roles.PAGETAB]),
  300. Paragraph: new BaseTraversalRule(
  301. [Roles.PARAGRAPH,
  302. Roles.SECTION],
  303. function Paragraph_match(aAccessible) {
  304. for (let child = aAccessible.firstChild; child; child = child.nextSibling) {
  305. if (child.role === Roles.TEXT_LEAF) {
  306. return Filters.MATCH | Filters.IGNORE_SUBTREE;
  307. }
  308. }
  309. return Filters.IGNORE;
  310. }),
  311. RadioButton: new BaseTraversalRule(
  312. [Roles.RADIOBUTTON,
  313. Roles.RADIO_MENU_ITEM]),
  314. Separator: new BaseTraversalRule(
  315. [Roles.SEPARATOR]),
  316. Table: new BaseTraversalRule(
  317. [Roles.TABLE]),
  318. Checkbox: new BaseTraversalRule(
  319. [Roles.CHECKBUTTON,
  320. Roles.CHECK_MENU_ITEM,
  321. Roles.SWITCH /* A type of checkbox that represents on/off values */]),
  322. _shouldSkipImage: function _shouldSkipImage(aAccessible) {
  323. if (gSkipEmptyImages.value && aAccessible.name === '') {
  324. return Filters.IGNORE;
  325. }
  326. return Filters.MATCH;
  327. }
  328. };
  329. this.TraversalHelper = {
  330. _helperPivotCache: null,
  331. get helperPivotCache() {
  332. delete this.helperPivotCache;
  333. this.helperPivotCache = new WeakMap();
  334. return this.helperPivotCache;
  335. },
  336. getHelperPivot: function TraversalHelper_getHelperPivot(aRoot) {
  337. let pivot = this.helperPivotCache.get(aRoot.DOMNode);
  338. if (!pivot) {
  339. pivot = Utils.AccService.createAccessiblePivot(aRoot);
  340. this.helperPivotCache.set(aRoot.DOMNode, pivot);
  341. }
  342. return pivot;
  343. },
  344. move: function TraversalHelper_move(aVirtualCursor, aMethod, aRule) {
  345. let rule = TraversalRules[aRule];
  346. if (rule.containerRule) {
  347. let moved = false;
  348. let helperPivot = this.getHelperPivot(aVirtualCursor.root);
  349. helperPivot.position = aVirtualCursor.position;
  350. // We continue to step through containers until there is one with an
  351. // atomic child (via 'Simple') on which we could land.
  352. while (!moved) {
  353. if (helperPivot[aMethod](rule)) {
  354. aVirtualCursor.modalRoot = helperPivot.position;
  355. moved = aVirtualCursor.moveFirst(TraversalRules.Simple);
  356. aVirtualCursor.modalRoot = null;
  357. } else {
  358. // If we failed to step to another container, break and return false.
  359. break;
  360. }
  361. }
  362. return moved;
  363. } else {
  364. return aVirtualCursor[aMethod](rule);
  365. }
  366. }
  367. };