michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: 'use strict'; michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cu = Components.utils; michael@0: const Cr = Components.results; michael@0: michael@0: this.EXPORTED_SYMBOLS = ['TraversalRules']; michael@0: michael@0: Cu.import('resource://gre/modules/accessibility/Utils.jsm'); michael@0: Cu.import('resource://gre/modules/XPCOMUtils.jsm'); michael@0: XPCOMUtils.defineLazyModuleGetter(this, 'Roles', michael@0: 'resource://gre/modules/accessibility/Constants.jsm'); michael@0: XPCOMUtils.defineLazyModuleGetter(this, 'Filters', michael@0: 'resource://gre/modules/accessibility/Constants.jsm'); michael@0: XPCOMUtils.defineLazyModuleGetter(this, 'States', michael@0: 'resource://gre/modules/accessibility/Constants.jsm'); michael@0: XPCOMUtils.defineLazyModuleGetter(this, 'Prefilters', michael@0: 'resource://gre/modules/accessibility/Constants.jsm'); michael@0: michael@0: let gSkipEmptyImages = new PrefCache('accessibility.accessfu.skip_empty_images'); michael@0: michael@0: function BaseTraversalRule(aRoles, aMatchFunc, aPreFilter) { michael@0: this._explicitMatchRoles = new Set(aRoles); michael@0: this._matchRoles = aRoles; michael@0: if (aRoles.indexOf(Roles.LABEL) < 0) { michael@0: this._matchRoles.push(Roles.LABEL); michael@0: } michael@0: this._matchFunc = aMatchFunc || function (acc) { return Filters.MATCH; }; michael@0: this.preFilter = aPreFilter || gSimplePreFilter; michael@0: } michael@0: michael@0: BaseTraversalRule.prototype = { michael@0: getMatchRoles: function BaseTraversalRule_getmatchRoles(aRules) { michael@0: aRules.value = this._matchRoles; michael@0: return aRules.value.length; michael@0: }, michael@0: michael@0: match: function BaseTraversalRule_match(aAccessible) michael@0: { michael@0: let role = aAccessible.role; michael@0: if (role == Roles.INTERNAL_FRAME) { michael@0: return (Utils.getMessageManager(aAccessible.DOMNode)) ? michael@0: Filters.MATCH | Filters.IGNORE_SUBTREE : Filters.IGNORE; michael@0: } michael@0: michael@0: let matchResult = this._explicitMatchRoles.has(role) ? michael@0: this._matchFunc(aAccessible) : Filters.IGNORE; michael@0: michael@0: // If we are on a label that nests a checkbox/radio we should land on it. michael@0: // It is a bigger touch target, and it reduces clutter. michael@0: if (role == Roles.LABEL && !(matchResult & Filters.IGNORE_SUBTREE)) { michael@0: let control = Utils.getEmbeddedControl(aAccessible); michael@0: if (control && this._explicitMatchRoles.has(control.role)) { michael@0: matchResult = this._matchFunc(control) | Filters.IGNORE_SUBTREE; michael@0: } michael@0: } michael@0: michael@0: return matchResult; michael@0: }, michael@0: michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsIAccessibleTraversalRule]) michael@0: }; michael@0: michael@0: var gSimpleTraversalRoles = michael@0: [Roles.MENUITEM, michael@0: Roles.LINK, michael@0: Roles.PAGETAB, michael@0: Roles.GRAPHIC, michael@0: Roles.STATICTEXT, michael@0: Roles.TEXT_LEAF, michael@0: Roles.PUSHBUTTON, michael@0: Roles.CHECKBUTTON, michael@0: Roles.RADIOBUTTON, michael@0: Roles.COMBOBOX, michael@0: Roles.PROGRESSBAR, michael@0: Roles.BUTTONDROPDOWN, michael@0: Roles.BUTTONMENU, michael@0: Roles.CHECK_MENU_ITEM, michael@0: Roles.PASSWORD_TEXT, michael@0: Roles.RADIO_MENU_ITEM, michael@0: Roles.TOGGLE_BUTTON, michael@0: Roles.ENTRY, michael@0: Roles.KEY, michael@0: Roles.HEADER, michael@0: Roles.HEADING, michael@0: Roles.SLIDER, michael@0: Roles.SPINBUTTON, michael@0: Roles.OPTION, michael@0: // Used for traversing in to child OOP frames. michael@0: Roles.INTERNAL_FRAME]; michael@0: michael@0: var gSimpleMatchFunc = function gSimpleMatchFunc(aAccessible) { michael@0: function hasZeroOrSingleChildDescendants () { michael@0: for (let acc = aAccessible; acc.childCount > 0; acc = acc.firstChild) { michael@0: if (acc.childCount > 1) { michael@0: return false; michael@0: } michael@0: } michael@0: michael@0: return true; michael@0: } michael@0: michael@0: switch (aAccessible.role) { michael@0: case Roles.COMBOBOX: michael@0: // We don't want to ignore the subtree because this is often michael@0: // where the list box hangs out. michael@0: return Filters.MATCH; michael@0: case Roles.TEXT_LEAF: michael@0: { michael@0: // Nameless text leaves are boring, skip them. michael@0: let name = aAccessible.name; michael@0: if (name && name.trim()) michael@0: return Filters.MATCH; michael@0: else michael@0: return Filters.IGNORE; michael@0: } michael@0: case Roles.STATICTEXT: michael@0: { michael@0: let parent = aAccessible.parent; michael@0: // Ignore prefix static text in list items. They are typically bullets or numbers. michael@0: if (parent.childCount > 1 && aAccessible.indexInParent == 0 && michael@0: parent.role == Roles.LISTITEM) michael@0: return Filters.IGNORE; michael@0: michael@0: return Filters.MATCH; michael@0: } michael@0: case Roles.GRAPHIC: michael@0: return TraversalRules._shouldSkipImage(aAccessible); michael@0: case Roles.HEADER: michael@0: case Roles.HEADING: michael@0: if ((aAccessible.childCount > 0 || aAccessible.name) && michael@0: hasZeroOrSingleChildDescendants()) { michael@0: return Filters.MATCH | Filters.IGNORE_SUBTREE; michael@0: } else { michael@0: return Filters.IGNORE; michael@0: } michael@0: default: michael@0: // Ignore the subtree, if there is one. So that we don't land on michael@0: // the same content that was already presented by its parent. michael@0: return Filters.MATCH | michael@0: Filters.IGNORE_SUBTREE; michael@0: } michael@0: }; michael@0: michael@0: var gSimplePreFilter = Prefilters.DEFUNCT | michael@0: Prefilters.INVISIBLE | michael@0: Prefilters.ARIA_HIDDEN | michael@0: Prefilters.TRANSPARENT; michael@0: michael@0: this.TraversalRules = { michael@0: Simple: new BaseTraversalRule(gSimpleTraversalRoles, gSimpleMatchFunc), michael@0: michael@0: SimpleOnScreen: new BaseTraversalRule( michael@0: gSimpleTraversalRoles, gSimpleMatchFunc, michael@0: Prefilters.DEFUNCT | Prefilters.INVISIBLE | Prefilters.ARIA_HIDDEN | michael@0: Prefilters.TRANSPARENT | Prefilters.OFFSCREEN), michael@0: michael@0: Anchor: new BaseTraversalRule( michael@0: [Roles.LINK], michael@0: function Anchor_match(aAccessible) michael@0: { michael@0: // We want to ignore links, only focus named anchors. michael@0: if (Utils.getState(aAccessible).contains(States.LINKED)) { michael@0: return Filters.IGNORE; michael@0: } else { michael@0: return Filters.MATCH; michael@0: } michael@0: }), michael@0: michael@0: Button: new BaseTraversalRule( michael@0: [Roles.PUSHBUTTON, michael@0: Roles.SPINBUTTON, michael@0: Roles.TOGGLE_BUTTON, michael@0: Roles.BUTTONDROPDOWN, michael@0: Roles.BUTTONDROPDOWNGRID]), michael@0: michael@0: Combobox: new BaseTraversalRule( michael@0: [Roles.COMBOBOX, michael@0: Roles.LISTBOX]), michael@0: michael@0: Landmark: new BaseTraversalRule( michael@0: [], michael@0: function Landmark_match(aAccessible) { michael@0: return Utils.getLandmarkName(aAccessible) ? Filters.MATCH : michael@0: Filters.IGNORE; michael@0: } michael@0: ), michael@0: michael@0: Entry: new BaseTraversalRule( michael@0: [Roles.ENTRY, michael@0: Roles.PASSWORD_TEXT]), michael@0: michael@0: FormElement: new BaseTraversalRule( michael@0: [Roles.PUSHBUTTON, michael@0: Roles.SPINBUTTON, michael@0: Roles.TOGGLE_BUTTON, michael@0: Roles.BUTTONDROPDOWN, michael@0: Roles.BUTTONDROPDOWNGRID, michael@0: Roles.COMBOBOX, michael@0: Roles.LISTBOX, michael@0: Roles.ENTRY, michael@0: Roles.PASSWORD_TEXT, michael@0: Roles.PAGETAB, michael@0: Roles.RADIOBUTTON, michael@0: Roles.RADIO_MENU_ITEM, michael@0: Roles.SLIDER, michael@0: Roles.CHECKBUTTON, michael@0: Roles.CHECK_MENU_ITEM]), michael@0: michael@0: Graphic: new BaseTraversalRule( michael@0: [Roles.GRAPHIC], michael@0: function Graphic_match(aAccessible) { michael@0: return TraversalRules._shouldSkipImage(aAccessible); michael@0: }), michael@0: michael@0: Heading: new BaseTraversalRule( michael@0: [Roles.HEADING], michael@0: function Heading_match(aAccessible) { michael@0: return aAccessible.childCount > 0 ? Filters.MATCH : Filters.IGNORE; michael@0: }), michael@0: michael@0: ListItem: new BaseTraversalRule( michael@0: [Roles.LISTITEM, michael@0: Roles.TERM]), michael@0: michael@0: Link: new BaseTraversalRule( michael@0: [Roles.LINK], michael@0: function Link_match(aAccessible) michael@0: { michael@0: // We want to ignore anchors, only focus real links. michael@0: if (Utils.getState(aAccessible).contains(States.LINKED)) { michael@0: return Filters.MATCH; michael@0: } else { michael@0: return Filters.IGNORE; michael@0: } michael@0: }), michael@0: michael@0: List: new BaseTraversalRule( michael@0: [Roles.LIST, michael@0: Roles.DEFINITION_LIST]), michael@0: michael@0: PageTab: new BaseTraversalRule( michael@0: [Roles.PAGETAB]), michael@0: michael@0: Paragraph: new BaseTraversalRule( michael@0: [Roles.PARAGRAPH, michael@0: Roles.SECTION], michael@0: function Paragraph_match(aAccessible) { michael@0: for (let child = aAccessible.firstChild; child; child = child.nextSibling) { michael@0: if (child.role === Roles.TEXT_LEAF) { michael@0: return Filters.MATCH | Filters.IGNORE_SUBTREE; michael@0: } michael@0: } michael@0: michael@0: return Filters.IGNORE; michael@0: }), michael@0: michael@0: RadioButton: new BaseTraversalRule( michael@0: [Roles.RADIOBUTTON, michael@0: Roles.RADIO_MENU_ITEM]), michael@0: michael@0: Separator: new BaseTraversalRule( michael@0: [Roles.SEPARATOR]), michael@0: michael@0: Table: new BaseTraversalRule( michael@0: [Roles.TABLE]), michael@0: michael@0: Checkbox: new BaseTraversalRule( michael@0: [Roles.CHECKBUTTON, michael@0: Roles.CHECK_MENU_ITEM]), michael@0: michael@0: _shouldSkipImage: function _shouldSkipImage(aAccessible) { michael@0: if (gSkipEmptyImages.value && aAccessible.name === '') { michael@0: return Filters.IGNORE; michael@0: } michael@0: return Filters.MATCH; michael@0: } michael@0: };