gecko-dev/accessible/jsat/Traversal.jsm

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);
},
};