1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/accessible/src/jsat/TraversalRules.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,283 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +'use strict'; 1.9 + 1.10 +const Cc = Components.classes; 1.11 +const Ci = Components.interfaces; 1.12 +const Cu = Components.utils; 1.13 +const Cr = Components.results; 1.14 + 1.15 +this.EXPORTED_SYMBOLS = ['TraversalRules']; 1.16 + 1.17 +Cu.import('resource://gre/modules/accessibility/Utils.jsm'); 1.18 +Cu.import('resource://gre/modules/XPCOMUtils.jsm'); 1.19 +XPCOMUtils.defineLazyModuleGetter(this, 'Roles', 1.20 + 'resource://gre/modules/accessibility/Constants.jsm'); 1.21 +XPCOMUtils.defineLazyModuleGetter(this, 'Filters', 1.22 + 'resource://gre/modules/accessibility/Constants.jsm'); 1.23 +XPCOMUtils.defineLazyModuleGetter(this, 'States', 1.24 + 'resource://gre/modules/accessibility/Constants.jsm'); 1.25 +XPCOMUtils.defineLazyModuleGetter(this, 'Prefilters', 1.26 + 'resource://gre/modules/accessibility/Constants.jsm'); 1.27 + 1.28 +let gSkipEmptyImages = new PrefCache('accessibility.accessfu.skip_empty_images'); 1.29 + 1.30 +function BaseTraversalRule(aRoles, aMatchFunc, aPreFilter) { 1.31 + this._explicitMatchRoles = new Set(aRoles); 1.32 + this._matchRoles = aRoles; 1.33 + if (aRoles.indexOf(Roles.LABEL) < 0) { 1.34 + this._matchRoles.push(Roles.LABEL); 1.35 + } 1.36 + this._matchFunc = aMatchFunc || function (acc) { return Filters.MATCH; }; 1.37 + this.preFilter = aPreFilter || gSimplePreFilter; 1.38 +} 1.39 + 1.40 +BaseTraversalRule.prototype = { 1.41 + getMatchRoles: function BaseTraversalRule_getmatchRoles(aRules) { 1.42 + aRules.value = this._matchRoles; 1.43 + return aRules.value.length; 1.44 + }, 1.45 + 1.46 + match: function BaseTraversalRule_match(aAccessible) 1.47 + { 1.48 + let role = aAccessible.role; 1.49 + if (role == Roles.INTERNAL_FRAME) { 1.50 + return (Utils.getMessageManager(aAccessible.DOMNode)) ? 1.51 + Filters.MATCH | Filters.IGNORE_SUBTREE : Filters.IGNORE; 1.52 + } 1.53 + 1.54 + let matchResult = this._explicitMatchRoles.has(role) ? 1.55 + this._matchFunc(aAccessible) : Filters.IGNORE; 1.56 + 1.57 + // If we are on a label that nests a checkbox/radio we should land on it. 1.58 + // It is a bigger touch target, and it reduces clutter. 1.59 + if (role == Roles.LABEL && !(matchResult & Filters.IGNORE_SUBTREE)) { 1.60 + let control = Utils.getEmbeddedControl(aAccessible); 1.61 + if (control && this._explicitMatchRoles.has(control.role)) { 1.62 + matchResult = this._matchFunc(control) | Filters.IGNORE_SUBTREE; 1.63 + } 1.64 + } 1.65 + 1.66 + return matchResult; 1.67 + }, 1.68 + 1.69 + QueryInterface: XPCOMUtils.generateQI([Ci.nsIAccessibleTraversalRule]) 1.70 +}; 1.71 + 1.72 +var gSimpleTraversalRoles = 1.73 + [Roles.MENUITEM, 1.74 + Roles.LINK, 1.75 + Roles.PAGETAB, 1.76 + Roles.GRAPHIC, 1.77 + Roles.STATICTEXT, 1.78 + Roles.TEXT_LEAF, 1.79 + Roles.PUSHBUTTON, 1.80 + Roles.CHECKBUTTON, 1.81 + Roles.RADIOBUTTON, 1.82 + Roles.COMBOBOX, 1.83 + Roles.PROGRESSBAR, 1.84 + Roles.BUTTONDROPDOWN, 1.85 + Roles.BUTTONMENU, 1.86 + Roles.CHECK_MENU_ITEM, 1.87 + Roles.PASSWORD_TEXT, 1.88 + Roles.RADIO_MENU_ITEM, 1.89 + Roles.TOGGLE_BUTTON, 1.90 + Roles.ENTRY, 1.91 + Roles.KEY, 1.92 + Roles.HEADER, 1.93 + Roles.HEADING, 1.94 + Roles.SLIDER, 1.95 + Roles.SPINBUTTON, 1.96 + Roles.OPTION, 1.97 + // Used for traversing in to child OOP frames. 1.98 + Roles.INTERNAL_FRAME]; 1.99 + 1.100 +var gSimpleMatchFunc = function gSimpleMatchFunc(aAccessible) { 1.101 + function hasZeroOrSingleChildDescendants () { 1.102 + for (let acc = aAccessible; acc.childCount > 0; acc = acc.firstChild) { 1.103 + if (acc.childCount > 1) { 1.104 + return false; 1.105 + } 1.106 + } 1.107 + 1.108 + return true; 1.109 + } 1.110 + 1.111 + switch (aAccessible.role) { 1.112 + case Roles.COMBOBOX: 1.113 + // We don't want to ignore the subtree because this is often 1.114 + // where the list box hangs out. 1.115 + return Filters.MATCH; 1.116 + case Roles.TEXT_LEAF: 1.117 + { 1.118 + // Nameless text leaves are boring, skip them. 1.119 + let name = aAccessible.name; 1.120 + if (name && name.trim()) 1.121 + return Filters.MATCH; 1.122 + else 1.123 + return Filters.IGNORE; 1.124 + } 1.125 + case Roles.STATICTEXT: 1.126 + { 1.127 + let parent = aAccessible.parent; 1.128 + // Ignore prefix static text in list items. They are typically bullets or numbers. 1.129 + if (parent.childCount > 1 && aAccessible.indexInParent == 0 && 1.130 + parent.role == Roles.LISTITEM) 1.131 + return Filters.IGNORE; 1.132 + 1.133 + return Filters.MATCH; 1.134 + } 1.135 + case Roles.GRAPHIC: 1.136 + return TraversalRules._shouldSkipImage(aAccessible); 1.137 + case Roles.HEADER: 1.138 + case Roles.HEADING: 1.139 + if ((aAccessible.childCount > 0 || aAccessible.name) && 1.140 + hasZeroOrSingleChildDescendants()) { 1.141 + return Filters.MATCH | Filters.IGNORE_SUBTREE; 1.142 + } else { 1.143 + return Filters.IGNORE; 1.144 + } 1.145 + default: 1.146 + // Ignore the subtree, if there is one. So that we don't land on 1.147 + // the same content that was already presented by its parent. 1.148 + return Filters.MATCH | 1.149 + Filters.IGNORE_SUBTREE; 1.150 + } 1.151 +}; 1.152 + 1.153 +var gSimplePreFilter = Prefilters.DEFUNCT | 1.154 + Prefilters.INVISIBLE | 1.155 + Prefilters.ARIA_HIDDEN | 1.156 + Prefilters.TRANSPARENT; 1.157 + 1.158 +this.TraversalRules = { 1.159 + Simple: new BaseTraversalRule(gSimpleTraversalRoles, gSimpleMatchFunc), 1.160 + 1.161 + SimpleOnScreen: new BaseTraversalRule( 1.162 + gSimpleTraversalRoles, gSimpleMatchFunc, 1.163 + Prefilters.DEFUNCT | Prefilters.INVISIBLE | Prefilters.ARIA_HIDDEN | 1.164 + Prefilters.TRANSPARENT | Prefilters.OFFSCREEN), 1.165 + 1.166 + Anchor: new BaseTraversalRule( 1.167 + [Roles.LINK], 1.168 + function Anchor_match(aAccessible) 1.169 + { 1.170 + // We want to ignore links, only focus named anchors. 1.171 + if (Utils.getState(aAccessible).contains(States.LINKED)) { 1.172 + return Filters.IGNORE; 1.173 + } else { 1.174 + return Filters.MATCH; 1.175 + } 1.176 + }), 1.177 + 1.178 + Button: new BaseTraversalRule( 1.179 + [Roles.PUSHBUTTON, 1.180 + Roles.SPINBUTTON, 1.181 + Roles.TOGGLE_BUTTON, 1.182 + Roles.BUTTONDROPDOWN, 1.183 + Roles.BUTTONDROPDOWNGRID]), 1.184 + 1.185 + Combobox: new BaseTraversalRule( 1.186 + [Roles.COMBOBOX, 1.187 + Roles.LISTBOX]), 1.188 + 1.189 + Landmark: new BaseTraversalRule( 1.190 + [], 1.191 + function Landmark_match(aAccessible) { 1.192 + return Utils.getLandmarkName(aAccessible) ? Filters.MATCH : 1.193 + Filters.IGNORE; 1.194 + } 1.195 + ), 1.196 + 1.197 + Entry: new BaseTraversalRule( 1.198 + [Roles.ENTRY, 1.199 + Roles.PASSWORD_TEXT]), 1.200 + 1.201 + FormElement: new BaseTraversalRule( 1.202 + [Roles.PUSHBUTTON, 1.203 + Roles.SPINBUTTON, 1.204 + Roles.TOGGLE_BUTTON, 1.205 + Roles.BUTTONDROPDOWN, 1.206 + Roles.BUTTONDROPDOWNGRID, 1.207 + Roles.COMBOBOX, 1.208 + Roles.LISTBOX, 1.209 + Roles.ENTRY, 1.210 + Roles.PASSWORD_TEXT, 1.211 + Roles.PAGETAB, 1.212 + Roles.RADIOBUTTON, 1.213 + Roles.RADIO_MENU_ITEM, 1.214 + Roles.SLIDER, 1.215 + Roles.CHECKBUTTON, 1.216 + Roles.CHECK_MENU_ITEM]), 1.217 + 1.218 + Graphic: new BaseTraversalRule( 1.219 + [Roles.GRAPHIC], 1.220 + function Graphic_match(aAccessible) { 1.221 + return TraversalRules._shouldSkipImage(aAccessible); 1.222 + }), 1.223 + 1.224 + Heading: new BaseTraversalRule( 1.225 + [Roles.HEADING], 1.226 + function Heading_match(aAccessible) { 1.227 + return aAccessible.childCount > 0 ? Filters.MATCH : Filters.IGNORE; 1.228 + }), 1.229 + 1.230 + ListItem: new BaseTraversalRule( 1.231 + [Roles.LISTITEM, 1.232 + Roles.TERM]), 1.233 + 1.234 + Link: new BaseTraversalRule( 1.235 + [Roles.LINK], 1.236 + function Link_match(aAccessible) 1.237 + { 1.238 + // We want to ignore anchors, only focus real links. 1.239 + if (Utils.getState(aAccessible).contains(States.LINKED)) { 1.240 + return Filters.MATCH; 1.241 + } else { 1.242 + return Filters.IGNORE; 1.243 + } 1.244 + }), 1.245 + 1.246 + List: new BaseTraversalRule( 1.247 + [Roles.LIST, 1.248 + Roles.DEFINITION_LIST]), 1.249 + 1.250 + PageTab: new BaseTraversalRule( 1.251 + [Roles.PAGETAB]), 1.252 + 1.253 + Paragraph: new BaseTraversalRule( 1.254 + [Roles.PARAGRAPH, 1.255 + Roles.SECTION], 1.256 + function Paragraph_match(aAccessible) { 1.257 + for (let child = aAccessible.firstChild; child; child = child.nextSibling) { 1.258 + if (child.role === Roles.TEXT_LEAF) { 1.259 + return Filters.MATCH | Filters.IGNORE_SUBTREE; 1.260 + } 1.261 + } 1.262 + 1.263 + return Filters.IGNORE; 1.264 + }), 1.265 + 1.266 + RadioButton: new BaseTraversalRule( 1.267 + [Roles.RADIOBUTTON, 1.268 + Roles.RADIO_MENU_ITEM]), 1.269 + 1.270 + Separator: new BaseTraversalRule( 1.271 + [Roles.SEPARATOR]), 1.272 + 1.273 + Table: new BaseTraversalRule( 1.274 + [Roles.TABLE]), 1.275 + 1.276 + Checkbox: new BaseTraversalRule( 1.277 + [Roles.CHECKBUTTON, 1.278 + Roles.CHECK_MENU_ITEM]), 1.279 + 1.280 + _shouldSkipImage: function _shouldSkipImage(aAccessible) { 1.281 + if (gSkipEmptyImages.value && aAccessible.name === '') { 1.282 + return Filters.IGNORE; 1.283 + } 1.284 + return Filters.MATCH; 1.285 + } 1.286 +};