Bug 1473030 - Show accessible object's name and role information with the info-bar highlighter. draft
authorMicah Tigley <mtigley@mozilla.com>
Thu, 16 Aug 2018 00:35:02 -0400
changeset 829630 b65d527baea9a6f10e0dbfc0f2e1427de3a6f7a3
parent 829629 5928090fcb23935e819e22f392367309428cb648
push id118782
push userbmo:mtigley@mozilla.com
push dateThu, 16 Aug 2018 04:40:36 +0000
bugs1473030
milestone63.0a1
Bug 1473030 - Show accessible object's name and role information with the info-bar highlighter. MozReview-Commit-ID: HyKIdqsL3u
devtools/client/inspector/markup/utils.js
devtools/client/inspector/markup/views/element-editor.js
devtools/server/actors/accessibility.js
devtools/server/actors/highlighters.css
devtools/server/actors/highlighters/accessible.js
devtools/server/actors/highlighters/utils/accessibility.js
devtools/server/actors/highlighters/xul-accessible.js
devtools/server/tests/browser/browser.ini
devtools/server/tests/browser/browser_accessibility_highlighter_infobar.js
devtools/server/tests/browser/browser_accessibility_infobar_show.js
devtools/server/tests/browser/doc_accessibility_infobar.html
devtools/shared/inspector/moz.build
devtools/shared/inspector/utils.js
devtools/shared/specs/accessibility.js
--- a/devtools/client/inspector/markup/utils.js
+++ b/devtools/client/inspector/markup/utils.js
@@ -108,28 +108,14 @@ function parseAttributeValues(attr, doc)
       // Prevents InvalidCharacterError - "String contains an invalid
       // character".
     }
   }
 
   return attributes;
 }
 
-/**
- * Truncate the string and add ellipsis to the middle of the string.
- */
-function truncateString(str, maxLength) {
-  if (!str || str.length <= maxLength) {
-    return str;
-  }
-
-  return str.substring(0, Math.ceil(maxLength / 2)) +
-         "…" +
-         str.substring(str.length - Math.floor(maxLength / 2));
-}
-
 module.exports = {
   flashElementOn,
   flashElementOff,
   getAutocompleteMaxWidth,
   parseAttributeValues,
-  truncateString,
 };
--- a/devtools/client/inspector/markup/views/element-editor.js
+++ b/devtools/client/inspector/markup/views/element-editor.js
@@ -6,18 +6,18 @@
 
 const Services = require("Services");
 const TextEditor = require("devtools/client/inspector/markup/views/text-editor");
 const {
   getAutocompleteMaxWidth,
   flashElementOn,
   flashElementOff,
   parseAttributeValues,
-  truncateString,
 } = require("devtools/client/inspector/markup/utils");
+const { truncateString } = require("devtools/shared/inspector/utils");
 const {editableField, InplaceEditor} =
       require("devtools/client/shared/inplace-editor");
 const {parseAttribute} =
       require("devtools/client/shared/node-attribute-parser");
 const {getCssProperties} = require("devtools/shared/fronts/css-properties");
 
 // Global tooltip inspector
 const {LocalizationHelper} = require("devtools/shared/l10n");
--- a/devtools/server/actors/accessibility.js
+++ b/devtools/server/actors/accessibility.js
@@ -343,16 +343,22 @@ const AccessibleActor = ActorClassWithSp
       x = x.value;
       y = y.value;
       w = w.value;
       h = h.value;
     } catch (e) {
       return null;
     }
 
