575 lines
18 KiB
JavaScript
575 lines
18 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";
|
|
|
|
loader.lazyRequireGetter(
|
|
this,
|
|
"CombinedProgress",
|
|
"resource://devtools/client/accessibility/utils/audit.js",
|
|
true
|
|
);
|
|
|
|
const {
|
|
accessibility: { AUDIT_TYPE },
|
|
} = require("resource://devtools/shared/constants.js");
|
|
const {
|
|
FILTERS,
|
|
} = require("resource://devtools/client/accessibility/constants.js");
|
|
|
|
/**
|
|
* Component responsible for tracking all Accessibility fronts in parent and
|
|
* content processes.
|
|
*/
|
|
class AccessibilityProxy {
|
|
#panel;
|
|
#initialized;
|
|
constructor(commands, panel) {
|
|
this.commands = commands;
|
|
this.#panel = panel;
|
|
|
|
this.#initialized = false;
|
|
this._accessibilityWalkerFronts = new Set();
|
|
this.lifecycleEvents = new Map();
|
|
this.accessibilityEvents = new Map();
|
|
|
|
this.audit = this.audit.bind(this);
|
|
this.enableAccessibility = this.enableAccessibility.bind(this);
|
|
this.getAccessibilityTreeRoot = this.getAccessibilityTreeRoot.bind(this);
|
|
this.resetAccessiblity = this.resetAccessiblity.bind(this);
|
|
this.startListeningForAccessibilityEvents =
|
|
this.startListeningForAccessibilityEvents.bind(this);
|
|
this.startListeningForLifecycleEvents =
|
|
this.startListeningForLifecycleEvents.bind(this);
|
|
this.startListeningForParentLifecycleEvents =
|
|
this.startListeningForParentLifecycleEvents.bind(this);
|
|
this.stopListeningForAccessibilityEvents =
|
|
this.stopListeningForAccessibilityEvents.bind(this);
|
|
this.stopListeningForLifecycleEvents =
|
|
this.stopListeningForLifecycleEvents.bind(this);
|
|
this.stopListeningForParentLifecycleEvents =
|
|
this.stopListeningForParentLifecycleEvents.bind(this);
|
|
this.highlightAccessible = this.highlightAccessible.bind(this);
|
|
this.unhighlightAccessible = this.unhighlightAccessible.bind(this);
|
|
this.onTargetAvailable = this.onTargetAvailable.bind(this);
|
|
this.onTargetDestroyed = this.onTargetDestroyed.bind(this);
|
|
this.onTargetSelected = this.onTargetSelected.bind(this);
|
|
this.onAccessibilityFrontAvailable =
|
|
this.onAccessibilityFrontAvailable.bind(this);
|
|
this.onAccessibilityFrontDestroyed =
|
|
this.onAccessibilityFrontDestroyed.bind(this);
|
|
this.onAccessibleWalkerFrontAvailable =
|
|
this.onAccessibleWalkerFrontAvailable.bind(this);
|
|
this.onAccessibleWalkerFrontDestroyed =
|
|
this.onAccessibleWalkerFrontDestroyed.bind(this);
|
|
this.unhighlightBeforeCalling = this.unhighlightBeforeCalling.bind(this);
|
|
this.toggleDisplayTabbingOrder = this.toggleDisplayTabbingOrder.bind(this);
|
|
}
|
|
|
|
get enabled() {
|
|
return this.accessibilityFront && this.accessibilityFront.enabled;
|
|
}
|
|
|
|
/**
|
|
* Indicates whether the accessibility service is enabled.
|
|
*/
|
|
get canBeEnabled() {
|
|
return this.parentAccessibilityFront.canBeEnabled;
|
|
}
|
|
|
|
get currentTarget() {
|
|
return this.commands.targetCommand.selectedTargetFront;
|
|
}
|
|
|
|
/**
|
|
* Perform an audit for a given filter.
|
|
*
|
|
* @param {String} filter
|
|
* Type of an audit to perform.
|
|
* @param {Function} onProgress
|
|
* Audit progress callback.
|
|
*
|
|
* @return {Promise}
|
|
* Resolves when the audit for every document, that each of the frame
|
|
* accessibility walkers traverse, completes.
|
|
*/
|
|
async audit(filter, onProgress) {
|
|
const types = filter === FILTERS.ALL ? Object.values(AUDIT_TYPE) : [filter];
|
|
|
|
const targetTypes = [this.commands.targetCommand.TYPES.FRAME];
|
|
const targets =
|
|
await this.commands.targetCommand.getAllTargetsInSelectedTargetTree(
|
|
targetTypes
|
|
);
|
|
|
|
const progress = new CombinedProgress({
|
|
onProgress,
|
|
totalFrames: targets.length,
|
|
});
|
|
const audits = await this.withAllAccessibilityWalkerFronts(
|
|
async accessibleWalkerFront =>
|
|
accessibleWalkerFront.audit({
|
|
types,
|
|
onProgress: progress.onProgressForWalker.bind(
|
|
progress,
|
|
accessibleWalkerFront
|
|
),
|
|
// If a frame was selected in the iframe picker, we don't want to retrieve the
|
|
// ancestries at it would mess with the tree structure and would make it misbehave.
|
|
retrieveAncestries:
|
|
this.commands.targetCommand.isTopLevelTargetSelected(),
|
|
})
|
|
);
|
|
|
|
// Accumulate all audits into a single structure.
|
|
const combinedAudit = { ancestries: [] };
|
|
for (const audit of audits) {
|
|
// If any of the audits resulted in an error, no need to continue.
|
|
if (audit.error) {
|
|
return audit;
|
|
}
|
|
|
|
combinedAudit.ancestries.push(...audit.ancestries);
|
|
}
|
|
|
|
return combinedAudit;
|
|
}
|
|
|
|
async toggleDisplayTabbingOrder(displayTabbingOrder) {
|
|
if (displayTabbingOrder) {
|
|
const { walker: domWalkerFront } =
|
|
await this.currentTarget.getFront("inspector");
|
|
await this.accessibilityFront.accessibleWalkerFront.showTabbingOrder(
|
|
await domWalkerFront.getRootNode(),
|
|
0
|
|
);
|
|
} else {
|
|
// we don't want to use withAllAccessibilityWalkerFronts as it only acts on selected
|
|
// target tree, and we want to hide _all_ highlighters.
|
|
const accessibilityFronts =
|
|
await this.commands.targetCommand.getAllFronts(
|
|
[this.commands.targetCommand.TYPES.FRAME],
|
|
"accessibility"
|
|
);
|
|
await Promise.all(
|
|
accessibilityFronts.map(accessibilityFront =>
|
|
accessibilityFront.accessibleWalkerFront.hideTabbingOrder()
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
async enableAccessibility() {
|
|
// Accessibility service is initialized using the parent accessibility
|
|
// front. That, in turn, initializes accessibility service in all content
|
|
// processes. We need to wait until that happens to be sure platform
|
|
// accessibility is fully enabled.
|
|
const enabled = this.accessibilityFront.once("init");
|
|
await this.parentAccessibilityFront.enable();
|
|
await enabled;
|
|
}
|
|
|
|
/**
|
|
* Return the topmost level accessibility walker to be used as the root of
|
|
* the accessibility tree view.
|
|
*
|
|
* @return {Object}
|
|
* Topmost accessibility walker.
|
|
*/
|
|
getAccessibilityTreeRoot() {
|
|
return this.accessibilityFront.accessibleWalkerFront;
|
|
}
|
|
|
|
/**
|
|
* Look up accessibility fronts (get an existing one or create a new one) for
|
|
* all existing target fronts and run a task with each one of them.
|
|
* @param {Function} task
|
|
* Function to execute with each accessiblity front.
|
|
*/
|
|
async withAllAccessibilityFronts(taskFn) {
|
|
const accessibilityFronts = await this.commands.targetCommand.getAllFronts(
|
|
[this.commands.targetCommand.TYPES.FRAME],
|
|
"accessibility",
|
|
{
|
|
// only get the fronts for the selected frame tree, in case a specific document
|
|
// is selected in the iframe picker (if not, the top-level target is considered
|
|
// as the selected target)
|
|
onlyInSelectedTargetTree: true,
|
|
}
|
|
);
|
|
const tasks = [];
|
|
for (const accessibilityFront of accessibilityFronts) {
|
|
tasks.push(taskFn(accessibilityFront));
|
|
}
|
|
|
|
return Promise.all(tasks);
|
|
}
|
|
|
|
/**
|
|
* Look up accessibility walker fronts (get an existing one or create a new
|
|
* one using accessibility front) for all existing target fronts and run a
|
|
* task with each one of them.
|
|
* @param {Function} task
|
|
* Function to execute with each accessiblity walker front.
|
|
*/
|
|
withAllAccessibilityWalkerFronts(taskFn) {
|
|
return this.withAllAccessibilityFronts(async accessibilityFront =>
|
|
taskFn(accessibilityFront.accessibleWalkerFront)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Unhighlight previous accessible object if we switched between processes and
|
|
* call the appropriate event handler.
|
|
*/
|
|
unhighlightBeforeCalling(listener) {
|
|
return async accessible => {
|
|
if (accessible) {
|
|
const accessibleWalkerFront = accessible.getParent();
|
|
if (this._currentAccessibleWalkerFront !== accessibleWalkerFront) {
|
|
if (this._currentAccessibleWalkerFront) {
|
|
await this._currentAccessibleWalkerFront.unhighlight();
|
|
}
|
|
|
|
this._currentAccessibleWalkerFront = accessibleWalkerFront;
|
|
}
|
|
}
|
|
|
|
await listener(accessible);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Start picking and add walker listeners.
|
|
* @param {Boolean} doFocus
|
|
* If true, move keyboard focus into content.
|
|
*/
|
|
pick(doFocus, onHovered, onPicked, onPreviewed, onCanceled) {
|
|
return this.withAllAccessibilityWalkerFronts(
|
|
async accessibleWalkerFront => {
|
|
this.startListening(accessibleWalkerFront, {
|
|
events: {
|
|
"picker-accessible-hovered":
|
|
this.unhighlightBeforeCalling(onHovered),
|
|
"picker-accessible-picked": this.unhighlightBeforeCalling(onPicked),
|
|
"picker-accessible-previewed":
|
|
this.unhighlightBeforeCalling(onPreviewed),
|
|
"picker-accessible-canceled":
|
|
this.unhighlightBeforeCalling(onCanceled),
|
|
},
|
|
// Only register listeners once (for top level), no need to register
|
|
// them for all walkers again and again.
|
|
register: accessibleWalkerFront.targetFront.isTopLevel,
|
|
});
|
|
await accessibleWalkerFront.pick(
|
|
// Only pass doFocus to the top level accessibility walker front.
|
|
doFocus && accessibleWalkerFront.targetFront.isTopLevel
|
|
);
|
|
}
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Stop picking and remove all walker listeners.
|
|
*/
|
|
async cancelPick() {
|
|
this._currentAccessibleWalkerFront = null;
|
|
return this.withAllAccessibilityWalkerFronts(
|
|
async accessibleWalkerFront => {
|
|
await accessibleWalkerFront.cancelPick();
|
|
this.stopListening(accessibleWalkerFront, {
|
|
events: {
|
|
"picker-accessible-hovered": null,
|
|
"picker-accessible-picked": null,
|
|
"picker-accessible-previewed": null,
|
|
"picker-accessible-canceled": null,
|
|
},
|
|
// Only unregister listeners once (for top level), no need to
|
|
// unregister them for all walkers again and again.
|
|
unregister: accessibleWalkerFront.targetFront.isTopLevel,
|
|
});
|
|
}
|
|
);
|
|
}
|
|
|
|
async resetAccessiblity() {
|
|
const { enabled } = this.accessibilityFront;
|
|
const { canBeEnabled, canBeDisabled } = this.parentAccessibilityFront;
|
|
return { enabled, canBeDisabled, canBeEnabled };
|
|
}
|
|
|
|
startListening(front, { events, register = false } = {}) {
|
|
for (const [type, listener] of Object.entries(events)) {
|
|
front.on(type, listener);
|
|
if (register) {
|
|
this.registerEvent(front, type, listener);
|
|
}
|
|
}
|
|
}
|
|
|
|
stopListening(front, { events, unregister = false } = {}) {
|
|
for (const [type, listener] of Object.entries(events)) {
|
|
front.off(type, listener);
|
|
if (unregister) {
|
|
this.unregisterEvent(front, type, listener);
|
|
}
|
|
}
|
|
}
|
|
|
|
startListeningForAccessibilityEvents(events) {
|
|
for (const accessibleWalkerFront of this._accessibilityWalkerFronts.values()) {
|
|
this.startListening(accessibleWalkerFront, {
|
|
events,
|
|
// Only register listeners once (for top level), no need to register
|
|
// them for all walkers again and again.
|
|
register: accessibleWalkerFront.targetFront.isTopLevel,
|
|
});
|
|
}
|
|
}
|
|
|
|
stopListeningForAccessibilityEvents(events) {
|
|
for (const accessibleWalkerFront of this._accessibilityWalkerFronts.values()) {
|
|
this.stopListening(accessibleWalkerFront, {
|
|
events,
|
|
// Only unregister listeners once (for top level), no need to unregister
|
|
// them for all walkers again and again.
|
|
unregister: accessibleWalkerFront.targetFront.isTopLevel,
|
|
});
|
|
}
|
|
}
|
|
|
|
startListeningForLifecycleEvents(events) {
|
|
this.startListening(this.accessibilityFront, { events, register: true });
|
|
}
|
|
|
|
stopListeningForLifecycleEvents(events) {
|
|
this.stopListening(this.accessibilityFront, { events, unregister: true });
|
|
}
|
|
|
|
startListeningForParentLifecycleEvents(events) {
|
|
this.startListening(this.parentAccessibilityFront, {
|
|
events,
|
|
register: false,
|
|
});
|
|
}
|
|
|
|
stopListeningForParentLifecycleEvents(events) {
|
|
this.stopListening(this.parentAccessibilityFront, {
|
|
events,
|
|
unregister: false,
|
|
});
|
|
}
|
|
|
|
highlightAccessible(accessibleFront, options) {
|
|
if (!accessibleFront) {
|
|
return;
|
|
}
|
|
|
|
const accessibleWalkerFront = accessibleFront.getParent();
|
|
if (!accessibleWalkerFront) {
|
|
return;
|
|
}
|
|
|
|
accessibleWalkerFront
|
|
.highlightAccessible(accessibleFront, options)
|
|
.catch(error => {
|
|
// Only report an error where there's still a commands instance.
|
|
// Ignore cases where toolbox is already destroyed.
|
|
if (this.commands) {
|
|
console.error(error);
|
|
}
|
|
});
|
|
}
|
|
|
|
unhighlightAccessible(accessibleFront) {
|
|
if (!accessibleFront) {
|
|
return;
|
|
}
|
|
|
|
const accessibleWalkerFront = accessibleFront.getParent();
|
|
if (!accessibleWalkerFront) {
|
|
return;
|
|
}
|
|
|
|
accessibleWalkerFront.unhighlight().catch(error => {
|
|
// Only report an error where there's still a commands instance.
|
|
// Ignore cases where toolbox is already destroyed.
|
|
if (this.commands) {
|
|
console.error(error);
|
|
}
|
|
});
|
|
}
|
|
|
|
async initialize() {
|
|
// Initialize it first as it may be used on target selection when calling watchTargets
|
|
this.parentAccessibilityFront =
|
|
await this.commands.targetCommand.rootFront.getFront(
|
|
"parentaccessibility"
|
|
);
|
|
|
|
await this.commands.targetCommand.watchTargets({
|
|
types: [this.commands.targetCommand.TYPES.FRAME],
|
|
onAvailable: this.onTargetAvailable,
|
|
onSelected: this.onTargetSelected,
|
|
onDestroyed: this.onTargetDestroyed,
|
|
});
|
|
|
|
// Enable accessibility service if necessary.
|
|
if (this.canBeEnabled && !this.enabled) {
|
|
await this.enableAccessibility();
|
|
}
|
|
this.#initialized = true;
|
|
}
|
|
|
|
get supports() {
|
|
// Retrieve backward compatibility traits.
|
|
// New API's must be described in the "getTraits" method of the AccessibilityActor.
|
|
return this.accessibilityFront.traits;
|
|
}
|
|
|
|
destroy() {
|
|
this.commands.targetCommand.unwatchTargets({
|
|
types: [this.commands.targetCommand.TYPES.FRAME],
|
|
onAvailable: this.onTargetAvailable,
|
|
onSelected: this.onTargetSelected,
|
|
onDestroyed: this.onTargetDestroyed,
|
|
});
|
|
|
|
this.lifecycleEvents.clear();
|
|
this.accessibilityEvents.clear();
|
|
|
|
this.accessibilityFront = null;
|
|
this.parentAccessibilityFront = null;
|
|
this.simulatorFront = null;
|
|
this.simulate = null;
|
|
this.commands = null;
|
|
}
|
|
|
|
_getEvents(front) {
|
|
return front.typeName === "accessiblewalker"
|
|
? this.accessibilityEvents
|
|
: this.lifecycleEvents;
|
|
}
|
|
|
|
registerEvent(front, type, listener) {
|
|
const events = this._getEvents(front);
|
|
if (events.has(type)) {
|
|
events.get(type).add(listener);
|
|
} else {
|
|
events.set(type, new Set([listener]));
|
|
}
|
|
}
|
|
|
|
unregisterEvent(front, type, listener) {
|
|
const events = this._getEvents(front);
|
|
if (!events.has(type)) {
|
|
return;
|
|
}
|
|
|
|
if (!listener) {
|
|
events.delete(type);
|
|
return;
|
|
}
|
|
|
|
const listeners = events.get(type);
|
|
if (listeners.has(listener)) {
|
|
listeners.delete(listener);
|
|
}
|
|
|
|
if (!listeners.size) {
|
|
events.delete(type);
|
|
}
|
|
}
|
|
|
|
onAccessibilityFrontAvailable(accessibilityFront) {
|
|
accessibilityFront.watchFronts(
|
|
"accessiblewalker",
|
|
this.onAccessibleWalkerFrontAvailable,
|
|
this.onAccessibleWalkerFrontDestroyed
|
|
);
|
|
}
|
|
|
|
onAccessibilityFrontDestroyed(accessibilityFront) {
|
|
accessibilityFront.unwatchFronts(
|
|
"accessiblewalker",
|
|
this.onAccessibleWalkerFrontAvailable,
|
|
this.onAccessibleWalkerFrontDestroyed
|
|
);
|
|
}
|
|
|
|
onAccessibleWalkerFrontAvailable(accessibleWalkerFront) {
|
|
this._accessibilityWalkerFronts.add(accessibleWalkerFront);
|
|
// Apply all existing accessible walker front event listeners to the new
|
|
// front.
|
|
for (const [type, listeners] of this.accessibilityEvents.entries()) {
|
|
for (const listener of listeners) {
|
|
accessibleWalkerFront.on(type, listener);
|
|
}
|
|
}
|
|
}
|
|
|
|
onAccessibleWalkerFrontDestroyed(accessibleWalkerFront) {
|
|
this._accessibilityWalkerFronts.delete(accessibleWalkerFront);
|
|
// Remove all existing accessible walker front event listeners from the
|
|
// destroyed front.
|
|
for (const [type, listeners] of this.accessibilityEvents.entries()) {
|
|
for (const listener of listeners) {
|
|
accessibleWalkerFront.off(type, listener);
|
|
}
|
|
}
|
|
}
|
|
|
|
async onTargetAvailable({ targetFront }) {
|
|
targetFront.watchFronts(
|
|
"accessibility",
|
|
this.onAccessibilityFrontAvailable,
|
|
this.onAccessibilityFrontDestroyed
|
|
);
|
|
|
|
if (!targetFront.isTopLevel) {
|
|
return;
|
|
}
|
|
|
|
// Clear all the fronts collected by `watchFronts` on the previous set of targets/documents.
|
|
this._accessibilityWalkerFronts.clear();
|
|
}
|
|
|
|
async onTargetDestroyed({ targetFront }) {
|
|
targetFront.unwatchFronts(
|
|
"accessibility",
|
|
this.onAccessibilityFrontAvailable,
|
|
this.onAccessibilityFrontDestroyed
|
|
);
|
|
}
|
|
|
|
async onTargetSelected({ targetFront }) {
|
|
this.accessibilityFront = await targetFront.getFront("accessibility");
|
|
|
|
this.simulatorFront = this.accessibilityFront.simulatorFront;
|
|
if (this.simulatorFront) {
|
|
this.simulate = types => this.simulatorFront.simulate({ types });
|
|
} else {
|
|
this.simulate = null;
|
|
}
|
|
|
|
await this.toggleDisplayTabbingOrder(false);
|
|
|
|
// Move accessibility front lifecycle event listeners to a new top level
|
|
// front.
|
|
for (const [type, listeners] of this.lifecycleEvents.entries()) {
|
|
for (const listener of listeners.values()) {
|
|
this.accessibilityFront.on(type, listener);
|
|
}
|
|
}
|
|
|
|
// Hold on refreshing the view on initialization.
|
|
// This will be done by the Panel class after everything is setup.
|
|
// (we especially need to wait for the a11y service to be started)
|
|
if (this.#initialized) {
|
|
await this.#panel.forceRefresh();
|
|
}
|
|
}
|
|
}
|
|
|
|
exports.AccessibilityProxy = AccessibilityProxy;
|