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/server/actors/highlighters/accessible.js | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.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/server/actors/highlighters/accessible.js')
-rw-r--r-- | devtools/server/actors/highlighters/accessible.js | 395 |
1 files changed, 395 insertions, 0 deletions
diff --git a/devtools/server/actors/highlighters/accessible.js b/devtools/server/actors/highlighters/accessible.js new file mode 100644 index 0000000000..71124239f2 --- /dev/null +++ b/devtools/server/actors/highlighters/accessible.js @@ -0,0 +1,395 @@ +/* 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 { + AutoRefreshHighlighter, +} = require("resource://devtools/server/actors/highlighters/auto-refresh.js"); +const { + CanvasFrameAnonymousContentHelper, + isNodeValid, +} = require("resource://devtools/server/actors/highlighters/utils/markup.js"); +const { + TEXT_NODE, + DOCUMENT_NODE, +} = require("resource://devtools/shared/dom-node-constants.js"); +const { + getCurrentZoom, + setIgnoreLayoutChanges, +} = require("resource://devtools/shared/layout/utils.js"); + +loader.lazyRequireGetter( + this, + ["getBounds", "getBoundsXUL", "Infobar"], + "resource://devtools/server/actors/highlighters/utils/accessibility.js", + true +); + +/** + * The AccessibleHighlighter draws the bounds of an accessible object. + * + * Usage example: + * + * let h = new AccessibleHighlighter(env); + * h.show(node, { x, y, w, h, [duration] }); + * h.hide(); + * h.destroy(); + * + * @param {Number} options.x + * X coordinate of the top left corner of the accessible object + * @param {Number} options.y + * Y coordinate of the top left corner of the accessible object + * @param {Number} options.w + * Width of the the accessible object + * @param {Number} options.h + * Height of the the accessible object + * @param {Number} options.duration + * Duration of time that the highlighter should be shown. + * @param {String|null} options.name + * Name of the the accessible object + * @param {String} options.role + * Role of the the accessible object + * + * Structure: + * <div class="highlighter-container" aria-hidden="true"> + * <div class="accessible-root"> + * <svg class="accessible-elements" hidden="true"> + * <path class="accessible-bounds" points="..." /> + * </svg> + * <div class="accessible-infobar-container"> + * <div class="accessible-infobar"> + * <div class="accessible-infobar-text"> + * <span class="accessible-infobar-role">Accessible Role</span> + * <span class="accessible-infobar-name">Accessible Name</span> + * </div> + * </div> + * </div> + * </div> + * </div> + */ +class AccessibleHighlighter extends AutoRefreshHighlighter { + constructor(highlighterEnv) { + super(highlighterEnv); + this.ID_CLASS_PREFIX = "accessible-"; + this.accessibleInfobar = new Infobar(this); + + this.markup = new CanvasFrameAnonymousContentHelper( + this.highlighterEnv, + this._buildMarkup.bind(this) + ); + this.isReady = this.markup.initialize(); + + this.onPageHide = this.onPageHide.bind(this); + this.onWillNavigate = this.onWillNavigate.bind(this); + + this.highlighterEnv.on("will-navigate", this.onWillNavigate); + + this.pageListenerTarget = highlighterEnv.pageListenerTarget; + this.pageListenerTarget.addEventListener("pagehide", this.onPageHide); + } + + /** + * Static getter that indicates that AccessibleHighlighter supports + * highlighting in XUL windows. + */ + static get XULSupported() { + return true; + } + + get supportsSimpleHighlighters() { + return true; + } + + /** + * Build highlighter markup. + * + * @return {Object} Container element for the highlighter markup. + */ + _buildMarkup() { + const container = this.markup.createNode({ + attributes: { + class: "highlighter-container", + "aria-hidden": "true", + }, + }); + + const root = this.markup.createNode({ + parent: container, + attributes: { + id: "root", + class: + "root" + + (this.highlighterEnv.useSimpleHighlightersForReducedMotion + ? " use-simple-highlighters" + : ""), + }, + prefix: this.ID_CLASS_PREFIX, + }); + + // Build the SVG element. + const svg = this.markup.createSVGNode({ + nodeType: "svg", + parent: root, + attributes: { + id: "elements", + width: "100%", + height: "100%", + hidden: "true", + }, + prefix: this.ID_CLASS_PREFIX, + }); + + this.markup.createSVGNode({ + nodeType: "path", + parent: svg, + attributes: { + class: "bounds", + id: "bounds", + }, + prefix: this.ID_CLASS_PREFIX, + }); + + // Build the accessible's infobar markup. + this.accessibleInfobar.buildMarkup(root); + + return container; + } + + /** + * Destroy the nodes. Remove listeners. + */ + destroy() { + if (this._highlightTimer) { + clearTimeout(this._highlightTimer); + this._highlightTimer = null; + } + + this.highlighterEnv.off("will-navigate", this.onWillNavigate); + this.pageListenerTarget.removeEventListener("pagehide", this.onPageHide); + this.pageListenerTarget = null; + + AutoRefreshHighlighter.prototype.destroy.call(this); + + this.accessibleInfobar.destroy(); + this.accessibleInfobar = null; + this.markup.destroy(); + } + + /** + * Find an element in highlighter markup. + * + * @param {String} id + * Highlighter markup elemet id attribute. + * @return {DOMNode} Element in the highlighter markup. + */ + getElement(id) { + return this.markup.getElement(this.ID_CLASS_PREFIX + id); + } + + /** + * Check if node is a valid element, document or text node. + * + * @override AutoRefreshHighlighter.prototype._isNodeValid + * @param {DOMNode} node + * The node to highlight. + * @return {Boolean} whether or not node is valid. + */ + _isNodeValid(node) { + return ( + super._isNodeValid(node) || + isNodeValid(node, TEXT_NODE) || + isNodeValid(node, DOCUMENT_NODE) + ); + } + + /** + * Show the highlighter on a given accessible. + * + * @return {Boolean} True if accessible is highlighted, false otherwise. + */ + _show() { + if (this._highlightTimer) { + clearTimeout(this._highlightTimer); + this._highlightTimer = null; + } + + const { duration } = this.options; + const shown = this._update(); + if (shown) { + this.emit("highlighter-event", { options: this.options, type: "shown" }); + if (duration) { + this._highlightTimer = setTimeout(() => { + this.hide(); + }, duration); + } + } + + return shown; + } + + /** + * Update and show accessible bounds for a current accessible. + * + * @return {Boolean} True if accessible is highlighted, false otherwise. + */ + _update() { + let shown = false; + setIgnoreLayoutChanges(true); + + if (this._updateAccessibleBounds()) { + this._showAccessibleBounds(); + + this.accessibleInfobar.show(); + + shown = true; + } else { + // Nothing to highlight (0px rectangle like a <script> tag for instance) + this.hide(); + } + + setIgnoreLayoutChanges( + false, + this.highlighterEnv.window.document.documentElement + ); + + return shown; + } + + /** + * Hide the highlighter. + */ + _hide() { + setIgnoreLayoutChanges(true); + this._hideAccessibleBounds(); + this.accessibleInfobar.hide(); + setIgnoreLayoutChanges( + false, + this.highlighterEnv.window.document.documentElement + ); + } + + /** + * Public API method to temporarily hide accessible bounds for things like + * color contrast calculation. + */ + hideAccessibleBounds() { + if (this.getElement("elements").hasAttribute("hidden")) { + return; + } + + this._hideAccessibleBounds(); + this._shouldRestoreBoundsVisibility = true; + } + + /** + * Public API method to show accessible bounds in case they were temporarily + * hidden. + */ + showAccessibleBounds() { + if (this._shouldRestoreBoundsVisibility) { + this._showAccessibleBounds(); + } + } + + /** + * Hide the accessible bounds container. + */ + _hideAccessibleBounds() { + this._shouldRestoreBoundsVisibility = null; + setIgnoreLayoutChanges(true); + this.getElement("elements").setAttribute("hidden", "true"); + setIgnoreLayoutChanges( + false, + this.highlighterEnv.window.document.documentElement + ); + } + + /** + * Show the accessible bounds container. + */ + _showAccessibleBounds() { + this._shouldRestoreBoundsVisibility = null; + if (!this.currentNode || !this.highlighterEnv.window) { + return; + } + + setIgnoreLayoutChanges(true); + this.getElement("elements").removeAttribute("hidden"); + setIgnoreLayoutChanges( + false, + this.highlighterEnv.window.document.documentElement + ); + } + + /** + * Get current accessible bounds. + * + * @return {Object|null} Returns, if available, positioning and bounds + * information for the accessible object. + */ + get _bounds() { + let { win, options } = this; + let getBoundsFn = getBounds; + if (this.options.isXUL) { + // Zoom level for the top level browser window does not change and only + // inner frames do. So we need to get the zoom level of the current node's + // parent window. + let zoom = getCurrentZoom(this.currentNode); + zoom *= zoom; + options = { ...options, zoom }; + getBoundsFn = getBoundsXUL; + win = this.win.parent.ownerGlobal; + } + + return getBoundsFn(win, options); + } + + /** + * Update accessible bounds for a current accessible. Re-draw highlighter + * markup. + * + * @return {Boolean} True if accessible is highlighted, false otherwise. + */ + _updateAccessibleBounds() { + const bounds = this._bounds; + if (!bounds) { + this._hide(); + return false; + } + + const boundsEl = this.getElement("bounds"); + const { left, right, top, bottom } = bounds; + const path = `M${left},${top} L${right},${top} L${right},${bottom} L${left},${bottom} L${left},${top}`; + boundsEl.setAttribute("d", path); + + // Un-zoom the root wrapper if the page was zoomed. + const rootId = this.ID_CLASS_PREFIX + "elements"; + this.markup.scaleRootElement(this.currentNode, rootId); + + return true; + } + + /** + * Hide highlighter on page hide. + */ + onPageHide({ target }) { + // If a pagehide event is triggered for current window's highlighter, hide + // the highlighter. + if (target.defaultView === this.win) { + this.hide(); + } + } + + /** + * Hide highlighter on navigation. + */ + onWillNavigate({ isTopLevel }) { + if (isTopLevel) { + this.hide(); + } + } +} + +exports.AccessibleHighlighter = AccessibleHighlighter; |