|
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 |
|
5 'use strict'; |
|
6 |
|
7 const Cc = Components.classes; |
|
8 const Ci = Components.interfaces; |
|
9 const Cu = Components.utils; |
|
10 const Cr = Components.results; |
|
11 |
|
12 this.EXPORTED_SYMBOLS = ['TraversalRules']; |
|
13 |
|
14 Cu.import('resource://gre/modules/accessibility/Utils.jsm'); |
|
15 Cu.import('resource://gre/modules/XPCOMUtils.jsm'); |
|
16 XPCOMUtils.defineLazyModuleGetter(this, 'Roles', |
|
17 'resource://gre/modules/accessibility/Constants.jsm'); |
|
18 XPCOMUtils.defineLazyModuleGetter(this, 'Filters', |
|
19 'resource://gre/modules/accessibility/Constants.jsm'); |
|
20 XPCOMUtils.defineLazyModuleGetter(this, 'States', |
|
21 'resource://gre/modules/accessibility/Constants.jsm'); |
|
22 XPCOMUtils.defineLazyModuleGetter(this, 'Prefilters', |
|
23 'resource://gre/modules/accessibility/Constants.jsm'); |
|
24 |
|
25 let gSkipEmptyImages = new PrefCache('accessibility.accessfu.skip_empty_images'); |
|
26 |
|
27 function BaseTraversalRule(aRoles, aMatchFunc, aPreFilter) { |
|
28 this._explicitMatchRoles = new Set(aRoles); |
|
29 this._matchRoles = aRoles; |
|
30 if (aRoles.indexOf(Roles.LABEL) < 0) { |
|
31 this._matchRoles.push(Roles.LABEL); |
|
32 } |
|
33 this._matchFunc = aMatchFunc || function (acc) { return Filters.MATCH; }; |
|
34 this.preFilter = aPreFilter || gSimplePreFilter; |
|
35 } |
|
36 |
|
37 BaseTraversalRule.prototype = { |
|
38 getMatchRoles: function BaseTraversalRule_getmatchRoles(aRules) { |
|
39 aRules.value = this._matchRoles; |
|
40 return aRules.value.length; |
|
41 }, |
|
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 |
|
51 let matchResult = this._explicitMatchRoles.has(role) ? |
|
52 this._matchFunc(aAccessible) : Filters.IGNORE; |
|
53 |
|
54 // If we are on a label that nests a checkbox/radio we should land on it. |
|
55 // It is a bigger touch target, and it reduces clutter. |
|
56 if (role == Roles.LABEL && !(matchResult & Filters.IGNORE_SUBTREE)) { |
|
57 let control = Utils.getEmbeddedControl(aAccessible); |
|
58 if (control && this._explicitMatchRoles.has(control.role)) { |
|
59 matchResult = this._matchFunc(control) | Filters.IGNORE_SUBTREE; |
|
60 } |
|
61 } |
|
62 |
|
63 return matchResult; |
|
64 }, |
|
65 |
|
66 QueryInterface: XPCOMUtils.generateQI([Ci.nsIAccessibleTraversalRule]) |
|
67 }; |
|
68 |
|
69 var gSimpleTraversalRoles = |
|
70 [Roles.MENUITEM, |
|
71 Roles.LINK, |
|
72 Roles.PAGETAB, |
|
73 Roles.GRAPHIC, |
|
74 Roles.STATICTEXT, |
|
75 Roles.TEXT_LEAF, |
|
76 Roles.PUSHBUTTON, |
|
77 Roles.CHECKBUTTON, |
|
78 Roles.RADIOBUTTON, |
|
79 Roles.COMBOBOX, |
|
80 Roles.PROGRESSBAR, |
|
81 Roles.BUTTONDROPDOWN, |
|
82 Roles.BUTTONMENU, |
|
83 Roles.CHECK_MENU_ITEM, |
|
84 Roles.PASSWORD_TEXT, |
|
85 Roles.RADIO_MENU_ITEM, |
|
86 Roles.TOGGLE_BUTTON, |
|
87 Roles.ENTRY, |
|
88 Roles.KEY, |
|
89 Roles.HEADER, |
|
90 Roles.HEADING, |
|
91 Roles.SLIDER, |
|
92 Roles.SPINBUTTON, |
|
93 Roles.OPTION, |
|
94 // Used for traversing in to child OOP frames. |
|
95 Roles.INTERNAL_FRAME]; |
|
96 |
|
97 var gSimpleMatchFunc = function gSimpleMatchFunc(aAccessible) { |
|
98 function hasZeroOrSingleChildDescendants () { |
|
99 for (let acc = aAccessible; acc.childCount > 0; acc = acc.firstChild) { |
|
100 if (acc.childCount > 1) { |
|
101 return false; |
|
102 } |
|
103 } |
|
104 |
|
105 return true; |
|
106 } |
|
107 |
|
108 switch (aAccessible.role) { |
|
109 case Roles.COMBOBOX: |
|
110 // We don't want to ignore the subtree because this is often |
|
111 // where the list box hangs out. |
|
112 return Filters.MATCH; |
|
113 case Roles.TEXT_LEAF: |
|
114 { |
|
115 // Nameless text leaves are boring, skip them. |
|
116 let name = aAccessible.name; |
|
117 if (name && name.trim()) |
|
118 return Filters.MATCH; |
|
119 else |
|
120 return Filters.IGNORE; |
|
121 } |
|
122 case Roles.STATICTEXT: |
|
123 { |
|
124 let parent = aAccessible.parent; |
|
125 // Ignore prefix static text in list items. They are typically bullets or numbers. |
|
126 if (parent.childCount > 1 && aAccessible.indexInParent == 0 && |
|
127 parent.role == Roles.LISTITEM) |
|
128 return Filters.IGNORE; |
|
129 |
|
130 return Filters.MATCH; |
|
131 } |
|
132 case Roles.GRAPHIC: |
|
133 return TraversalRules._shouldSkipImage(aAccessible); |
|
134 case Roles.HEADER: |
|
135 case Roles.HEADING: |
|
136 if ((aAccessible.childCount > 0 || aAccessible.name) && |
|
137 hasZeroOrSingleChildDescendants()) { |
|
138 return Filters.MATCH | Filters.IGNORE_SUBTREE; |
|
139 } else { |
|
140 return Filters.IGNORE; |
|
141 } |
|
142 default: |
|
143 // Ignore the subtree, if there is one. So that we don't land on |
|
144 // the same content that was already presented by its parent. |
|
145 return Filters.MATCH | |
|
146 Filters.IGNORE_SUBTREE; |
|
147 } |
|
148 }; |
|
149 |
|
150 var gSimplePreFilter = Prefilters.DEFUNCT | |
|
151 Prefilters.INVISIBLE | |
|
152 Prefilters.ARIA_HIDDEN | |
|
153 Prefilters.TRANSPARENT; |
|
154 |
|
155 this.TraversalRules = { |
|
156 Simple: new BaseTraversalRule(gSimpleTraversalRoles, gSimpleMatchFunc), |
|
157 |
|
158 SimpleOnScreen: new BaseTraversalRule( |
|
159 gSimpleTraversalRoles, gSimpleMatchFunc, |
|
160 Prefilters.DEFUNCT | Prefilters.INVISIBLE | Prefilters.ARIA_HIDDEN | |
|
161 Prefilters.TRANSPARENT | Prefilters.OFFSCREEN), |
|
162 |
|
163 Anchor: new BaseTraversalRule( |
|
164 [Roles.LINK], |
|
165 function Anchor_match(aAccessible) |
|
166 { |
|
167 // We want to ignore links, only focus named anchors. |
|
168 if (Utils.getState(aAccessible).contains(States.LINKED)) { |
|
169 return Filters.IGNORE; |
|
170 } else { |
|
171 return Filters.MATCH; |
|
172 } |
|
173 }), |
|
174 |
|
175 Button: new BaseTraversalRule( |
|
176 [Roles.PUSHBUTTON, |
|
177 Roles.SPINBUTTON, |
|
178 Roles.TOGGLE_BUTTON, |
|
179 Roles.BUTTONDROPDOWN, |
|
180 Roles.BUTTONDROPDOWNGRID]), |
|
181 |
|
182 Combobox: new BaseTraversalRule( |
|
183 [Roles.COMBOBOX, |
|
184 Roles.LISTBOX]), |
|
185 |
|
186 Landmark: new BaseTraversalRule( |
|
187 [], |
|
188 function Landmark_match(aAccessible) { |
|
189 return Utils.getLandmarkName(aAccessible) ? Filters.MATCH : |
|
190 Filters.IGNORE; |
|
191 } |
|
192 ), |
|
193 |
|
194 Entry: new BaseTraversalRule( |
|
195 [Roles.ENTRY, |
|
196 Roles.PASSWORD_TEXT]), |
|
197 |
|
198 FormElement: new BaseTraversalRule( |
|
199 [Roles.PUSHBUTTON, |
|
200 Roles.SPINBUTTON, |
|
201 Roles.TOGGLE_BUTTON, |
|
202 Roles.BUTTONDROPDOWN, |
|
203 Roles.BUTTONDROPDOWNGRID, |
|
204 Roles.COMBOBOX, |
|
205 Roles.LISTBOX, |
|
206 Roles.ENTRY, |
|
207 Roles.PASSWORD_TEXT, |
|
208 Roles.PAGETAB, |
|
209 Roles.RADIOBUTTON, |
|
210 Roles.RADIO_MENU_ITEM, |
|
211 Roles.SLIDER, |
|
212 Roles.CHECKBUTTON, |
|
213 Roles.CHECK_MENU_ITEM]), |
|
214 |
|
215 Graphic: new BaseTraversalRule( |
|
216 [Roles.GRAPHIC], |
|
217 function Graphic_match(aAccessible) { |
|
218 return TraversalRules._shouldSkipImage(aAccessible); |
|
219 }), |
|
220 |
|
221 Heading: new BaseTraversalRule( |
|
222 [Roles.HEADING], |
|
223 function Heading_match(aAccessible) { |
|
224 return aAccessible.childCount > 0 ? Filters.MATCH : Filters.IGNORE; |
|
225 }), |
|
226 |
|
227 ListItem: new BaseTraversalRule( |
|
228 [Roles.LISTITEM, |
|
229 Roles.TERM]), |
|
230 |
|
231 Link: new BaseTraversalRule( |
|
232 [Roles.LINK], |
|
233 function Link_match(aAccessible) |
|
234 { |
|
235 // We want to ignore anchors, only focus real links. |
|
236 if (Utils.getState(aAccessible).contains(States.LINKED)) { |
|
237 return Filters.MATCH; |
|
238 } else { |
|
239 return Filters.IGNORE; |
|
240 } |
|
241 }), |
|
242 |
|
243 List: new BaseTraversalRule( |
|
244 [Roles.LIST, |
|
245 Roles.DEFINITION_LIST]), |
|
246 |
|
247 PageTab: new BaseTraversalRule( |
|
248 [Roles.PAGETAB]), |
|
249 |
|
250 Paragraph: new BaseTraversalRule( |
|
251 [Roles.PARAGRAPH, |
|
252 Roles.SECTION], |
|
253 function Paragraph_match(aAccessible) { |
|
254 for (let child = aAccessible.firstChild; child; child = child.nextSibling) { |
|
255 if (child.role === Roles.TEXT_LEAF) { |
|
256 return Filters.MATCH | Filters.IGNORE_SUBTREE; |
|
257 } |
|
258 } |
|
259 |
|
260 return Filters.IGNORE; |
|
261 }), |
|
262 |
|
263 RadioButton: new BaseTraversalRule( |
|
264 [Roles.RADIOBUTTON, |
|
265 Roles.RADIO_MENU_ITEM]), |
|
266 |
|
267 Separator: new BaseTraversalRule( |
|
268 [Roles.SEPARATOR]), |
|
269 |
|
270 Table: new BaseTraversalRule( |
|
271 [Roles.TABLE]), |
|
272 |
|
273 Checkbox: new BaseTraversalRule( |
|
274 [Roles.CHECKBUTTON, |
|
275 Roles.CHECK_MENU_ITEM]), |
|
276 |
|
277 _shouldSkipImage: function _shouldSkipImage(aAccessible) { |
|
278 if (gSkipEmptyImages.value && aAccessible.name === '') { |
|
279 return Filters.IGNORE; |
|
280 } |
|
281 return Filters.MATCH; |
|
282 } |
|
283 }; |