Bug1473030 - Show accessible object's name and role information with info-bar highlight. r=yzen draft
authorMicah Tigley <mtigley@mozilla.com>
Wed, 04 Jul 2018 09:52:40 -0400
changeset 816214 a08f23c05a4771efd66d14bf79c1edeaa227a47b
parent 813748 9a2b02fe351bd65f6850a5c80b91dd0eec4a878a
push id115777
push userbmo:mtigley@mozilla.com
push dateTue, 10 Jul 2018 18:35:06 +0000
reviewersyzen
bugs1473030
milestone63.0a1
Bug1473030 - Show accessible object's name and role information with info-bar highlight. r=yzen MozReview-Commit-ID: A7yeUbY5RxR
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/doc_accessibility_infobar.html
devtools/shared/specs/accessibility.js
--- a/devtools/server/actors/accessibility.js
+++ b/devtools/server/actors/accessibility.js
@@ -381,19 +381,22 @@ 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.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,16 +439,17 @@ const AccessibleWalkerActor = ActorClass
     this.setA11yServiceGetter();
   },
 
   destroy() {
     Actor.prototype.destroy.call(this);
 
     this.reset();
 
+    this.highlighter.removeEventListener("highlighter-event", this.onHighlighterEvent);
     this.highlighter.destroy();
     this.highlighter = null;
 
     this.targetActor = null;
     this.refMap = null;
   },
 
   getRef(rawAccessible) {
@@ -580,16 +584,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 +703,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 +796,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
@@ -640,8 +640,24 @@
 }
 
 /* Accessible highlighter */
 
 :-moz-native-anonymous .accessible-bounds {
   opacity: 0.6;
   fill: #6a5acd;
 }
