Bug 1473030 - Show accessible object's name and role information with the info-bar highlighter. r=gl,yzen
authorMicah Tigley <mtigley@mozilla.com>
Thu, 16 Aug 2018 00:35:02 -0400
changeset 431690 b0ac567c436c
parent 431689 9c569226e852
child 431691 138014f66617
push id67828
push userdluca@mozilla.com
push dateThu, 16 Aug 2018 06:45:00 +0000
treeherderautoland@b0ac567c436c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgl, yzen
bugs1473030
milestone63.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1473030 - Show accessible object's name and role information with the info-bar highlighter. r=gl,yzen 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")