summaryrefslogtreecommitdiffstats
path: root/accessible/jsat/Traversal.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'accessible/jsat/Traversal.jsm')
-rw-r--r--accessible/jsat/Traversal.jsm419
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);
+ }
+ }
+
+};