+    // Check if accessible bounds are invalid.
+    const left = x, right = x + w, top = y, bottom = y + h;
+    if (left === right || top === bottom) {
+      return null;
+    }
+
     return { x, y, w, h };
   },
 
   form() {
     return {
       actor: this.actorID,
       role: this.role,
       name: this.name,
@@ -381,19 +387,23 @@ const AccessibleWalkerActor = ActorClass
   initialize(conn, targetActor) {
     Actor.prototype.initialize.call(this, conn);
     this.targetActor = targetActor;
     this.refMap = new Map();
     this.setA11yServiceGetter();
     this.onPick = this.onPick.bind(this);
     this.onHovered = this.onHovered.bind(this);
     this.onKey = this.onKey.bind(this);
+    this.onHighlighterEvent = this.onHighlighterEvent.bind(this);
 
     this.highlighter = CustomHighlighterActor(this, isXUL(this.rootWin) ?
       "XULWindowAccessibleHighlighter" : "AccessibleHighlighter");
+
+    this.manage(this.highlighter);
+    this.highlighter.on("highlighter-event", this.onHighlighterEvent);
   },
 
   setA11yServiceGetter() {
     DevToolsUtils.defineLazyGetter(this, "a11yService", () => {
       Services.obs.addObserver(this, "accessible-event");
       return Cc["@mozilla.org/accessibilityService;1"].getService(
         Ci.nsIAccessibilityService);
     });
@@ -436,17 +446,17 @@ const AccessibleWalkerActor = ActorClass
     this.setA11yServiceGetter();
   },
 
   destroy() {
     Actor.prototype.destroy.call(this);
 
     this.reset();
 
-    this.highlighter.destroy();
+    this.highlighter.off("highlighter-event", this.onHighlighterEvent);
     this.highlighter = null;
 
     this.targetActor = null;
     this.refMap = null;
   },
 
   getRef(rawAccessible) {
     return this.refMap.get(rawAccessible);
@@ -580,16 +590,20 @@ const AccessibleWalkerActor = ActorClass
     } catch (error) {
       throw new Error(`Failed to get ancestor for ${accessible}: ${error}`);
     }
 
     return ancestry.map(parent => (
       { accessible: parent, children: parent.children() }));
   },
 
+  onHighlighterEvent: function(data) {
+    this.emit("highlighter-event", data);
+  },
+
   /**
    * Accessible event observer function.
    *
    * @param {nsIAccessibleEvent} subject
    *                                      accessible event object.
    */
   observe(subject) {
     const event = subject.QueryInterface(nsIAccessibleEvent);
@@ -695,23 +709,23 @@ const AccessibleWalkerActor = ActorClass
    * @param  {Object} options
    *         Object used for passing options. Available options:
    *         - duration {Number}
    *                    Duration of time that the highlighter should be shown.
    * @return {Boolean}
    *         True if highlighter shows the accessible object.
    */
   highlightAccessible(accessible, options = {}) {
-    const bounds = accessible.bounds;
+    const { bounds, name, role } = accessible;
     if (!bounds) {
       return false;
     }
 
     return this.highlighter.show({ rawNode: accessible.rawAccessible.DOMNode },
-                                 { ...options, ...bounds });
+                                 { ...options, ...bounds, name, role });
   },
 
   /**
    * Public method used to hide an accessible object highlighter on the client
    * side.
    */
   unhighlight() {
     this.highlighter.hide();
@@ -788,19 +802,23 @@ const AccessibleWalkerActor = ActorClass
     }
 
     const accessible = await this._findAndAttachAccessible(event);
     if (!accessible) {
       return;
     }
 
     if (this._currentAccessible !== accessible) {
-      const { bounds } = accessible;
+      const { bounds, role, name } = accessible;
       if (bounds) {
-        this.highlighter.show({ rawNode: event.originalTarget || event.target }, bounds);
+        this.highlighter.show({ rawNode: event.originalTarget || event.target }, {
+          ...bounds,
+          role,
+          name
+        });
       }
 
       events.emit(this, "picker-accessible-hovered", accessible);
       this._currentAccessible = accessible;
     }
   },
 
   /**
--- a/devtools/server/actors/highlighters.css
+++ b/devtools/server/actors/highlighters.css
@@ -30,16 +30,17 @@
   --highlighter-guide-color: #08c;
   --highlighter-content-color: #87ceeb;
   --highlighter-bubble-text-color: hsl(216, 33%, 97%);
   --highlighter-bubble-background-color: hsl(214, 13%, 24%);
   --highlighter-bubble-border-color: rgba(255, 255, 255, 0.2);
   --highlighter-bubble-arrow-size: 8px;
   --highlighter-font-family: message-box;
   --highlighter-font-size: 11px;
+  --highlighter-infobar-color: hsl(210, 30%, 85%);
   --highlighter-marker-color: #000;
 }
 
 /**
  * Highlighters are asbolute positioned in the page by default.
  * A single highlighter can have fixed position in its css class if needed (see below the
  * eye dropper or rulers highlighter, for example); but if it has to handle the
  * document's scrolling (as rulers does), it would lag a bit behind due the APZ (Async
@@ -205,17 +206,17 @@
 :-moz-native-anonymous .box-model-infobar-classes,
 :-moz-native-anonymous .box-model-infobar-pseudo-classes {
   color: hsl(200, 74%, 57%);
   overflow: hidden;
   text-overflow: ellipsis;
 }
 
 :-moz-native-anonymous [class$=infobar-dimensions] {
-  color: hsl(210, 30%, 85%);
+  color: var(--highlighter-infobar-color);
   border-inline-start: 1px solid #5a6169;
   margin-inline-start: 6px;
   padding-inline-start: 6px;
 }
 
 /* CSS Grid Highlighter */
 
 :-moz-native-anonymous .css-grid-canvas {
@@ -238,17 +239,17 @@
 
 :-moz-native-anonymous .css-grid-area-infobar-name,
 :-moz-native-anonymous .css-grid-cell-infobar-position,
 :-moz-native-anonymous .css-grid-line-infobar-number {
   color: hsl(285, 100%, 75%);
 }
 
 :-moz-native-anonymous .css-grid-line-infobar-names:not(:empty) {
-  color: hsl(210, 30%, 85%);
+  color: var(--highlighter-infobar-color);
   border-inline-start: 1px solid #5a6169;
   margin-inline-start: 6px;
   padding-inline-start: 6px;
 }
 
 /* CSS Transform Highlighter */
 
 :-moz-native-anonymous .css-transform-transformed {
@@ -640,8 +641,24 @@
 }
 
 /* Accessible highlighter */
 
 :-moz-native-anonymous .accessible-bounds {
   opacity: 0.6;
   fill: #6a5acd;
 }
+
+:-moz-native-anonymous .accessible-infobar-name {
+  color:var(--highlighter-infobar-color);
+  max-width: 90%;
+}
+
+:-moz-native-anonymous .accessible-infobar-name:not(:empty) {
+  color: var(--highlighter-infobar-color);
+  border-inline-start: 1px solid #5a6169;
+  margin-inline-start: 6px;
+  padding-inline-start: 6px;
+}
+
+:-moz-native-anonymous .accessible-infobar-role {
+  color: #9CDCFE;
+}
--- a/devtools/server/actors/highlighters/accessible.js
+++ b/devtools/server/actors/highlighters/accessible.js
@@ -1,21 +1,20 @@
 /* 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("./auto-refresh");
-const { getBounds } = require("./utils/accessibility");
-
+const { getBounds, Infobar } = require("./utils/accessibility");
 const {
   CanvasFrameAnonymousContentHelper,
   createNode,
-  createSVGNode
+  createSVGNode,
 } = require("./utils/markup");
 
 const { setIgnoreLayoutChanges } = require("devtools/shared/layout/utils");
 
 /**
  * The AccessibleHighlighter draws the bounds of an accessible object.
  *
  * Usage example:
@@ -31,31 +30,43 @@ const { setIgnoreLayoutChanges } = requi
  *         - {Number} y
  *           y coordinate of the top left corner of the accessible object
  *         - {Number} w
  *           width of the the accessible object
  *         - {Number} h
  *           height of the the accessible object
  *         - {Number} duration
  *           Duration of time that the highlighter should be shown.
+ *         - {String|null} name
+ *           name of the the accessible object
+ *         - {String} role
+ *           role of the the accessible object
  *
  * Structure:
- * <div class="highlighter-container">
+ * <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.onPageHide = this.onPageHide.bind(this);
     this.onWillNavigate = this.onWillNavigate.bind(this);
 
     this.highlighterEnv.on("will-navigate", this.onWillNavigate);
@@ -68,70 +79,72 @@ class AccessibleHighlighter extends Auto
    * Build highlighter markup.
    *
    * @return {Object} Container element for the highlighter markup.
    */
   _buildMarkup() {
     const container = createNode(this.win, {
       attributes: {
         "class": "highlighter-container",
-        "role": "presentation"
+        "aria-hidden": "true"
       }
     });
 
     const root = createNode(this.win, {
       parent: container,
       attributes: {
         "id": "root",
         "class": "root",
-        "role": "presentation"
       },
       prefix: this.ID_CLASS_PREFIX
     });
 
     // Build the SVG element.
     const svg = createSVGNode(this.win, {
       nodeType: "svg",
       parent: root,
       attributes: {
         "id": "elements",
         "width": "100%",
         "height": "100%",
         "hidden": "true",
-        "role": "presentation"
       },
       prefix: this.ID_CLASS_PREFIX
     });
 
     createSVGNode(this.win, {
       nodeType: "path",
       parent: svg,
       attributes: {
         "class": "bounds",
         "id": "bounds",
-        "role": "presentation"
       },
       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;
+    this.accessibleInfobar.destroy();
+    this.accessibleInfobar = null;
 
     this.markup.destroy();
     AutoRefreshHighlighter.prototype.destroy.call(this);
   }
 
   /**
    * Find an element in highlighter markup.
    *
@@ -151,35 +164,42 @@ class AccessibleHighlighter extends Auto
   _show() {
     if (this._highlightTimer) {
       clearTimeout(this._highlightTimer);
       this._highlightTimer = null;
     }
 
     const { duration } = this.options;
     const shown = this._update();
-    if (shown && duration) {
-      this._highlightTimer = setTimeout(() => {
-        this.hide();
-      }, duration);
+    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);
@@ -188,29 +208,30 @@ class AccessibleHighlighter extends Auto
   }
 
   /**
    * Hide the highlighter.
    */
   _hide() {
     setIgnoreLayoutChanges(true);
     this._hideAccessibleBounds();
+    this.accessibleInfobar.hide();
     setIgnoreLayoutChanges(false,
                            this.highlighterEnv.window.document.documentElement);
   }
 
   /**
    * Hide the accessible bounds container.
    */
   _hideAccessibleBounds() {
     this.getElement("elements").setAttribute("hidden", "true");
   }
 
   /**
-   * Showthe accessible bounds container.
+   * Show the accessible bounds container.
    */
   _showAccessibleBounds() {
     this.getElement("elements").removeAttribute("hidden");
   }
 
   /**
    * Get current accessible bounds.
    *
@@ -225,17 +246,17 @@ class AccessibleHighlighter extends Auto
    * 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._hideAccessibleBounds();
+      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}`;
     boundsEl.setAttribute("d", path);
--- a/devtools/server/actors/highlighters/utils/accessibility.js
+++ b/devtools/server/actors/highlighters/utils/accessibility.js
@@ -1,15 +1,364 @@
 /* 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 { getCurrentZoom } = require("devtools/shared/layout/utils");
+const { getCurrentZoom, getViewportDimensions } = require("devtools/shared/layout/utils");
+const { moveInfobar, createNode } = require("./markup");
+const { truncateString } = require("devtools/shared/inspector/utils");
+
+// Max string length for truncating accessible name values.
+const MAX_STRING_LENGTH = 50;
+
+/**
+ * The AccessibleInfobar is a class responsible for creating the markup for the
+ * accessible highlighter. It is also reponsible for updating content within the
+ * infobar such as role and name values.
+ */
+class Infobar {
+  constructor(highlighter) {
+    this.highlighter = highlighter;
+  }
+
+  get document() {
+    return this.highlighter.win.document;
+  }
+
+  get bounds() {
+    return this.highlighter._bounds;
+  }
+
+  get options() {
+    return this.highlighter.options;
+  }
+
+  get prefix() {
+    return this.highlighter.ID_CLASS_PREFIX;
+  }
+
+  get win() {
+    return this.highlighter.win;
+  }
+
+  /**
+   * Move the Infobar to the right place in the highlighter.
+   *
+   * @param  {Element} container
+   *         Container of infobar.
+   */
+  _moveInfobar(container) {
+    // Position the infobar using accessible's bounds
+    const { left: x, top: y, bottom, width } = this.bounds;
+    const infobarBounds = { x, y, bottom, width };
+
+    moveInfobar(container, infobarBounds, this.win);
+  }
+
+  /**
+   * Build markup for infobar.
+   *
+   * @param  {Element} root
+   *         Root element to build infobar with.
+   */
+  buildMarkup(root) {
+    const container = createNode(this.win, {
+      parent: root,
+      attributes: {
+        "class": "infobar-container",
+        "id": "infobar-container",
+        "aria-hidden": "true",
+        "hidden": "true"
+      },
+      prefix: this.prefix,
+    });
+
+    const infobar = createNode(this.win, {
+      parent: container,
+      attributes: {
+        "class": "infobar",
+        "id": "infobar",
+      },
+      prefix: this.prefix,
+    });
+
+    const infobarText = createNode(this.win, {
+      parent: infobar,
+      attributes: {
+        "class": "infobar-text",
+        "id": "infobar-text",
+      },
+      prefix: this.prefix,
+    });
+
+    createNode(this.win, {
+      nodeType: "span",
+      parent: infobarText,
+      attributes: {
+        "class": "infobar-role",
+        "id": "infobar-role",
+      },
+      prefix: this.prefix,
+    });
+
+    createNode(this.win, {
+      nodeType: "span",
+      parent: infobarText,
+      attributes: {
+        "class": "infobar-name",
+        "id": "infobar-name",
+      },
+      prefix: this.prefix,
+    });
+  }
+
+  /**
+   * Destroy the Infobar's highlighter.
+   */
+  destroy() {
+    this.highlighter = null;
+  }
+
+  /**
+   * Gets the element with the specified ID.
+   *
+   * @param  {String} id
+   *         Element ID.
+   * @return {Element} The element with specified ID.
+   */
+  getElement(id) {
+    return this.highlighter.getElement(id);
+  }
+
+  /**
+   * Gets the text content of element.
+   *
+   * @param  {String} id
+   *          Element ID to retrieve text content from.
+   * @return {String} The text content of the element.
+   */
+  getTextContent(id) {
+    const anonymousContent = this.highlighter.markup.content;
+    return anonymousContent.getTextContentForElement(`${this.prefix}${id}`);
+  }
+
+  /**
+   * Hide the accessible infobar.
+   */
+  hide() {
+    const container = this.getElement("infobar-container");
+    container.setAttribute("hidden", "true");
+  }
+
+  /**
+   * Show the accessible infobar highlighter.
+   */
+  show() {
+    const container = this.getElement("infobar-container");
+
+    // Remove accessible's infobar "hidden" attribute. We do this first to get the
+    // computed styles of the infobar container.
+    container.removeAttribute("hidden");
+
+    // Update the infobar's position and content.
+    this.update(container);
+  }
+
+  /**
+   * Update content of the infobar.
+   */
+  update(container) {
+    const { name, role } = this.options;
+
+    this.updateRole(role, this.getElement("infobar-role"));
+    this.updateName(name, this.getElement("infobar-name"));
+
+    // Position the infobar.
+    this._moveInfobar(container);
+  }
+
+  /**
+   * Sets the text content of the specified element.
+   *
+   * @param  {Element} el
+   *         Element to set text content on.
+   * @param  {String} text
+   *         Text for content.
+   */
+  setTextContent(el, text) {
+    el.setTextContent(text);
+  }
+
+  /**
+   * Show the accessible's name message.
+   *
+   * @param  {String} name
+   *         Accessible's name value.
+   * @param  {Element} el
+   *         Element to set text content on.
+   */
+  updateName(name, el) {
+    const nameText = name ? `"${truncateString(name, MAX_STRING_LENGTH)}"` : "";
+    this.setTextContent(el, nameText);
+  }
+
+  /**
+   * Show the accessible's role.
+   *
+   * @param  {String} role
+   *         Accessible's role value.
+   * @param  {Element} el
+   *         Element to set text content on.
+   */
+  updateRole(role, el) {
+    this.setTextContent(el, role);
+  }
+}
+
+/**
+ * The XULAccessibleInfobar handles building the XUL infobar markup where it isn't
+ * possible with the regular accessible highlighter.
+ */
+class XULWindowInfobar extends Infobar {
+  /**
+   * A helper function that calculates the positioning of a XUL accessible's infobar.
+   *
+   * @param  {Object} container
+   *         The infobar container.
+   */
+  _moveInfobar(container) {
+    const arrow = this.getElement("arrow");
+
+    // Show the container and arrow elements first.
+    container.removeAttribute("hidden");
+    arrow.removeAttribute("hidden");
+
+    // Set the left value of the infobar container in relation to
+    // highlighter's bounds position.
+    const {
+      left: boundsLeft,
+      right: boundsRight,
+      top: boundsTop,
+      bottom: boundsBottom
+    } = this.bounds;
+    const boundsMidPoint = (boundsLeft + boundsRight) / 2;
+    container.style.left = `${boundsMidPoint}px`;
+
+    const zoom = getCurrentZoom(this.win);
+    let {
+      width: viewportWidth,
+      height: viewportHeight,
+    } = getViewportDimensions(this.win);
+
+    const { width, height, left, } = container.getBoundingClientRect();
+
+    const containerHalfWidth = width / 2;
+    const containerHeight = height;
+    const margin = 100 * zoom;
+
+    viewportHeight *= zoom;
+    viewportWidth *= zoom;
+
+    // Determine viewport boundaries for infobar.
+    const topBoundary = margin;
+    const bottomBoundary = viewportHeight - containerHeight;
+    const leftBoundary = containerHalfWidth;
+    const rightBoundary = viewportWidth - containerHalfWidth;
+
+    // Determine if an infobar's position is offscreen.
+    const isOffScreenOnTop = boundsBottom < topBoundary;
+    const isOffScreenOnBottom = boundsBottom > bottomBoundary;
+    const isOffScreenOnLeft = left < leftBoundary;
+    const isOffScreenOnRight = left > rightBoundary;
+
+    // Check if infobar is offscreen on either left/right of viewport and position.
+    if (isOffScreenOnLeft) {
+      container.style.left = `${leftBoundary + boundsLeft}px`;
+      arrow.setAttribute("hidden", "true");
+    } else if (isOffScreenOnRight) {
+      const leftOffset = rightBoundary - boundsRight;
+      container.style.left = `${rightBoundary - leftOffset - containerHalfWidth}px`;
+      arrow.setAttribute("hidden", "true");
+    }
+
+    // Check if infobar is offscreen on either top/bottom of viewport and position.
+    const bubbleArrowSize = "var(--highlighter-bubble-arrow-size)";
+
+    if (isOffScreenOnTop) {
+      if (boundsTop < 0) {
+        container.style.top = bubbleArrowSize;
+      } else {
+        container.style.top = `calc(${boundsBottom}px + ${bubbleArrowSize})`;
+      }
+      arrow.setAttribute("class", "accessible-arrow top");
+    } else if (isOffScreenOnBottom) {
+      container.style.top = `calc(${bottomBoundary}px - ${bubbleArrowSize})`;
+      arrow.setAttribute("hidden", "true");
+    } else {
+      container.style.top = `calc(${boundsTop}px -
+        (${containerHeight}px + ${bubbleArrowSize}))`;
+      arrow.setAttribute("class", "accessible-arrow bottom");
+    }
+  }
+
+  /**
+   * Build markup for XUL window infobar.
+   *
+   * @param  {Element} root
+   *         Root element to build infobar with.
+   */
+  buildMarkup(root) {
+    super.buildMarkup(root, createNode);
+
+    createNode(this.win, {
+      parent: this.getElement("infobar"),
+      attributes: {
+        "class": "arrow",
+        "id": "arrow",
+      },
+      prefix: this.prefix,
+    });
+  }
+
+  /**
+   * Override of Infobar class's getTextContent method.
+   *
+   * @param  {String} id
+   *         Element ID to retrieve text content from.
+   * @return {String} Returns the text content of the element.
+   */
+  getTextContent(id) {
+    return this.getElement(id).textContent;
+  }
+
+  /**
+   * Override of Infobar class's getElement method.
+   *
+   * @param  {String} id
+   *         Element ID.
+   * @return {String} Returns the specified element.
+   */
+  getElement(id) {
+    return this.win.document.getElementById(`${this.prefix}${id}`);
+  }
+
+  /**
+   * Override of Infobar class's setTextContent method.
+   *
+   * @param  {Element} el
+   *         Element to set text content on.
+   * @param  {String} text
+   *         Text for content.
+   */
+  setTextContent(el, text) {
+    el.textContent = text;
+  }
+}
 
 /**
  * A helper function that calculate accessible object bounds and positioning to
  * be used for highlighting.
  *
  * @param  {Object} win
  *         window that contains accessible object.
  * @param  {Object} options
@@ -25,17 +374,20 @@ const { getCurrentZoom } = require("devt
  *         - {Number} zoom
  *           zoom level of the accessible object's parent window
  * @return {Object|null} Returns, if available, positioning and bounds information for
  *                 the accessible object.
  */
 function getBounds(win, { x, y, w, h, zoom }) {
   let { mozInnerScreenX, mozInnerScreenY, scrollX, scrollY } = win;
   let zoomFactor = getCurrentZoom(win);
-  let left = x, right = x + w, top = y, bottom = y + h;
+  let left = x;
+  let right = x + w;
+  let top = y;
+  let bottom = y + h;
 
   // For a XUL accessible, normalize the top-level window with its current zoom level.
   // We need to do this because top-level browser content does not allow zooming.
   if (zoom) {
     zoomFactor = zoom;
     mozInnerScreenX /= zoomFactor;
     mozInnerScreenY /= zoomFactor;
     scrollX /= zoomFactor;
@@ -53,9 +405,12 @@ function getBounds(win, { x, y, w, h, zo
   bottom *= zoomFactor;
 
   const width = right - left;
   const height = bottom - top;
 
   return { left, right, top, bottom, width, height };
 }
 
+exports.MAX_STRING_LENGTH = MAX_STRING_LENGTH;
 exports.getBounds = getBounds;
+exports.Infobar = Infobar;
+exports.XULWindowInfobar = XULWindowInfobar;
--- a/devtools/server/actors/highlighters/xul-accessible.js
+++ b/devtools/server/actors/highlighters/xul-accessible.js
@@ -1,30 +1,96 @@
 /* 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 { getBounds } = require("./utils/accessibility");
+const { getBounds, XULWindowInfobar } = require("./utils/accessibility");
 const { createNode, isNodeValid } = require("./utils/markup");
 const { getCurrentZoom, loadSheet } = require("devtools/shared/layout/utils");
 
 /**
  * Stylesheet used for highlighter styling of accessible objects in chrome. It
  * is consistent with the styling of an in-content accessible highlighter.
  */
 const ACCESSIBLE_BOUNDS_SHEET = "data:text/css;charset=utf-8," + encodeURIComponent(`
+  .highlighter-container {
+    --highlighter-bubble-background-color: hsl(214, 13%, 24%);
+    --highlighter-bubble-border-color: rgba(255, 255, 255, 0.2);
+    --highlighter-bubble-arrow-size: 8px;
+  }
+
   .accessible-bounds {
     position: fixed;
     pointer-events: none;
     z-index: 10;
     display: block;
     background-color: #6a5acd!important;
     opacity: 0.6;
+  }
+
+  .accessible-infobar-container {
+    position: fixed;
+    max-width: 90%;
+    z-index: 11;
+  }
+
+  .accessible-infobar {
+    position: relative;
+    left: -50%;
+    background-color: var(--highlighter-bubble-background-color);
+    min-width: 75px;
+    border: 1px solid var(--highlighter-bubble-border-color);
+    border-radius: 3px;
+    padding: 5px;
+  }
+
+  .accessible-arrow {
+    position: absolute;
+    width: 0;
+    height: 0;
+    border-left: var(--highlighter-bubble-arrow-size) solid transparent;
+    border-right: var(--highlighter-bubble-arrow-size) solid transparent;
+    left: calc(50% - var(--highlighter-bubble-arrow-size));
+  }
+
+  .top {
+    border-bottom: var(--highlighter-bubble-arrow-size) solid
+      var(--highlighter-bubble-background-color);
+    top: calc(-1 * var(--highlighter-bubble-arrow-size));
+  }
+
+  .bottom {
+    border-top: var(--highlighter-bubble-arrow-size) solid
+      var(--highlighter-bubble-background-color);
+    bottom: calc(-1 * var(--highlighter-bubble-arrow-size));
+  }
+
+  .accessible-infobar-text {
+    overflow: hidden;
+    white-space: nowrap;
+    display: flex;
+    justify-content: center;
+  }
+
+  .accessible-infobar-name {
+    color: rgb(221, 0, 169);
+    max-width: 90%;
+  }
+
+  .accessible-infobar-name:not(:empty) {
+    color: hsl(210, 30%, 85%);
+    border-inline-start: 1px solid #5a6169;
+    margin-inline-start: 6px;
+    padding-inline-start: 6px;
+  }
+
+  .accessible-infobar-role {
+    color: #9CDCFE;
   }`);
 
 /**
  * The XULWindowAccessibleHighlighter is a class that has the same API as the
  * AccessibleHighlighter, and by extension other highlighters that implement
  * auto-refresh highlighter, but instead of drawing in canvas frame anonymous
  * content (that is not available for chrome accessible highlighting) it adds a
  * transparrent inactionable element with the same position and bounds as the
@@ -32,18 +98,20 @@ const ACCESSIBLE_BOUNDS_SHEET = "data:te
  * element (that corresponds to accessible object) itself because the accessible
  * position and bounds are calculated differently.
  *
  * It is used when canvasframe-based AccessibleHighlighter can't be used. This
  * is the case for XUL windows.
  */
 class XULWindowAccessibleHighlighter {
   constructor(highlighterEnv) {
+    this.ID_CLASS_PREFIX = "accessible-";
     this.highlighterEnv = highlighterEnv;
     this.win = highlighterEnv.window;
+    this.accessibleInfobar = new XULWindowInfobar(this);
   }
 
   /**
    * Static getter that indicates that XULWindowAccessibleHighlighter supports
    * highlighting in XUL windows.
    */
   static get XULSupported() {
     return true;
@@ -55,27 +123,28 @@ class XULWindowAccessibleHighlighter {
   _buildMarkup() {
     const doc = this.win.document;
     loadSheet(doc.ownerGlobal, ACCESSIBLE_BOUNDS_SHEET);
 
     this.container = createNode(this.win, {
       parent: doc.body || doc.documentElement,
       attributes: {
         "class": "highlighter-container",
-        "role": "presentation"
+        "aria-hidden": "true"
       }
     });
 
     this.bounds = createNode(this.win, {
       parent: this.container,
       attributes: {
         "class": "accessible-bounds",
-        "role": "presentation"
       }
     });
+
+    this.accessibleInfobar.buildMarkup(this.container);
   }
 
   /**
    * Get current accessible bounds.
    *
    * @return {Object|null} Returns, if available, positioning and bounds
    *                       information for the accessible object.
    */
@@ -99,16 +168,21 @@ class XULWindowAccessibleHighlighter {
    *         - {Number} y
    *           y coordinate of the top left corner of the accessible object
    *         - {Number} w
    *           width of the the accessible object
    *         - {Number} h
    *           height of the the accessible object
    *         - duration {Number}
    *                    Duration of time that the highlighter should be shown.
+   *         - {String|null} name
+   *           name of the the accessible object
+   *         - {String} role
+   *           role of the the accessible object
+   *
    * @return {Boolean} True if accessible is highlighted, false otherwise.
    */
   show(node, options = {}) {
     const isSameNode = node === this.currentNode;
     const hasBounds = options && typeof options.x == "number" &&
                                typeof options.y == "number" &&
                                typeof options.w == "number" &&
                                typeof options.h == "number";
@@ -153,40 +227,45 @@ class XULWindowAccessibleHighlighter {
   _update() {
     this._hideAccessibleBounds();
     const bounds = this._bounds;
     if (!bounds) {
       return false;
     }
 
     let boundsEl = this.bounds;
+
     if (!boundsEl) {
       this._buildMarkup();
       boundsEl = this.bounds;
     }
 
     const { left, top, width, height } = bounds;
     boundsEl.style.top = `${top}px`;
     boundsEl.style.left = `${left}px`;
     boundsEl.style.width = `${width}px`;
     boundsEl.style.height = `${height}px`;
+
     this._showAccessibleBounds();
+    this.accessibleInfobar.show();
 
     return true;
   }
 
   /**
    * Hide the highlighter
    */
   hide() {
     if (!this.currentNode || !this.highlighterEnv.window) {
       return;
     }
 
     this._hideAccessibleBounds();
+    this.accessibleInfobar.hide();
+
     this.currentNode = null;
     this.options = null;
   }
 
   /**
    * Show accessible bounds highlighter.
    */
   _showAccessibleBounds() {
@@ -213,13 +292,16 @@ class XULWindowAccessibleHighlighter {
       this._highlightTimer = null;
     }
 
     this.hide();
     if (this.container) {
       this.container.remove();
     }
 
+    this.accessibleInfobar.destroy();
+
+    this.accessibleInfobar = null;
     this.win = null;
   }
 }
 
 exports.XULWindowAccessibleHighlighter = XULWindowAccessibleHighlighter;
--- a/devtools/server/tests/browser/browser.ini
+++ b/devtools/server/tests/browser/browser.ini
@@ -1,14 +1,15 @@
 [DEFAULT]
 tags = devtools
 subsuite = devtools
 support-files =
   head.js
   animation.html
+  doc_accessibility_infobar.html
   doc_accessibility.html
   doc_allocations.html
   doc_force_cc.html
   doc_force_gc.html
   doc_innerHTML.html
   doc_perf.html
   error-actor.js
   grid.html
@@ -25,16 +26,18 @@ support-files =
   test-spawn-actor-in-parent.js
   timeline-iframe-child.html
   timeline-iframe-parent.html
   storage-helpers.js
   !/devtools/client/shared/test/shared-head.js
   !/devtools/client/shared/test/telemetry-test-helpers.js
   !/devtools/server/tests/mochitest/hello-actor.js
 
+[browser_accessibility_highlighter_infobar.js]
+[browser_accessibility_infobar_show.js]
 [browser_accessibility_node.js]
 [browser_accessibility_node_events.js]
 [browser_accessibility_simple.js]
 [browser_accessibility_walker.js]
 [browser_actor_error.js]
 [browser_animation_emitMutations.js]
 [browser_animation_getFrames.js]
 [browser_animation_getProperties.js]
new file mode 100644
--- /dev/null
+++ b/devtools/server/tests/browser/browser_accessibility_highlighter_infobar.js
@@ -0,0 +1,59 @@
+/* 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";
+
+// Test the accessible highlighter's infobar content.
+
+const { truncateString } = require("devtools/shared/inspector/utils");
+const { MAX_STRING_LENGTH } = require("devtools/server/actors/highlighters/utils/accessibility");
+
+add_task(async function() {
+  const { client, walker, accessibility } =
+    await initAccessibilityFrontForUrl(MAIN_DOMAIN + "doc_accessibility_infobar.html");
+
+  const a11yWalker = await accessibility.getWalker();
+  await accessibility.enable();
+
+  info("Button front checks");
+  await checkNameAndRole(walker, "#button", a11yWalker, "Accessible Button");
+
+  info("Front with long name checks");
+  await checkNameAndRole(walker, "#h1", a11yWalker,
+    "Lorem ipsum dolor sit ame" + "\u2026" + "e et dolore magna aliqua.");
+
+  await accessibility.disable();
+  await waitForA11yShutdown();
+  await client.close();
+  gBrowser.removeCurrentTab();
+});
+
+/**
+ * A helper function for testing the accessible's displayed name and roles.
+ *
+ * @param  {Object} walker
+ *         The DOM walker.
+ * @param  {String} querySelector
+ *         The selector for the node to retrieve accessible from.
+ * @param  {Object} a11yWalker
+ *         The accessibility walker.
+ * @param  {String} expectedName
+ *         Expected string content for displaying the accessible's name.
+ *         We are testing this in particular because name can be truncated.
+ */
+async function checkNameAndRole(walker, querySelector, a11yWalker, expectedName) {
+  const node = await walker.querySelector(walker.rootNode, querySelector);
+  const accessibleFront = await a11yWalker.getAccessibleFor(node);
+
+  const { name, role } = accessibleFront;
+  const onHighlightEvent = a11yWalker.once("highlighter-event");
+
+  await a11yWalker.highlightAccessible(accessibleFront);
+  const { options } = await onHighlightEvent;
+  is(options.name, name, "Accessible highlight has correct name option");
+  is(options.role, role, "Accessible highlight has correct role option");
+
+  is(`"${truncateString(name, MAX_STRING_LENGTH)}"`, `"${expectedName}"`,
+    "Accessible has correct displayed name.");
+}
new file mode 100644
--- /dev/null
+++ b/devtools/server/tests/browser/browser_accessibility_infobar_show.js
@@ -0,0 +1,162 @@
+/* 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";
+
+// Checks for the AccessibleHighlighter's and XULWindowHighlighter's infobar components.
+
+add_task(async function() {
+  await BrowserTestUtils.withNewTab({
+    gBrowser,
+    url: MAIN_DOMAIN + "doc_accessibility_infobar.html",
+  }, async function(browser) {
+    await ContentTask.spawn(browser, null, async function() {
+      const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm", {});
+      const { HighlighterEnvironment } = require("devtools/server/actors/highlighters");
+      const { AccessibleHighlighter } = require("devtools/server/actors/highlighters/accessible");
+      const { XULWindowAccessibleHighlighter } = require("devtools/server/actors/highlighters/xul-accessible");
+
+      /**
+       * Get whether or not infobar container is hidden.
+       *
+       * @param  {Object} infobar
+       *         Accessible highlighter's infobar component.
+       * @return {String|null} If the infobar container is hidden.
+       */
+      function isContainerHidden(infobar) {
+        return !!infobar.getElement("infobar-container").getAttribute("hidden");
+      }
+
+      /**
+       * Get name of accessible object.
+       *
+       * @param  {Object} infobar
+       *         Accessible highlighter's infobar component.
+       * @return {String} The text content of the infobar-name element.
+       */
+      function getName(infobar) {
+        return infobar.getTextContent("infobar-name");
+      }
+
+      /**
+       * Get role of accessible object.
+       *
+       * @param  {Object} infobar
+       *         Accessible highlighter's infobar component.
+       * @return {String} The text content of the infobar-role element.
+       */
+      function getRole(infobar) {
+        return infobar.getTextContent("infobar-role");
+      }
+
+      /**
+       * Checks for updated content for an infobar with valid bounds.
+       *
+       * @param  {Object} infobar
+       *         Accessible highlighter's infobar component.
+       * @param  {Object} options
+       *         Options to pass for the highlighter's show method.
+       *         Available options:
+       *         - {String} role
+       *           Role value of the accessible.
+       *         - {String} name
+       *           Name value of the accessible.
+       *         - {Boolean} shouldBeHidden
+       *           If the infobar component should be hidden.
+       */
+      function checkInfobar(infobar, { shouldBeHidden, role, name }) {
+        is(isContainerHidden(infobar), shouldBeHidden,
+          "Infobar's hidden state is correct.");
+
+        if (shouldBeHidden) {
+          return;
+        }
+
+        is(getRole(infobar), role, "infobarRole text content is correct");
+        is(getName(infobar), `"${name}"`, "infoBarName text content is correct");
+      }
+
+      /**
+       * Checks for updated content of an infobar with valid bounds.
+       *
+       * @param  {Element} node
+       *         Node to check infobar content on.
+       * @param  {Object}  highlighter
+       *         Accessible highlighter.
+       */
+      function testInfobar(node, highlighter) {
+        const infobar = highlighter.accessibleInfobar;
+        const bounds = {
+          x: 0,
+          y: 0,
+          w: 250,
+          h: 100,
+        };
+
+        info("Check that infobar is shown with valid bounds.");
+        highlighter.show(node, {
+          ...bounds,
+          role: "button",
+          name: "Accessible Button"
+        });
+
+        checkInfobar(infobar, {
+          role: "button",
+          name: "Accessible Button",
+          shouldBeHidden: false
+        });
+        highlighter.hide();
+
+        info("Check that infobar is hidden after .hide() is called.");
+        checkInfobar(infobar, { shouldBeHidden: true });
+
+        info("Check to make sure content is updated with new options.");
+        highlighter.show(node, {
+          ...bounds,
+          name: "Test link",
+          role: "link"
+        });
+        checkInfobar(infobar, {
+          name: "Test link",
+          role: "link",
+          shouldBeHidden: false
+        });
+        highlighter.hide();
+      }
+
+      // Start testing. First, create highlighter environment and initialize.
+      const env = new HighlighterEnvironment();
+      env.initFromWindow(content.window);
+
+      // Wait for loading highlighter environment content to complete before creating the
+      // highlighter.
+      await new Promise(resolve => {
+        const doc = env.document;
+
+        function onContentLoaded() {
+          if (doc.readyState === "interactive" || doc.readyState === "complete") {
+            resolve();
+          } else {
+            doc.addEventListener("DOMContentLoaded", onContentLoaded, { once: true });
+          }
+        }
+
+        onContentLoaded();
+      });
+
+      // Now, we can test the Infobar and XULWindowInfobar components with their
+      // respective highlighters.
+      const node = content.document.createElement("div");
+      content.document.body.append(node);
+
+      info("Checks for Infobar's show method");
+      const highlighter = new AccessibleHighlighter(env);
+      testInfobar(node, highlighter);
+
+      info("Checks for XULWindowInfobar's show method");
+      const xulWindowHighlighter = new XULWindowAccessibleHighlighter(env);
+      testInfobar(node, xulWindowHighlighter);
+    });
+  });
+});
new file mode 100644
--- /dev/null
+++ b/devtools/server/tests/browser/doc_accessibility_infobar.html
@@ -0,0 +1,10 @@
+<!DOCTYPE HTML>
+<html>
+  <head>
+    <meta charset="utf-8">
+  </head>
+<body>
+  <h1 id="h1">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</h1>
+  <button id="button">Accessible Button</button>
+</body>
+</html>
--- a/devtools/shared/inspector/moz.build
+++ b/devtools/shared/inspector/moz.build
@@ -1,9 +1,10 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
 DevToolsModules(
-    'css-logic.js'
+    'css-logic.js',
+    'utils.js'
 )
new file mode 100644
--- /dev/null
+++ b/devtools/shared/inspector/utils.js
@@ -0,0 +1,20 @@
+/* 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";
+
+/**
+ * Truncate the string and add ellipsis to the middle of the string.
+ */
+function truncateString(str, maxLength) {
+  if (!str || str.length <= maxLength) {
+    return str;
+  }
+
+  return str.substring(0, Math.ceil(maxLength / 2)) +
+        "…" +
+        str.substring(str.length - Math.floor(maxLength / 2));
+}
+
+exports.truncateString = truncateString;
--- a/devtools/shared/specs/accessibility.js
+++ b/devtools/shared/specs/accessibility.js
@@ -99,16 +99,20 @@ const accessibleWalkerSpec = generateAct
       accessible: Arg(0, "nullable:accessible")
     },
     "picker-accessible-hovered": {
       type: "pickerAccessibleHovered",
       accessible: Arg(0, "nullable:accessible")
     },
     "picker-accessible-canceled": {
       type: "pickerAccessibleCanceled"
+    },
+    "highlighter-event": {
+      type: "highlighter-event",
+      data: Arg(0, "json")
     }
   },
 
   methods: {
     children: {
       request: {},
       response: {
         children: RetVal("array:accessible")