зеркало из https://github.com/mozilla/gecko-dev.git
396 строки
11 KiB
JavaScript
396 строки
11 KiB
JavaScript
/* 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);
|
|
},
|
|
|
|
};
|