зеркало из https://github.com/mozilla/gecko-dev.git
434 строки
11 KiB
JavaScript
434 строки
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() {
|
|
return this._matchRoles;
|
|
},
|
|
|
|
match: function BaseTraversalRule_match(aAccessible) {
|
|
let role = aAccessible.role;
|
|
if (role == Roles.INTERNAL_FRAME) {
|
|
return Utils.getMessageManagerForFrame(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);
|
|
},
|
|
};
|