/* 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 EventEmitter = require("resource://devtools/shared/event-emitter.js"); loader.lazyRequireGetter( this, "AccessibilityProxy", "resource://devtools/client/accessibility/accessibility-proxy.js", true ); loader.lazyRequireGetter( this, "Picker", "resource://devtools/client/accessibility/picker.js", true ); // 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. */ class AccessibilityPanel extends EventEmitter { #toolbox; #commands; #opening; #telemetry; #timerID; #destroyed; constructor(iframeWindow, toolbox, commands) { super(); this.panelWin = iframeWindow; this.#toolbox = toolbox; this.#commands = commands; 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); } /** * Open is effectively an asynchronous constructor. */ async open() { if (this.#opening) { await this.#opening; return this.#opening; } // This first promise includes initialization of proxy *and* the call to forceRefresh let resolver; this.#opening = new Promise(resolve => { resolver = resolve; }); // This second promise only include the initialization of proxy and few other things, // but not the call to forceRefresh. const { promise, resolve } = Promise.withResolvers(); this.initializedPromise = promise; this.#telemetry = this.#toolbox.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.picker = new Picker(this); this.fluentBundles = await this.createFluentBundles(); this.accessibilityProxy = new AccessibilityProxy(this.#commands, this); await this.accessibilityProxy.initialize(); this.accessibilityProxy.startListeningForLifecycleEvents({ init: this.onLifecycleEvent, shutdown: this.onLifecycleEvent, }); // Start recording the duration where a11y service is enabled via the proxy. this.updateA11YServiceDurationTimer(); // Resolve the `this.initializedPromise` resolve(); // Force rendering the panel once everything is initialized await this.forceRefresh(); 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.getInstance().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. */ async forceRefresh() { this.shouldRefresh = true; // Wait for initialization to be done, in case this is called early on. await this.initializedPromise; const onUpdated = this.panelWin.once(EVENTS.INITIALIZED); this.refresh(); await onUpdated; this.emit("reloaded"); } /** * 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.#timerID = Glean.devtools.accessibilityServiceTimeActive.start(); } else if (this.#timerID) { Glean.devtools.accessibilityServiceTimeActive.stopAndAccumulate( this.#timerID ); this.#timerID = null; } } selectAccessible(accessibleFront) { this.postContentMessage("selectAccessible", accessibleFront); } selectAccessibleForNode(nodeFront, reason) { if (reason) { Glean.devtoolsAccessibility.selectAccessibleForNode[reason].add(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() { 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"; } get toolbox() { return this.#toolbox; } destroy() { if (this.#destroyed) { return; } this.#destroyed = true; this.postContentMessage("destroy"); if (this.accessibilityProxy) { this.accessibilityProxy.stopListeningForLifecycleEvents({ init: this.onLifecycleEvent, shutdown: this.onLifecycleEvent, }); this.accessibilityProxy.destroy(); this.accessibilityProxy = null; this.initializedPromise = 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;