diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /devtools/client/accessibility/panel.js | |
parent | Initial commit. (diff) | |
download | firefox-esr-upstream.tar.xz firefox-esr-upstream.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/accessibility/panel.js')
-rw-r--r-- | devtools/client/accessibility/panel.js | 350 |
1 files changed, 350 insertions, 0 deletions
diff --git a/devtools/client/accessibility/panel.js b/devtools/client/accessibility/panel.js new file mode 100644 index 0000000000..02c6c8f415 --- /dev/null +++ b/devtools/client/accessibility/panel.js @@ -0,0 +1,350 @@ +/* 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 +); +const { + A11Y_SERVICE_DURATION, +} = require("resource://devtools/client/accessibility/constants.js"); + +// 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, commands) { + 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); + + 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 = 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.accessibilityProxy = new AccessibilityProxy(this._commands, this); + 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, + }); + + // Force refresh to render the UI and wait for the INITIALIZED event. + const onInitialized = this.panelWin.once(EVENTS.INITIALIZED); + this.shouldRefresh = true; + this.refresh(); + await onInitialized; + + 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; + await this._opening; + + await this.accessibilityProxy.accessibilityFrontGetPromise; + 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._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.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; |