+
+:-moz-native-anonymous .accessible-infobar-name {
+  color:hsl(210, 30%, 85%);
+  max-width: 90%;
+}
+
+:-moz-native-anonymous .accessible-infobar-name:not(:empty) {
+  color: hsl(210, 30%, 85%);
+  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,21 @@
 /* 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, AccessibleChecker } = require("./utils/accessibility");
 const {
   CanvasFrameAnonymousContentHelper,
   createNode,
-  createSVGNode
+  createSVGNode,
+  moveInfobar,
 } = require("./utils/markup");
 
 const { setIgnoreLayoutChanges } = require("devtools/shared/layout/utils");
 
 /**
  * The AccessibleHighlighter draws the bounds of an accessible object.
  *
  * Usage example:
@@ -31,23 +31,35 @@ 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 | null} role
+ *           role of the the accessible object
  *
  * Structure:
  * <div class="highlighter-container">
  *   <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-";
@@ -107,16 +119,66 @@ class AccessibleHighlighter extends Auto
       attributes: {
         "class": "bounds",
         "id": "bounds",
         "role": "presentation"
       },
       prefix: this.ID_CLASS_PREFIX
     });
 
+    // Build the accessible's infobar markup
+    const accessibleInfobarContainer = createNode(this.win, {
+      parent: container,
+      attributes: {
+        "class": "infobar-container",
+        "id": "infobar-container",
+        "aria-hidden": "true",
+        "hidden": "true",
+      },
+      prefix: this.ID_CLASS_PREFIX
+    });
+
+    const accessibleInfobar = createNode(this.win, {
+      parent: accessibleInfobarContainer,
+      attributes: {
+        "class": "infobar",
+        "id": "infobar",
+      },
+      prefix: this.ID_CLASS_PREFIX
+    });
+
+    const accessibleInfobarText = createNode(this.win, {
+      parent: accessibleInfobar,
+      attributes: {
+        "class": "infobar-text",
+        "id": "infobar-text",
+      },
+      prefix: this.ID_CLASS_PREFIX
+    });
+
+    createNode(this.win, {
+      nodeType: "span",
+      parent: accessibleInfobarText,
+      attributes: {
+        "class": "infobar-role",
+        "id": "infobar-role",
+      },
+      prefix: this.ID_CLASS_PREFIX
+    });
+
+    createNode(this.win, {
+      nodeType: "span",
+      parent: accessibleInfobarText,
+      attributes: {
+        "class": "infobar-name",
+        "id": "infobar-name",
+      },
+      prefix: this.ID_CLASS_PREFIX
+    });
+
     return container;
   }
 
   /**
    * Destroy the nodes. Remove listeners.
    */
   destroy() {
     if (this._highlightTimer) {
@@ -151,35 +213,44 @@ 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();
+
+      // Show accessible's infobar
+      this._showAccessibleInfobar();
+      this._updateAccessibleInfobar();
+
       shown = true;
     } else {
       // Nothing to highlight (0px rectangle like a <script> tag for instance)
       this.hide();
     }
 
     setIgnoreLayoutChanges(false,
                            this.highlighterEnv.window.document.documentElement);
@@ -188,35 +259,57 @@ class AccessibleHighlighter extends Auto
   }
 
   /**
    * Hide the highlighter.
    */
   _hide() {
     setIgnoreLayoutChanges(true);
     this._hideAccessibleBounds();
+    this._hideAccessibleInfobar();
     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");
   }
 
   /**
+   * Hide the accessible infobar container.
+   */
+  _hideAccessibleInfobar() {
+    this.getElement("infobar-container").setAttribute("hidden", "true");
+  }
+
+  /**
+   * Hide the accessible infobar information content.
+   */
+  _hideAccessibleContent() {
+    this.getElement("infobar-name").setAttribute("hidden", "true");
+  }
+
+  /**
+   * Show the accessible infobar container.
+   */
+  _showAccessibleInfobar() {
+    this.getElement("infobar-container").removeAttribute("hidden");
+  }
+
+  /**
    * Get current accessible bounds.
    *
    * @return {Object|null} Returns, if available, positioning and bounds
    *                       information for the accessible object.
    */
   get _bounds() {
     return getBounds(this.win, this.options);
   }
@@ -225,17 +318,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);
@@ -243,16 +336,39 @@ class AccessibleHighlighter extends Auto
     // Un-zoom the root wrapper if the page was zoomed.
     const rootId = this.ID_CLASS_PREFIX + "elements";
     this.markup.scaleRootElement(this.currentNode, rootId);
 
     return true;
   }
 
   /**
+   * Update accessible information displayed for current accessible's infobar.
+   *
+   */
+  _updateAccessibleInfobar() {
+    // Refresh infobar content.
+    this._hideAccessibleContent();
+
+    // Initially, show only the accessible's role and name. In future patches,
+    // we will be able to show messages for contrast ratio and more.
+    this.showAccessibleRole();
+    this.showAccessibleNameInfo();
+
+    const container = this.getElement("infobar-container");
+
+    // Position the infobar using accessible's bounds
+    const bounds = this._bounds;
+    const { left: x, top: y, bottom, width } = bounds;
+    const infobarBounds = { x, y, bottom, width };
+
+    moveInfobar(container, infobarBounds, this.win);
+  }
+
+  /**
    * Hide highlighter on page hide.
    */
   onPageHide({ target }) {
     // If a pagehide event is triggered for current window's highlighter, hide
     // the highlighter.
     if (target.defaultView === this.win) {
       this.hide();
     }
@@ -261,11 +377,39 @@ class AccessibleHighlighter extends Auto
   /**
    * Hide highlighter on navigation.
    */
   onWillNavigate({ isTopLevel }) {
     if (isTopLevel) {
       this.hide();
     }
   }
+
+  /**
+   * Show the accessible's name message.
+   */
+  showAccessibleNameInfo() {
+    const Check = new AccessibleChecker(this.options);
+    const { warning, message: nameMessage } = Check.evaluateAccessibleName();
+    let nameText =  nameMessage;
+
+    if (warning) {
+      this.getElement("infobar-name").setAttribute("style", "color: #DCDDA4");
+      nameText = "\u26A0 " + nameMessage;
+    } else {
+      this.getElement("infobar-name").removeAttribute("style");
+    }
+
+    this.getElement("infobar-name").setTextContent(nameText);
+    this.getElement("infobar-name").removeAttribute("hidden");
+  }
+
+  /**
+   * Show the accessible's role.
+   */
+  showAccessibleRole() {
+    const { role } = this.options;
+    const roleText = role ? `${role}` : "";
+    this.getElement("infobar-role").setTextContent(roleText);
+  }
 }
 
 exports.AccessibleHighlighter = AccessibleHighlighter;
