diff options
Diffstat (limited to 'accessible/jsat/Traversal.jsm')
-rw-r--r-- | accessible/jsat/Traversal.jsm | 419 |
1 files changed, 419 insertions, 0 deletions
diff --git a/accessible/jsat/Traversal.jsm b/accessible/jsat/Traversal.jsm new file mode 100644 index 000000000..5b3bbdf89 --- /dev/null +++ b/accessible/jsat/Traversal.jsm @@ -0,0 +1,419 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* global PrefCache, Roles, Prefilters, States, Filters, Utils, + TraversalRules, Components, XPCOMUtils */ +/* exported TraversalRules, TraversalHelper */ + +'use strict'; + +const Ci = Components.interfaces; +const Cu = Components.utils; + +this.EXPORTED_SYMBOLS = ['TraversalRules', 'TraversalHelper']; // jshint ignore:line + +Cu.import('resource://gre/modules/accessibility/Utils.jsm'); +Cu.import('resource://gre/modules/XPCOMUtils.jsm'); +XPCOMUtils.defineLazyModuleGetter(this, 'Roles', // jshint ignore:line + 'resource://gre/modules/accessibility/Constants.jsm'); +XPCOMUtils.defineLazyModuleGetter(this, 'Filters', // jshint ignore:line + 'resource://gre/modules/accessibility/Constants.jsm'); +XPCOMUtils.defineLazyModuleGetter(this, 'States', // jshint ignore:line + 'resource://gre/modules/accessibility/Constants.jsm'); +XPCOMUtils.defineLazyModuleGetter(this, 'Prefilters', // jshint ignore:line + 'resource://gre/modules/accessibility/Constants.jsm'); + +var gSkipEmptyImages = new PrefCache('accessibility.accessfu.skip_empty_images'); + +function BaseTraversalRule(aRoles, aMatchFunc, aPreFilter, aContainerRule) { + this._explicitMatchRoles = new Set(aRoles); + this._matchRoles = aRoles; + if (aRoles.length) { + if (aRoles.indexOf(Roles.LABEL) < 0) { + this._matchRoles.push(Roles.LABEL); + } + if (aRoles.indexOf(Roles.INTERNAL_FRAME) < 0) { + // Used for traversing in to child OOP frames. + this._matchRoles.push(Roles.INTERNAL_FRAME); + } + } + this._matchFunc = aMatchFunc || function() { return Filters.MATCH; }; + this.preFilter = aPreFilter || gSimplePreFilter; + this.containerRule = aContainerRule; +} + +BaseTraversalRule.prototype = { + getMatchRoles: function BaseTraversalRule_getmatchRoles(aRoles) { + aRoles.value = this._matchRoles; + return aRoles.value.length; + }, + + match: function BaseTraversalRule_match(aAccessible) + { + let role = aAccessible.role; + if (role == Roles.INTERNAL_FRAME) { + return (Utils.getMessageManager(aAccessible.DOMNode)) ? + Filters.MATCH | Filters.IGNORE_SUBTREE : Filters.IGNORE; + } + + let matchResult = + (this._explicitMatchRoles.has(role) || !this._explicitMatchRoles.size) ? + this._matchFunc(aAccessible) : Filters.IGNORE; + + // If we are on a label that nests a checkbox/radio we should land on it. + // It is a bigger touch target, and it reduces clutter. + if (role == Roles.LABEL && !(matchResult & Filters.IGNORE_SUBTREE)) { + let control = Utils.getEmbeddedControl(aAccessible); + if (control && this._explicitMatchRoles.has(control.role)) { + matchResult = this._matchFunc(control) | Filters.IGNORE_SUBTREE; + } + } + + return matchResult; + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIAccessibleTraversalRule]) +}; + +var gSimpleTraversalRoles = + [Roles.MENUITEM, + Roles.LINK, + Roles.PAGETAB, + Roles.GRAPHIC, + Roles.STATICTEXT, + Roles.TEXT_LEAF, + Roles.PUSHBUTTON, + Roles.CHECKBUTTON, + Roles.RADIOBUTTON, + Roles.COMBOBOX, + Roles.PROGRESSBAR, + Roles.BUTTONDROPDOWN, + Roles.BUTTONMENU, + Roles.CHECK_MENU_ITEM, + Roles.PASSWORD_TEXT, + Roles.RADIO_MENU_ITEM, + Roles.TOGGLE_BUTTON, + Roles.ENTRY, + Roles.KEY, + Roles.HEADER, + Roles.HEADING, + Roles.SLIDER, + Roles.SPINBUTTON, + Roles.OPTION, + Roles.LISTITEM, + Roles.GRID_CELL, + Roles.COLUMNHEADER, + Roles.ROWHEADER, + Roles.STATUSBAR, + Roles.SWITCH, + Roles.MATHML_MATH]; + +var gSimpleMatchFunc = function gSimpleMatchFunc(aAccessible) { + // An object is simple, if it either has a single child lineage, + // or has a flat subtree. + function isSingleLineage(acc) { + for (let child = acc; child; child = child.firstChild) { + if (Utils.visibleChildCount(child) > 1) { + return false; + } + } + return true; + } + + function isFlatSubtree(acc) { + for (let child = acc.firstChild; child; child = child.nextSibling) { + // text leafs inherit the actionCount of any ancestor that has a click + // listener. + if ([Roles.TEXT_LEAF, Roles.STATICTEXT].indexOf(child.role) >= 0) { + continue; + } + if (Utils.visibleChildCount(child) > 0 || child.actionCount > 0) { + return false; + } + } + return true; + } + + switch (aAccessible.role) { + case Roles.COMBOBOX: + // We don't want to ignore the subtree because this is often + // where the list box hangs out. + return Filters.MATCH; + case Roles.TEXT_LEAF: + { + // Nameless text leaves are boring, skip them. + let name = aAccessible.name; + return (name && name.trim()) ? Filters.MATCH : Filters.IGNORE; + } + case Roles.STATICTEXT: + // Ignore prefix static text in list items. They are typically bullets or numbers. + return Utils.isListItemDecorator(aAccessible) ? + Filters.IGNORE : Filters.MATCH; + case Roles.GRAPHIC: + return TraversalRules._shouldSkipImage(aAccessible); + case Roles.HEADER: + case Roles.HEADING: + case Roles.COLUMNHEADER: + case Roles.ROWHEADER: + case Roles.STATUSBAR: + if ((aAccessible.childCount > 0 || aAccessible.name) && + (isSingleLineage(aAccessible) || isFlatSubtree(aAccessible))) { + return Filters.MATCH | Filters.IGNORE_SUBTREE; + } + return Filters.IGNORE; + case Roles.GRID_CELL: + return isSingleLineage(aAccessible) || isFlatSubtree(aAccessible) ? + Filters.MATCH | Filters.IGNORE_SUBTREE : Filters.IGNORE; + case Roles.LISTITEM: + { + let item = aAccessible.childCount === 2 && + aAccessible.firstChild.role === Roles.STATICTEXT ? + aAccessible.lastChild : aAccessible; + return isSingleLineage(item) || isFlatSubtree(item) ? + Filters.MATCH | Filters.IGNORE_SUBTREE : Filters.IGNORE; + } + default: + // Ignore the subtree, if there is one. So that we don't land on + // the same content that was already presented by its parent. + return Filters.MATCH | + Filters.IGNORE_SUBTREE; + } +}; + +var gSimplePreFilter = Prefilters.DEFUNCT | + Prefilters.INVISIBLE | + Prefilters.ARIA_HIDDEN | + Prefilters.TRANSPARENT; + +this.TraversalRules = { // jshint ignore:line + Simple: new BaseTraversalRule(gSimpleTraversalRoles, gSimpleMatchFunc), + + SimpleOnScreen: new BaseTraversalRule( + gSimpleTraversalRoles, gSimpleMatchFunc, + Prefilters.DEFUNCT | Prefilters.INVISIBLE | Prefilters.ARIA_HIDDEN | + Prefilters.TRANSPARENT | Prefilters.OFFSCREEN), + + Anchor: new BaseTraversalRule( + [Roles.LINK], + function Anchor_match(aAccessible) + { + // We want to ignore links, only focus named anchors. + if (Utils.getState(aAccessible).contains(States.LINKED)) { + return Filters.IGNORE; + } else { + return Filters.MATCH; + } + }), + + Button: new BaseTraversalRule( + [Roles.PUSHBUTTON, + Roles.SPINBUTTON, + Roles.TOGGLE_BUTTON, + Roles.BUTTONDROPDOWN, + Roles.BUTTONDROPDOWNGRID]), + + Combobox: new BaseTraversalRule( + [Roles.COMBOBOX, + Roles.LISTBOX]), + + Landmark: new BaseTraversalRule( + [], + function Landmark_match(aAccessible) { + return Utils.getLandmarkName(aAccessible) ? Filters.MATCH : + Filters.IGNORE; + }, null, true), + + /* A rule for Android's section navigation, lands on landmarks, regions, and + on headings to aid navigation of traditionally structured documents */ + Section: new BaseTraversalRule( + [], + function Section_match(aAccessible) { + if (aAccessible.role === Roles.HEADING) { + return Filters.MATCH; + } + + let matchedRole = Utils.matchRoles(aAccessible, [ + 'banner', + 'complementary', + 'contentinfo', + 'main', + 'navigation', + 'search', + 'region' + ]); + + return matchedRole ? Filters.MATCH : Filters.IGNORE; + }, null, true), + + Entry: new BaseTraversalRule( + [Roles.ENTRY, + Roles.PASSWORD_TEXT]), + + FormElement: new BaseTraversalRule( + [Roles.PUSHBUTTON, + Roles.SPINBUTTON, + Roles.TOGGLE_BUTTON, + Roles.BUTTONDROPDOWN, + Roles.BUTTONDROPDOWNGRID, + Roles.COMBOBOX, + Roles.LISTBOX, + Roles.ENTRY, + Roles.PASSWORD_TEXT, + Roles.PAGETAB, + Roles.RADIOBUTTON, + Roles.RADIO_MENU_ITEM, + Roles.SLIDER, + Roles.CHECKBUTTON, + Roles.CHECK_MENU_ITEM, + Roles.SWITCH]), + + Graphic: new BaseTraversalRule( + [Roles.GRAPHIC], + function Graphic_match(aAccessible) { + return TraversalRules._shouldSkipImage(aAccessible); + }), + + Heading: new BaseTraversalRule( + [Roles.HEADING], + function Heading_match(aAccessible) { + return aAccessible.childCount > 0 ? Filters.MATCH : Filters.IGNORE; + }), + + ListItem: new BaseTraversalRule( + [Roles.LISTITEM, + Roles.TERM]), + + Link: new BaseTraversalRule( + [Roles.LINK], + function Link_match(aAccessible) + { + // We want to ignore anchors, only focus real links. + if (Utils.getState(aAccessible).contains(States.LINKED)) { + return Filters.MATCH; + } else { + return Filters.IGNORE; + } + }), + + /* For TalkBack's "Control" granularity. Form conrols and links */ + Control: new BaseTraversalRule( + [Roles.PUSHBUTTON, + Roles.SPINBUTTON, + Roles.TOGGLE_BUTTON, + Roles.BUTTONDROPDOWN, + Roles.BUTTONDROPDOWNGRID, + Roles.COMBOBOX, + Roles.LISTBOX, + Roles.ENTRY, + Roles.PASSWORD_TEXT, + Roles.PAGETAB, + Roles.RADIOBUTTON, + Roles.RADIO_MENU_ITEM, + Roles.SLIDER, + Roles.CHECKBUTTON, + Roles.CHECK_MENU_ITEM, + Roles.SWITCH, + Roles.LINK, + Roles.MENUITEM], + function Control_match(aAccessible) + { + // We want to ignore anchors, only focus real links. + if (aAccessible.role == Roles.LINK && + !Utils.getState(aAccessible).contains(States.LINKED)) { + return Filters.IGNORE; + } + return Filters.MATCH; + }), + + List: new BaseTraversalRule( + [Roles.LIST, + Roles.DEFINITION_LIST], + null, null, true), + + PageTab: new BaseTraversalRule( + [Roles.PAGETAB]), + + Paragraph: new BaseTraversalRule( + [Roles.PARAGRAPH, + Roles.SECTION], + function Paragraph_match(aAccessible) { + for (let child = aAccessible.firstChild; child; child = child.nextSibling) { + if (child.role === Roles.TEXT_LEAF) { + return Filters.MATCH | Filters.IGNORE_SUBTREE; + } + } + + return Filters.IGNORE; + }), + + RadioButton: new BaseTraversalRule( + [Roles.RADIOBUTTON, + Roles.RADIO_MENU_ITEM]), + + Separator: new BaseTraversalRule( + [Roles.SEPARATOR]), + + Table: new BaseTraversalRule( + [Roles.TABLE]), + + Checkbox: new BaseTraversalRule( + [Roles.CHECKBUTTON, + Roles.CHECK_MENU_ITEM, + Roles.SWITCH /* A type of checkbox that represents on/off values */]), + + _shouldSkipImage: function _shouldSkipImage(aAccessible) { + if (gSkipEmptyImages.value && aAccessible.name === '') { + return Filters.IGNORE; + } + return Filters.MATCH; + } +}; + +this.TraversalHelper = { + _helperPivotCache: null, + + get helperPivotCache() { + delete this.helperPivotCache; + this.helperPivotCache = new WeakMap(); + return this.helperPivotCache; + }, + + getHelperPivot: function TraversalHelper_getHelperPivot(aRoot) { + let pivot = this.helperPivotCache.get(aRoot.DOMNode); + if (!pivot) { + pivot = Utils.AccService.createAccessiblePivot(aRoot); + this.helperPivotCache.set(aRoot.DOMNode, pivot); + } + + return pivot; + }, + + move: function TraversalHelper_move(aVirtualCursor, aMethod, aRule) { + let rule = TraversalRules[aRule]; + + if (rule.containerRule) { + let moved = false; + let helperPivot = this.getHelperPivot(aVirtualCursor.root); + helperPivot.position = aVirtualCursor.position; + + // We continue to step through containers until there is one with an + // atomic child (via 'Simple') on which we could land. + while (!moved) { + if (helperPivot[aMethod](rule)) { + aVirtualCursor.modalRoot = helperPivot.position; + moved = aVirtualCursor.moveFirst(TraversalRules.Simple); + aVirtualCursor.modalRoot = null; + } else { + // If we failed to step to another container, break and return false. + break; + } + } + + return moved; + } else { + return aVirtualCursor[aMethod](rule); + } + } + +}; |