/* 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/. */ "use strict"; var EXPORTED_SYMBOLS = ["TraversalRules", "TraversalHelper"]; // jshint ignore:line const {PrefCache, Utils} = ChromeUtils.import("resource://gre/modules/accessibility/Utils.jsm"); ChromeUtils.defineModuleGetter(this, "Roles", // jshint ignore:line "resource://gre/modules/accessibility/Constants.jsm"); ChromeUtils.defineModuleGetter(this, "Filters", // jshint ignore:line "resource://gre/modules/accessibility/Constants.jsm"); ChromeUtils.defineModuleGetter(this, "States", // jshint ignore:line "resource://gre/modules/accessibility/Constants.jsm"); ChromeUtils.defineModuleGetter(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.includes(Roles.LABEL)) { this._matchRoles.push(Roles.LABEL); } if (!aRoles.includes(Roles.INTERNAL_FRAME)) { // 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; } if (this._explicitMatchRoles.has(role) || !this._explicitMatchRoles.size) { return this._matchFunc(aAccessible); } return Filters.IGNORE; }, QueryInterface: ChromeUtils.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].includes(child.role)) { 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.TRANSPARENT | Prefilters.PLATFORM_PRUNED; var TraversalRules = { // jshint ignore:line Simple: new BaseTraversalRule(gSimpleTraversalRoles, gSimpleMatchFunc), SimpleOnScreen: new BaseTraversalRule( gSimpleTraversalRoles, gSimpleMatchFunc, gSimplePreFilter | 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; } 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; } 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; }, }; var 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; } return aVirtualCursor[aMethod](rule); }, };