--- a/devtools/server/actors/highlighters/utils/accessibility.js
+++ b/devtools/server/actors/highlighters/utils/accessibility.js
@@ -1,15 +1,15 @@
 /* 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");
 
 /**
  * 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
@@ -53,9 +53,160 @@ 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 };
 }
 
+/**
+ * A helper function that calculates the positioning of a XUL accessible's infobar.
+ *
+ * @param  {Object} win
+ *         Window that contains accessible object.
+ * @param  {Object} container
+ *         The infobar container
+ * @param  {Object} arrowEl
+ *         The arrow tip element to indicate infobar direction.
+ * @param  {Object} bounds
+ *         The bounds information of accessible object.
+ *
+ */
+function moveInfobar(win, container, arrowEl, bounds) {
+  const zoom = getCurrentZoom(win);
+  let { width: viewportWidth, height: viewportHeight } = getViewportDimensions(win);
+  const { width, height, left, } = container.getBoundingClientRect();
+  const {
+    left: boundsLeft,
+    right: boundsRight,
+    top: boundsTop,
+    bottom: boundsBottom} = bounds;
+  const arrowSize = 8;
+
+  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`;
+    arrowEl.setAttribute("hidden", "true");
+  } else if (isOffScreenOnRight) {
+    const leftOffset = rightBoundary - boundsRight;
+    container.style.left = `${rightBoundary - leftOffset - containerHalfWidth}px`;
+    arrowEl.setAttribute("hidden", "true");
+  }
+
+  // Check if infobar is offscreen on either top/bottom of viewport and position.
+  if (isOffScreenOnTop) {
+    if (boundsTop < 0) {
+      container.style.top = "8px";
+    } else {
+      container.style.top = `${boundsBottom + arrowSize}px`;
+    }
+    arrowEl.setAttribute("class", "arrow top");
+  } else if (isOffScreenOnBottom) {
+    container.style.top = `${bottomBoundary - arrowSize}px`;
+    arrowEl.setAttribute("hidden", "true");
+  } else {
+    container.style.top = `${boundsTop - (containerHeight + arrowSize)}px`;
+    arrowEl.setAttribute("class", "arrow bottom");
+  }
+}
+
+/**
+ * A helper function that truncates text.
+ *
+ * @param  {String} str
+ *         The text to truncate.
+ * @param  {Number} maxLength
+ *         The number of characters to truncate with. Default is 50 chars.
+ *
+ * @return {String} The truncated text.
+ */
+function truncateText(str, maxLength = 50) {
+  if (str.length < maxLength) {
+    return str;
+  }
+
+  const chars = Math.floor(maxLength / 2);
+  const truncatedString = `${str.slice(0, chars)}...${str.slice(-chars)}`;
+  return truncatedString.trim();
+}
+
+/**
+ * The AccessibleChecker generates information about an accessible's options for
+ * displaying.
+ */
+class AccessibleChecker {
+  constructor(accessibleOptions) {
+    this.options = accessibleOptions;
+  }
+
+  get name() {
+    return this.options.name;
+  }
+
+  get role() {
+    return this.options.role;
+  }
+
+  /**
+   * Evaluates current accessible's name value and returns a message to display for
+   * the accessible's name property. In addition, a flag is returned indicating
+   * whether or not the message is a warning.
+   *
+   * @return {Object}
+   *         Object containing the display message of accessible's name value and a
+   *         warning flag.
+   *         - {Boolean} hasWarning
+   *            A flag indicating that the message being returned is a warning.
+   *         - {String} message
+   *           The message to display for an accessible's name.
+   *
+   */
+  evaluateAccessibleName() {
+    // Future patches will allow for displaying warning messages for a current accessible.
+    const hasWarning = false;
+    const message = this.hasNameProperty() ? `"${truncateText(this.name)}"` : "";
+
+    return { hasWarning, message };
+  }
+
+  /**
+   * Determines if an accessible object has a name.
+   *
+   * @return {boolean}
+   *         A flag indicating that the accessible has a name property.
+   */
+  hasNameProperty() {
+    const name = this.name;
+
+    // Check if "name" is null or undefined. We explicitly check this since name can be
+    // an empty string.
+    if (name === null || name === undefined) {
+      return false;
+    }
+
+    return true;
+  }
+}
+
+exports.AccessibleChecker = AccessibleChecker;
 exports.getBounds = getBounds;
+exports.moveInfobar = moveInfobar;
+exports.truncateText = truncateText;
--- a/devtools/server/actors/highlighters/xul-accessible.js
+++ b/devtools/server/actors/highlighters/xul-accessible.js
@@ -1,30 +1,88 @@
 /* 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, AccessibleChecker, moveInfobar } = 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(`
   .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: hsl(214, 13%, 24%);
+    min-width: 75px;
+    border: 1px solid rgba(255, 255, 255, 0.2);
+    border-radius: 3px;
+    padding: 5px;
+  }
+
+  .arrow {
+    position: absolute;
+    width: 0; 
+    height: 0; 
+    border-left: 8px solid transparent;
+    border-right: 8px solid transparent;
+    left: calc(50% - 8px);
+  }
+
+  .top {
+    border-bottom: 8px solid hsl(214, 13%, 24%);
+    top: -8px;
+  }
+
+  .bottom {
+    border-top: 8px solid hsl(214, 13%, 24%);
+    bottom: -8px;
+  }
+
+  .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
@@ -66,16 +124,64 @@ class XULWindowAccessibleHighlighter {
 
     this.bounds = createNode(this.win, {
       parent: this.container,
       attributes: {
         "class": "accessible-bounds",
         "role": "presentation"
       }
     });
+
+    this.infobarContainer = createNode(this.win, {
+      parent: this.container,
+      attributes: {
+        "class": "accessible-infobar-container",
+        "aria-hidden": "true"
+      },
+    });
+
+    this.infobar = createNode(this.win, {
+      parent: this.infobarContainer,
+      attributes: {
+        "class": "accessible-infobar"
+      }
+    });
+
+    this.arrow = createNode(this.win, {
+      parent: this.infobar,
+      attributes: {
+        "class": "arrow"
+      }
+    });
+
+    this.infobarText = createNode(this.win, {
+      parent: this.infobar,
+      attributes: {
+        "class": "accessible-infobar-text",
+        "id": "accessible-infobar-text"
+      }
+    });
+
+    this.infobarRole = createNode(this.win, {
+      nodeType: "span",
+      parent: this.infobarText,
+      attributes: {
+        "class": "accessible-infobar-role",
+        "id": "accessible-infobar-role",
+      },
+    });
+
+    this.infobarName = createNode(this.win, {
+      nodeType: "span",
+      parent: this.infobarText,
+      attributes: {
+        "class": "accessible-infobar-name",
+        "id": "accessible-infobar-name",
+      },
+    });
   }
 
   /**
    * Get current accessible bounds.
    *
    * @return {Object|null} Returns, if available, positioning and bounds
    *                       information for the accessible object.
    */
