summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/highlighters/accessible.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/actors/highlighters/accessible.js')
-rw-r--r--devtools/server/actors/highlighters/accessible.js395
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;