ffAccessibility.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. exports.getAccessibilityTree = getAccessibilityTree;
  6. /**
  7. * Copyright 2018 Google Inc. All rights reserved.
  8. * Modifications copyright (c) Microsoft Corporation.
  9. *
  10. * Licensed under the Apache License, Version 2.0 (the 'License');
  11. * you may not use this file except in compliance with the License.
  12. * You may obtain a copy of the License at
  13. *
  14. * http://www.apache.org/licenses/LICENSE-2.0
  15. *
  16. * Unless required by applicable law or agreed to in writing, software
  17. * distributed under the License is distributed on an 'AS IS' BASIS,
  18. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  19. * See the License for the specific language governing permissions and
  20. * limitations under the License.
  21. */
  22. async function getAccessibilityTree(session, needle) {
  23. const objectId = needle ? needle._objectId : undefined;
  24. const {
  25. tree
  26. } = await session.send('Accessibility.getFullAXTree', {
  27. objectId
  28. });
  29. const axNode = new FFAXNode(tree);
  30. return {
  31. tree: axNode,
  32. needle: needle ? axNode._findNeedle() : null
  33. };
  34. }
  35. const FFRoleToARIARole = new Map(Object.entries({
  36. 'pushbutton': 'button',
  37. 'checkbutton': 'checkbox',
  38. 'editcombobox': 'combobox',
  39. 'content deletion': 'deletion',
  40. 'footnote': 'doc-footnote',
  41. 'non-native document': 'document',
  42. 'grouping': 'group',
  43. 'graphic': 'img',
  44. 'content insertion': 'insertion',
  45. 'animation': 'marquee',
  46. 'flat equation': 'math',
  47. 'menupopup': 'menu',
  48. 'check menu item': 'menuitemcheckbox',
  49. 'radio menu item': 'menuitemradio',
  50. 'listbox option': 'option',
  51. 'radiobutton': 'radio',
  52. 'statusbar': 'status',
  53. 'pagetab': 'tab',
  54. 'pagetablist': 'tablist',
  55. 'propertypage': 'tabpanel',
  56. 'entry': 'textbox',
  57. 'outline': 'tree',
  58. 'tree table': 'treegrid',
  59. 'outlineitem': 'treeitem'
  60. }));
  61. class FFAXNode {
  62. constructor(payload) {
  63. this._children = void 0;
  64. this._payload = void 0;
  65. this._editable = void 0;
  66. this._richlyEditable = void 0;
  67. this._focusable = void 0;
  68. this._expanded = void 0;
  69. this._name = void 0;
  70. this._role = void 0;
  71. this._cachedHasFocusableChild = void 0;
  72. this._payload = payload;
  73. this._children = (payload.children || []).map(x => new FFAXNode(x));
  74. this._editable = !!payload.editable;
  75. this._richlyEditable = this._editable && payload.tag !== 'textarea' && payload.tag !== 'input';
  76. this._focusable = !!payload.focusable;
  77. this._expanded = !!payload.expanded;
  78. this._name = this._payload.name;
  79. this._role = this._payload.role;
  80. }
  81. _isPlainTextField() {
  82. if (this._richlyEditable) return false;
  83. if (this._editable) return true;
  84. return this._role === 'entry';
  85. }
  86. _isTextOnlyObject() {
  87. const role = this._role;
  88. return role === 'text leaf' || role === 'text' || role === 'statictext';
  89. }
  90. _hasFocusableChild() {
  91. if (this._cachedHasFocusableChild === undefined) {
  92. this._cachedHasFocusableChild = false;
  93. for (const child of this._children) {
  94. if (child._focusable || child._hasFocusableChild()) {
  95. this._cachedHasFocusableChild = true;
  96. break;
  97. }
  98. }
  99. }
  100. return this._cachedHasFocusableChild;
  101. }
  102. children() {
  103. return this._children;
  104. }
  105. _findNeedle() {
  106. if (this._payload.foundObject) return this;
  107. for (const child of this._children) {
  108. const found = child._findNeedle();
  109. if (found) return found;
  110. }
  111. return null;
  112. }
  113. isLeafNode() {
  114. if (!this._children.length) return true;
  115. // These types of objects may have children that we use as internal
  116. // implementation details, but we want to expose them as leaves to platform
  117. // accessibility APIs because screen readers might be confused if they find
  118. // any children.
  119. if (this._isPlainTextField() || this._isTextOnlyObject()) return true;
  120. // Roles whose children are only presentational according to the ARIA and
  121. // HTML5 Specs should be hidden from screen readers.
  122. // (Note that whilst ARIA buttons can have only presentational children, HTML5
  123. // buttons are allowed to have content.)
  124. switch (this._role) {
  125. case 'graphic':
  126. case 'scrollbar':
  127. case 'slider':
  128. case 'separator':
  129. case 'progressbar':
  130. return true;
  131. default:
  132. break;
  133. }
  134. // Here and below: Android heuristics
  135. if (this._hasFocusableChild()) return false;
  136. if (this._focusable && this._role !== 'document' && this._name) return true;
  137. if (this._role === 'heading' && this._name) return true;
  138. return false;
  139. }
  140. isControl() {
  141. switch (this._role) {
  142. case 'checkbutton':
  143. case 'check menu item':
  144. case 'check rich option':
  145. case 'combobox':
  146. case 'combobox option':
  147. case 'color chooser':
  148. case 'listbox':
  149. case 'listbox option':
  150. case 'listbox rich option':
  151. case 'popup menu':
  152. case 'menupopup':
  153. case 'menuitem':
  154. case 'menubar':
  155. case 'button':
  156. case 'pushbutton':
  157. case 'radiobutton':
  158. case 'radio menuitem':
  159. case 'scrollbar':
  160. case 'slider':
  161. case 'spinbutton':
  162. case 'switch':
  163. case 'pagetab':
  164. case 'entry':
  165. case 'tree table':
  166. return true;
  167. default:
  168. return false;
  169. }
  170. }
  171. isInteresting(insideControl) {
  172. if (this._focusable || this._richlyEditable) return true;
  173. // If it's not focusable but has a control role, then it's interesting.
  174. if (this.isControl()) return true;
  175. // A non focusable child of a control is not interesting
  176. if (insideControl) return false;
  177. return this.isLeafNode() && !!this._name.trim();
  178. }
  179. serialize() {
  180. const node = {
  181. role: FFRoleToARIARole.get(this._role) || this._role,
  182. name: this._name || ''
  183. };
  184. const userStringProperties = ['name', 'description', 'roledescription', 'valuetext', 'keyshortcuts'];
  185. for (const userStringProperty of userStringProperties) {
  186. if (!(userStringProperty in this._payload)) continue;
  187. node[userStringProperty] = this._payload[userStringProperty];
  188. }
  189. const booleanProperties = ['disabled', 'expanded', 'focused', 'modal', 'multiline', 'multiselectable', 'readonly', 'required', 'selected'];
  190. for (const booleanProperty of booleanProperties) {
  191. if (this._role === 'document' && booleanProperty === 'focused') continue; // document focusing is strange
  192. const value = this._payload[booleanProperty];
  193. if (!value) continue;
  194. node[booleanProperty] = value;
  195. }
  196. const numericalProperties = ['level'];
  197. for (const numericalProperty of numericalProperties) {
  198. if (!(numericalProperty in this._payload)) continue;
  199. node[numericalProperty] = this._payload[numericalProperty];
  200. }
  201. const tokenProperties = ['autocomplete', 'haspopup', 'invalid', 'orientation'];
  202. for (const tokenProperty of tokenProperties) {
  203. const value = this._payload[tokenProperty];
  204. if (!value || value === 'false') continue;
  205. node[tokenProperty] = value;
  206. }
  207. const axNode = node;
  208. axNode.valueString = this._payload.value;
  209. if ('checked' in this._payload) axNode.checked = this._payload.checked === true ? 'checked' : this._payload.checked === 'mixed' ? 'mixed' : 'unchecked';
  210. if ('pressed' in this._payload) axNode.pressed = this._payload.pressed === true ? 'pressed' : 'released';
  211. return axNode;
  212. }
  213. }