@@ -99,16 +205,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 | null} 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,27 +264,30 @@ 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._updateAccessibleInfobar(bounds);
 
     return true;
   }
 
   /**
    * Hide the highlighter
    */
   hide() {
@@ -200,26 +314,91 @@ class XULWindowAccessibleHighlighter {
    */
   _hideAccessibleBounds() {
     if (this.container) {
       this.container.setAttribute("hidden", "true");
     }
   }
 
   /**
+   * Hide infobar's info
+   */
+  _hideAccessibleContent() {
+    this.infobarName.setAttribute("hidden", "true");
+  }
+
+  /**
+   * Update accessible infobar for a current accessible.
+   *
+   */
+  _updateAccessibleInfobar(bounds) {
+    if (!bounds) {
+      return false;
+    }
+
+    this.arrow.removeAttribute("hidden");
+    this._hideAccessibleContent();
+    const { left, right } = bounds;
+
+    // Set the position values of the infobar container in relation to
+    // highlighter's bounds position.
+    const infobarEl = this.infobarContainer;
+    const boundsMidPoint = (left + right) / 2;
+
+    infobarEl.style.left = `${boundsMidPoint}px`;
+
+    // Set the text content of the infobar.
+    this.showAccessibleRole();
+    this.showAccessibleNameInfo();
+
+    // Position the infobar.
+    moveInfobar(this.win, infobarEl, this.arrow, bounds);
+
+    return true;
+  }
+
+  /**
    * Hide accessible highlighter, clean up and remove the markup.
    */
   destroy() {
     if (this._highlightTimer) {
       clearTimeout(this._highlightTimer);
       this._highlightTimer = null;
     }
 
     this.hide();
     if (this.container) {
       this.container.remove();
     }
 
     this.win = null;
   }
+
+  /**
+   * Show the accessible's name message.
+   */
+  showAccessibleNameInfo() {
+    const Check = new AccessibleChecker(this.options);
+    const { warning, message: nameMessage } = Check.evaluateAccessibleName();
+    let nameText =  nameMessage;
+
+    if (warning) {
+      this.infobarName.setAttribute("style", "color: #DCDDA4");
+      nameText = "\u26A0 " + nameMessage;
+    } else {
+      this.infobarName.removeAttribute("style");
+    }
+
+    this.infobarName.textContent = nameText;
+    this.infobarName.removeAttribute("hidden");
+  }
+
+  /**
+   * Show the accessible's role.
+   */
+  showAccessibleRole() {
+    const { role } = this.options;
+    const roleText = role ? `${role}` : "";
+    this.infobarRole.textContent = roleText;
+  }
 }
 
 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
@@ -24,16 +25,17 @@ support-files =
   stylesheets-nested-iframes.html
   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_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,64 @@
+/* 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 { AccessibleChecker } = require("devtools/server/actors/highlighters/utils/accessibility");
+
+ // Checks for the accessible highlighter's infobar content.
+
+ 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");
+   const buttonNode = await walker.querySelector(walker.rootNode, "#button");
+   const accessibleButtonFront = await a11yWalker.getAccessibleFor(buttonNode);
+   let { name, role } = accessibleButtonFront;
+   let onHighlightEvent = a11yWalker.once("highlighter-event");
+
+   await a11yWalker.highlightAccessible(accessibleButtonFront);
+   let response = await onHighlightEvent;
+   let options = response.options;
+   is(options.name, name, "Button highlight has correct name option");
+   is(options.role, role, "Button highlight has correct role option");
+
+   const ButtonChecker = new AccessibleChecker(options);
+   const { hasWarning: buttonWarning, message: buttonMessage } =
+    ButtonChecker.evaluateAccessibleName();
+
+   is(buttonMessage, "\"Accessible Button\"", "Button accessible has correct name.");
+   is(buttonWarning, false, "Button name's message is not a warning.");
+
+   info("Front with long name checks");
+   const headerNode =
+    await walker.querySelector(walker.rootNode, "#h1");
+   const headerFront = await a11yWalker.getAccessibleFor(headerNode);
+
+   name = headerFront.name;
+   role = headerFront.role;
+
+   onHighlightEvent = a11yWalker.once("highlighter-event");
+   await a11yWalker.highlightAccessible(headerFront);
+   response = await onHighlightEvent;
+   options = response.options;
+   is(options.name, name, "Header highlight has correct name option");
+   is(options.role, role, "Header highlight has correct role option");
+
+   const HeaderChecker = new AccessibleChecker(options);
+   const { hasWarning: h1Warning, message: h1Message } =
+   HeaderChecker.evaluateAccessibleName();
+
+   is(h1Message, "\"Lorem ipsum dolor sit ame...e et dolore magna aliqua.\"",
+    "H1 accessible has correct name.");
+   is(h1Warning, false, "H1 message is not a warning.");
+
+   await accessibility.disable();
+   await waitForA11yShutdown();
+   await client.close();
+   gBrowser.removeCurrentTab();
+ });
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/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")