summaryrefslogtreecommitdiffstats
path: root/devtools/client/accessibility/panel.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/accessibility/panel.js')
-rw-r--r--devtools/client/accessibility/panel.js367
1 files changed, 367 insertions, 0 deletions
diff --git a/devtools/client/accessibility/panel.js b/devtools/client/accessibility/panel.js
new file mode 100644
index 0000000000..1e57f2b69a
--- /dev/null
+++ b/devtools/client/accessibility/panel.js
@@ -0,0 +1,367 @@
+/* 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";
+
+const Services = require("Services");
+const { L10nRegistry } = require("resource://gre/modules/L10nRegistry.jsm");
+
+const EventEmitter = require("devtools/shared/event-emitter");
+
+const Telemetry = require("devtools/client/shared/telemetry");
+
+loader.lazyRequireGetter(
+ this,
+ "AccessibilityProxy",
+ "devtools/client/accessibility/accessibility-proxy",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "Picker",
+ "devtools/client/accessibility/picker",
+ true
+);
+const {
+ A11Y_SERVICE_DURATION,
+} = require("devtools/client/accessibility/constants");
+
+// The panel's window global is an EventEmitter firing the following events:
+const EVENTS = {
+ // When the accessibility inspector has a new accessible front selected.
+ NEW_ACCESSIBLE_FRONT_SELECTED: "Accessibility:NewAccessibleFrontSelected",
+ // When the accessibility inspector has a new accessible front highlighted.
+ NEW_ACCESSIBLE_FRONT_HIGHLIGHTED:
+ "Accessibility:NewAccessibleFrontHighlighted",
+ // When the accessibility inspector has a new accessible front inspected.
+ NEW_ACCESSIBLE_FRONT_INSPECTED: "Accessibility:NewAccessibleFrontInspected",
+ // When the accessibility inspector is updated.
+ ACCESSIBILITY_INSPECTOR_UPDATED:
+ "Accessibility:AccessibilityInspectorUpdated",
+ // When accessibility panel UI is initialized (rendered).
+ INITIALIZED: "Accessibility:Initialized",
+ // When accessibile object properties are updated in the panel sidebar for a
+ // new accessible object.
+ PROPERTIES_UPDATED: "Accessibility:PropertiesUpdated",
+};
+
+/**
+ * This object represents Accessibility panel. It's responsibility is to
+ * render Accessibility Tree of the current debugger target and the sidebar that
+ * displays current relevant accessible details.
+ */
+function AccessibilityPanel(iframeWindow, toolbox) {
+ this.panelWin = iframeWindow;
+ this._toolbox = toolbox;
+
+ this.onTabNavigated = this.onTabNavigated.bind(this);
+ this.onTargetUpdated = this.onTargetUpdated.bind(this);
+ this.onPanelVisibilityChange = this.onPanelVisibilityChange.bind(this);
+ this.onNewAccessibleFrontSelected = this.onNewAccessibleFrontSelected.bind(
+ this
+ );
+ this.onAccessibilityInspectorUpdated = this.onAccessibilityInspectorUpdated.bind(
+ this
+ );
+ this.updateA11YServiceDurationTimer = this.updateA11YServiceDurationTimer.bind(
+ this
+ );
+ this.forceUpdatePickerButton = this.forceUpdatePickerButton.bind(this);
+ this.onLifecycleEvent = this.onLifecycleEvent.bind(this);
+
+ EventEmitter.decorate(this);
+}
+
+AccessibilityPanel.prototype = {
+ /**
+ * Open is effectively an asynchronous constructor.
+ */
+ async open() {
+ if (this._opening) {
+ await this._opening;
+ return this._opening;
+ }
+
+ let resolver;
+ this._opening = new Promise(resolve => {
+ resolver = resolve;
+ });
+
+ this._telemetry = new Telemetry();
+ this.panelWin.gTelemetry = this._telemetry;
+
+ this._toolbox.on("select", this.onPanelVisibilityChange);
+
+ this.panelWin.EVENTS = EVENTS;
+ EventEmitter.decorate(this.panelWin);
+ this.panelWin.on(
+ EVENTS.NEW_ACCESSIBLE_FRONT_SELECTED,
+ this.onNewAccessibleFrontSelected
+ );
+ this.panelWin.on(
+ EVENTS.ACCESSIBILITY_INSPECTOR_UPDATED,
+ this.onAccessibilityInspectorUpdated
+ );
+
+ this.shouldRefresh = true;
+
+ this.accessibilityProxy = new AccessibilityProxy(this._toolbox);
+ this.accessibilityProxy.startListeningForTargetUpdated(
+ this.onTargetUpdated
+ );
+ await this.accessibilityProxy.initialize();
+
+ // Enable accessibility service if necessary.
+ if (
+ this.accessibilityProxy.canBeEnabled &&
+ !this.accessibilityProxy.enabled
+ ) {
+ await this.accessibilityProxy.enableAccessibility();
+ }
+
+ this.picker = new Picker(this);
+ this.fluentBundles = await this.createFluentBundles();
+
+ this.updateA11YServiceDurationTimer();
+ this.accessibilityProxy.startListeningForLifecycleEvents({
+ init: this.onLifecycleEvent,
+ shutdown: this.onLifecycleEvent,
+ });
+
+ this.isReady = true;
+ this.emit("ready");
+ resolver(this);
+ return this._opening;
+ },
+
+ /**
+ * Retrieve message contexts for the current locales, and return them as an
+ * array of FluentBundles elements.
+ */
+ async createFluentBundles() {
+ const locales = Services.locale.appLocalesAsBCP47;
+ const generator = L10nRegistry.generateBundles(locales, [
+ "devtools/client/accessibility.ftl",
+ ]);
+
+ // Return value of generateBundles is a generator and should be converted to
+ // a sync iterable before using it with React.
+ const contexts = [];
+ for await (const message of generator) {
+ contexts.push(message);
+ }
+
+ return contexts;
+ },
+
+ onLifecycleEvent() {
+ this.updateA11YServiceDurationTimer();
+ this.forceUpdatePickerButton();
+ },
+
+ onNewAccessibleFrontSelected(selected) {
+ this.emit("new-accessible-front-selected", selected);
+ },
+
+ onAccessibilityInspectorUpdated() {
+ this.emit("accessibility-inspector-updated");
+ },
+
+ /**
+ * Make sure the panel is refreshed when the page is reloaded. The panel is
+ * refreshed immediatelly if it's currently selected or lazily when the user
+ * actually selects it.
+ */
+ onTabNavigated() {
+ this.shouldRefresh = true;
+ this._opening.then(() => this.refresh());
+ },
+
+ onTargetUpdated({ isTargetSwitching }) {
+ this.accessibilityProxy.currentTarget.on("navigate", this.onTabNavigated);
+ if (isTargetSwitching) {
+ this.onTabNavigated();
+ }
+ },
+
+ /**
+ * Make sure the panel is refreshed (if needed) when it's selected.
+ */
+ onPanelVisibilityChange() {
+ this._opening.then(() => this.refresh());
+ },
+
+ refresh() {
+ this.cancelPicker();
+
+ if (!this.isVisible) {
+ // Do not refresh if the panel isn't visible.
+ return;
+ }
+
+ // Do not refresh if it isn't necessary.
+ if (!this.shouldRefresh) {
+ return;
+ }
+ // Alright reset the flag we are about to refresh the panel.
+ this.shouldRefresh = false;
+ const {
+ supports,
+ getAccessibilityTreeRoot,
+ startListeningForAccessibilityEvents,
+ stopListeningForAccessibilityEvents,
+ audit,
+ simulate,
+ toggleDisplayTabbingOrder,
+ enableAccessibility,
+ resetAccessiblity,
+ startListeningForLifecycleEvents,
+ stopListeningForLifecycleEvents,
+ startListeningForParentLifecycleEvents,
+ stopListeningForParentLifecycleEvents,
+ highlightAccessible,
+ unhighlightAccessible,
+ } = this.accessibilityProxy;
+ this.postContentMessage("initialize", {
+ fluentBundles: this.fluentBundles,
+ toolbox: this._toolbox,
+ supports,
+ getAccessibilityTreeRoot,
+ startListeningForAccessibilityEvents,
+ stopListeningForAccessibilityEvents,
+ audit,
+ simulate,
+ toggleDisplayTabbingOrder,
+ enableAccessibility,
+ resetAccessiblity,
+ startListeningForLifecycleEvents,
+ stopListeningForLifecycleEvents,
+ startListeningForParentLifecycleEvents,
+ stopListeningForParentLifecycleEvents,
+ highlightAccessible,
+ unhighlightAccessible,
+ });
+ },
+
+ updateA11YServiceDurationTimer() {
+ if (this.accessibilityProxy.enabled) {
+ this._telemetry.start(A11Y_SERVICE_DURATION, this);
+ } else {
+ this._telemetry.finish(A11Y_SERVICE_DURATION, this, true);
+ }
+ },
+
+ selectAccessible(accessibleFront) {
+ this.postContentMessage("selectAccessible", accessibleFront);
+ },
+
+ selectAccessibleForNode(nodeFront, reason) {
+ if (reason) {
+ this._telemetry.keyedScalarAdd(
+ "devtools.accessibility.select_accessible_for_node",
+ reason,
+ 1
+ );
+ }
+
+ this.postContentMessage("selectNodeAccessible", nodeFront);
+ },
+
+ highlightAccessible(accessibleFront) {
+ this.postContentMessage("highlightAccessible", accessibleFront);
+ },
+
+ postContentMessage(type, ...args) {
+ const event = new this.panelWin.MessageEvent("devtools/chrome/message", {
+ bubbles: true,
+ cancelable: true,
+ data: { type, args },
+ });
+
+ this.panelWin.dispatchEvent(event);
+ },
+
+ updatePickerButton() {
+ this.picker && this.picker.updateButton();
+ },
+
+ forceUpdatePickerButton() {
+ // Only update picker button when the panel is selected.
+ if (!this.isVisible) {
+ return;
+ }
+
+ this.updatePickerButton();
+ // Calling setToolboxButtons to make sure toolbar is forced to re-render.
+ this._toolbox.component.setToolboxButtons(this._toolbox.toolbarButtons);
+ },
+
+ togglePicker(focus) {
+ this.picker && this.picker.toggle();
+ },
+
+ cancelPicker() {
+ this.picker && this.picker.cancel();
+ },
+
+ stopPicker() {
+ this.picker && this.picker.stop();
+ },
+
+ /**
+ * Return true if the Accessibility panel is currently selected.
+ */
+ get isVisible() {
+ return this._toolbox.currentToolId === "accessibility";
+ },
+
+ destroy() {
+ if (this._destroyed) {
+ return;
+ }
+ this._destroyed = true;
+
+ this.postContentMessage("destroy");
+
+ if (this.accessibilityProxy) {
+ this.accessibilityProxy.stopListeningForTargetUpdated(
+ this.onTargetUpdated
+ );
+ this.accessibilityProxy.currentTarget.off(
+ "navigate",
+ this.onTabNavigated
+ );
+ this.accessibilityProxy.stopListeningForLifecycleEvents({
+ init: this.onLifecycleEvent,
+ shutdown: this.onLifecycleEvent,
+ });
+ this.accessibilityProxy.destroy();
+ this.accessibilityProxy = null;
+ }
+
+ this._toolbox.off("select", this.onPanelVisibilityChange);
+
+ this.panelWin.off(
+ EVENTS.NEW_ACCESSIBLE_FRONT_SELECTED,
+ this.onNewAccessibleFrontSelected
+ );
+ this.panelWin.off(
+ EVENTS.ACCESSIBILITY_INSPECTOR_UPDATED,
+ this.onAccessibilityInspectorUpdated
+ );
+
+ // Older versions of devtools server do not support picker functionality.
+ if (this.picker) {
+ this.picker.release();
+ this.picker = null;
+ }
+
+ this._telemetry = null;
+ this.panelWin.gTelemetry = null;
+
+ this.emit("destroyed");
+ },
+};
+
+// Exports from this module
+exports.AccessibilityPanel = AccessibilityPanel;