Bug 1151468 - Add accessibility inspection functionality to our DevTools r?yzen draft
authorNancy Pang <npang@mozilla.com>
Wed, 27 Jul 2016 09:45:10 -0400
changeset 393265 96d7e7294e1f44cd51c15f31cf9491d492ff44e8
parent 393264 f0d2679582bea4d7d74ea47d4b1f7f458b1bd359
child 526547 f14edd883f9763c1e250e6a22bfd3f7f5dcfe517
push id24270
push userbmo:npang@mozilla.com
push dateWed, 27 Jul 2016 14:28:04 +0000
reviewersyzen
bugs1151468
milestone50.0a1
Bug 1151468 - Add accessibility inspection functionality to our DevTools r?yzen MozReview-Commit-ID: 2TyGPCN4821
devtools/client/inspector/accessibility/accessibility.js
devtools/client/inspector/accessibility/moz.build
devtools/client/inspector/inspector-panel.js
devtools/client/inspector/inspector.xul
devtools/client/inspector/moz.build
devtools/client/inspector/shared/style-inspector-menu.js
devtools/client/inspector/shared/style-inspector-overlays.js
devtools/client/jar.mn
devtools/client/locales/en-US/inspector.properties
devtools/client/locales/en-US/styleinspector.dtd
devtools/client/shared/output-parser.js
devtools/client/themes/accessibility.css
devtools/client/themes/inspector.css
devtools/shared/locales/en-US/accessibilityinspector.properties
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/accessibility/accessibility.js
@@ -0,0 +1,1283 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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/. */
+
+/* globals StopIteration */
+
+"use strict";
+
+const {Cc, Ci, Cu} = require("chrome");
+
+const promise = require("promise");
+const Services = require("Services");
+const {OutputParser} = require("devtools/client/shared/output-parser");
+const {PREF_ORIG_SOURCES} = require("devtools/client/styleeditor/utils");
+const {createChild} = require("devtools/client/inspector/shared/utils");
+const {gDevTools} = require("devtools/client/framework/devtools");
+const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm");
+
+const nsIAccessibleRetrieval = Ci.nsIAccessibleRetrieval;
+const nsIDOMNode = Ci.nsIDOMNode;
+const nsIAccessible = Ci.nsIAccessible;
+
+/**
+ * nsIAccessibleRetrieval service.
+ */
+const gAccRetrieval = Cc["@mozilla.org/accessibleRetrieval;1"]
+  .getService(nsIAccessibleRetrieval);
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
+                                  "resource://gre/modules/PluralForm.jsm");
+
+XPCOMUtils.defineLazyGetter(CssAccessibilityView, "_strings", function () {
+  return Services.strings.createBundle(
+    "chrome://devtools-shared/locale/accessibilityinspector.properties");
+});
+
+XPCOMUtils.defineLazyGetter(this, "clipboardHelper", function () {
+  return Cc["@mozilla.org/widget/clipboardhelper;1"]
+         .getService(Ci.nsIClipboardHelper);
+});
+
+const FILTER_CHANGED_TIMEOUT = 150;
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+
+/**
+ * Helper for long-running processes that should yield occasionally to
+ * the mainloop.
+ *
+ * @param {Window} win
+ *        Timeouts will be set on this window when appropriate.
+ * @param {Generator} generator
+ *        Will iterate this generator.
+ * @param {Object} options
+ *        Options for the update process:
+ *          onItem {function} Will be called with the value of each iteration.
+ *          onBatch {function} Will be called after each batch of iterations,
+ *            before yielding to the main loop.
+ *          onDone {function} Will be called when iteration is complete.
+ *          onCancel {function} Will be called if the process is canceled.
+ *          threshold {int} How long to process before yielding, in ms.
+ */
+function UpdateProcess(win, generator, options) {
+  this.win = win;
+  this.iter = _Iterator(generator);
+  this.onItem = options.onItem || function () {};
+  this.onBatch = options.onBatch || function () {};
+  this.onDone = options.onDone || function () {};
+  this.onCancel = options.onCancel || function () {};
+  this.threshold = options.threshold || 45;
+
+  this.canceled = false;
+}
+
+UpdateProcess.prototype = {
+  /**
+   * Schedule a new batch on the main loop.
+   */
+  schedule: function () {
+    if (this.canceled) {
+      return;
+    }
+    this._timeout = setTimeout(this._timeoutHandler.bind(this), 0);
+  },
+
+  /**
+   * Cancel the running process.  onItem will not be called again,
+   * and onCancel will be called.
+   */
+  cancel: function () {
+    if (this._timeout) {
+      clearTimeout(this._timeout);
+      this._timeout = 0;
+    }
+    this.canceled = true;
+    this.onCancel();
+  },
+
+  _timeoutHandler: function () {
+    this._timeout = null;
+    try {
+      this._runBatch();
+      this.schedule();
+    } catch (e) {
+      if (e instanceof StopIteration) {
+        this.onBatch();
+        this.onDone();
+        return;
+      }
+      console.error(e);
+      throw e;
+    }
+  },
+
+  _runBatch: function () {
+    let time = Date.now();
+    while (!this.canceled) {
+      // Continue until iter.next() throws...
+      let next = this.iter.next();
+      this.onItem(next[1]);
+      if ((Date.now() - time) > this.threshold) {
+        this.onBatch();
+        return;
+      }
+    }
+  }
+};
+
+/**
+ * CssAccessibilityView is a panel that manages the display of a table
+ * showing accessibility attributes. There should be one instance of CssAccessbilityView
+ * per style display (of which there will generally only be one).
+ *
+ * @param {Inspector} inspector
+ *        Inspector toolbox panel
+ * @param {Document} document
+ *        The document that will contain the accesibilty view.
+ * @param {PageStyleFront} pageStyle
+ *        Front for the page style actor that will be providing
+ *        the style information.
+ */
+function CssAccessibilityView(inspector, document, pageStyle) {
+
+  this.inspector = inspector;
+  this.styleDocument = document;
+  this.styleWindow = this.styleDocument.defaultView;
+  this.pageStyle = pageStyle;
+
+  this.propertyViews = [];
+
+  this._outputParser = new OutputParser(document);
+
+  let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"]
+    .getService(Ci.nsIXULChromeRegistry);
+  this.getRTLAttr = chromeReg.isLocaleRTL("global") ? "rtl" : "ltr";
+
+  // Bounded Panel Methods for focusing, copying and searching
+  this.focusWindow = this.focusWindow.bind(this);
+  this._onKeypress = this._onKeypress.bind(this);
+  this._onCopy = this._onCopy.bind(this);
+  this._onFilterProps = this._onFilterProps.bind(this);
+  this._onFilterKeyPress = this._onFilterKeyPress.bind(this);
+  this._onClearSearch = this._onClearSearch.bind(this);
+
+  // Identify Panel containers and search elements
+  let doc = this.styleDocument;
+  this.root = doc.getElementById("root");
+  this.element = doc.getElementById("accessibilityPropertyContainer");
+  this.searchField = doc.getElementById("accessibilityview-searchbox");
+  this.searchClearButton = doc.getElementById("accessibilityview-searchinput-clear");
+
+  this.styleDocument.addEventListener("keypress", this._onKeypress);
+  this.styleDocument.addEventListener("mousedown", this.focusWindow);
+  this.element.addEventListener("copy", this._onCopy);
+  this.searchField.addEventListener("input", this._onFilterProps);
+  this.searchField.addEventListener("keypress", this._onFilterKeyPress);
+  this.searchClearButton.addEventListener("click", this._onClearSearch);
+
+  this.searchClearButton.hidden = true;
+
+  // Message displayed when node doesn't have any properties or no search results matched
+  this.noResults = this.styleDocument.getElementById("accessibilityNoResults");
+
+  // Refresh panel when color theme is changed
+  this._handlePrefChange = this._handlePrefChange.bind(this);
+  gDevTools.on("pref-changed", this._handlePrefChange);
+
+  // The current element being inspected
+  this.viewedElement = null;
+}
+
+/**
+ * Memoized lookup of a l10n string from a string bundle.
+ *
+ * @param {String} name
+ *        The key to lookup.
+ * @returns {String} localized version of the given key.
+ */
+CssAccessibilityView.l10n = function (name) {
+  try {
+    return CssAccessibilityView._strings.GetStringFromName(name);
+  } catch (ex) {
+    console.log("Error reading '" + name + "'");
+    throw new Error("l10n error with " + name);
+  }
+};
+
+CssAccessibilityView.prototype = {
+  // Used for cancelling timeouts in the search filter.
+  _filterChangedTimeout: null,
+
+  // Number of visible properties
+  numProperties: 0,
+
+  _accessibleName: null,
+
+  /**
+  *  Retrieve accessible properties for inspected node and
+  *  add properties to CssAccessibilityView
+  */
+  addAccessibleProperties: function () {
+    if (!CssAccessibilityView.propertyNames) {
+      CssAccessibilityView.propertyNames = [];
+    }
+
+    let accprops = this.accessibleProperties();
+    let newprops = [];
+
+    for (let property in accprops) {
+      if (this.isNewProperty(property)) {
+        newprops.push(property);
+      }
+    }
+
+    for (let property of newprops) {
+      CssAccessibilityView.propertyNames.push(
+        {
+          name: property,
+          element: this.viewedElement
+        });
+    }
+
+    CssAccessibilityView.propertyNames.sort();
+  },
+
+  /**
+   *  Check if property is new and needs to be added
+   *
+   * @param String name
+   *        The Accessible Property Name
+   * @returns bool
+   *        Whether the property is new or not
+   */
+  isNewProperty: function (name) {
+    for (let key in CssAccessibilityView.propertyNames) {
+      let property = CssAccessibilityView.propertyNames[key];
+      if (this.viewedElement === property.element) {
+        if (property.name === name) {
+          return false;
+        }
+      }
+    }
+
+    return true;
+  },
+
+  /**
+  * Get all accessible properties for the current element
+  *
+  * @returns Object
+  *        The accessible properties found for the inspected element
+  */
+  accessibleProperties: function () {
+    let currentAccProps = {};
+
+    if (this.viewedElement instanceof nsIDOMNode) {
+      let acc = gAccRetrieval.getAccessibleFor(this.viewedElement);
+
+      if (acc instanceof nsIAccessible) {
+        // GET NAME from Aria Label and Title
+        this.setPropName(currentAccProps, acc);
+        this.setPropRole(currentAccProps, acc);
+        this.setPropActions(currentAccProps, acc);
+        this.setPropGroupPosition(currentAccProps, acc);
+        this.setPropStates(currentAccProps, acc);
+        this.setPropAria(currentAccProps, acc);
+        this.setPropBounds(currentAccProps, acc);
+      }
+    }
+
+    return currentAccProps;
+  },
+
+  /**
+  * Set Property Name for accessible properties object
+  *
+  * @param Object props
+  *        Current accessible properties object
+  * @param Object acc
+  *        Accessible Retrieval Object
+  */
+  setPropName: function (props, acc) {
+    let name = this._accessibleName = acc.name;
+    let title = this.viewedElement.title;
+    let arialabel = this.getAriaPropByName(acc.DOMNode.attributes, "aria-label");
+
+    if (name) {
+      props.Name = {};
+      props.Name["displayed-name"] = name;
+    }
+
+    if (title) {
+      props.Name["title"] = title;
+    }
+
+    if (arialabel) {
+      props.Name["aria-label"] = arialabel;
+    }
+  },
+
+  /**
+  * Set Property Role for accessible properties object
+  *
+  * @param Object props
+  *        Current accessible properties object
+  * @param Object acc
+  *        Accessible Retrieval Object
+  */
+  setPropRole: function (props, acc) {
+    let role = gAccRetrieval.getStringRole(acc.role);
+    if (role) {
+      props.Role = {"role": role};
+    }
+  },
+
+  /**
+  * Set Property Actions for accessible properties object
+  *
+  * @param Object props
+  *        Current accessible properties object
+  * @param Object acc
+  *        Accessible Retrieval Object
+  */
+  setPropActions: function (props, acc) {
+    if (acc.actionCount > 0) {
+      props.Actions = {};
+      for (let i = 0; i < acc.actionCount; i++) {
+        props.Actions["unlabelled" + i.toString()] = acc.getActionDescription(i);
+      }
+    }
+  },
+
+  /**
+  * Set Group Position for accessible properties object
+  *
+  * @param Object props
+  *        Current accessible properties object
+  * @param Object acc
+  *        Accessible Retrieval Object
+  */
+  setPropGroupPosition: function (props, acc) {
+    let itemno = {}, itemof = {};
+    acc.groupPosition({}, itemof, itemno);
+    if (itemno.value !== 0 && itemof.value !== 0) {
+      props["Group Position"] = {position: itemno.value + " of " + itemof.value};
+    }
+  },
+
+  /**
+  * Set Property States for accessible properties object
+  *
+  * @param Object props
+  *        Current accessible properties object
+  * @param Object acc
+  *        Accessible Retrieval Object
+  */
+  setPropStates: function (props, acc) {
+    let state = {}, extraState = {};
+    acc.getState(state, extraState);
+    let states = gAccRetrieval.getStringStates(state.value, extraState.value);
+    if (states) {
+      props.States = {};
+      for (let i = 0; i < states.length; i++) {
+        props.States["unlabelled" + i.toString()] = states[i];
+      }
+    }
+  },
+
+  /**
+  * Set Property Bounds for accessible properties object
+  *
+  * @param Object props
+  *        Current accessible properties object
+  * @param Object acc
+  *        Accessible Retrieval Object
+  */
+  setPropBounds: function (props, acc) {
+    let objX = {}, objY = {}, objW = {}, objH = {};
+    acc.getBounds(objX, objY, objW, objH);
+
+    let bounds = objX.value != null &&
+      objY.value != null &&
+      objW.value != null &&
+      objH.value != null;
+
+    if (bounds) {
+      props.Bounds = {
+        x: objX.value.toString(),
+        y: objY.value.toString(),
+        width: objW.value.toString(),
+        height: objH.value.toString()
+      };
+    }
+  },
+
+  /**
+  * Set Property Aria fields for accessible properties object
+  *
+  * @param Object props
+  *        Current accessible properties object
+  * @param Object acc
+  *        Accessible Retrieval Object
+  */
+  setPropAria: function (props, acc) {
+    let attributes = acc.DOMNode.attributes;
+    let ariaProps = this.getAriaProps(attributes);
+    if (ariaProps) {
+      props.Aria = ariaProps;
+    }
+  },
+
+  getAriaPropByName: function (attributes, name) {
+    let ariaprop = "";
+
+    for (let i = attributes.length - 1; i >= 0; i--) {
+      if (attributes[i].name.startsWith(name)) {
+        ariaprop = attributes[i].value;
+      }
+    }
+    return ariaprop;
+  },
+
+  getAriaProps: function (attributes) {
+    let aria = {};
+    for (let i = 0, numAria = attributes.length; i < numAria; i++) {
+      let prop = attributes.item(i);
+      if (prop.name.startsWith("aria")) {
+        aria[prop.name] = prop.value;
+      }
+    }
+    return Object.keys(aria).length === 0 ? null : aria;
+  },
+
+  setPageStyle: function (pageStyle) {
+    this.pageStyle = pageStyle;
+  },
+
+  /**
+   * Refresh the panel according to data preferences
+   *
+   * @param Event event
+   *        The event triggered by changes
+   * @param Object data
+   *        Object that contains prefrences
+   */
+  _handlePrefChange: function (event, data) {
+    if (this._accessibility && (data.pref === "devtools.defaultColorUnit" ||
+        data.pref === PREF_ORIG_SOURCES)) {
+      this.refreshPanel();
+    }
+  },
+
+  /**
+   * Update the view with a new selected element. The CssAccessiblityView panel
+   * will show the accessible information for the given element.
+   *
+   * @param {Node} element
+   *        The selected node to get accessible properties for.
+   * @returns a promise that will be resolved when selecting is done.
+   */
+  selectElement: function (element) {
+    if (!element) {
+      this.viewedElement = null;
+      this.noResults.hidden = false;
+
+      if (this._refreshProcess) {
+        this._refreshProcess.cancel();
+      }
+
+      // Hide all displayed properties
+      for (let propView of this.propertyViews) {
+        propView.refresh();
+      }
+      return promise.resolve(undefined);
+    }
+
+    // Return if current inspected element is same as current displayed element
+    if (element === this.viewedElement) {
+      return promise.resolve(undefined);
+    }
+
+    this.viewedElement = element;
+
+    // Display accessible properties for currenly viewed elements
+    return this.refreshPanel();
+  },
+
+  /**
+   * Focus the window on mousedown.
+   */
+  focusWindow: function () {
+    let win = this.styleDocument.defaultView;
+    win.focus();
+  },
+
+  /**
+   * Handle the find keypress event in the accessbility view.
+   */
+  _onKeypress: function (event) {
+    if (!event.target.closest("#sidebar-panel-accessibilityview")) {
+      return;
+    }
+    let isOSX = Services.appinfo.OS === "Darwin";
+
+    if (((isOSX && event.metaKey && !event.ctrlKey && !event.altKey) ||
+        (!isOSX && event.ctrlKey && !event.metaKey && !event.altKey)) &&
+        event.key === "f") {
+      this.searchField.focus();
+      event.preventDefault();
+    }
+  },
+
+  /**
+   * Callback for copy event. Copy selected text.
+   *
+   * @param {Event} event
+   *        copy event object.
+   */
+  _onCopy: function (event) {
+    this.copySelection();
+    event.preventDefault();
+  },
+
+  /**
+   * Set the filter style search value.
+   * @param {String} value
+   *        The search value.
+   */
+  setFilterProps: function (value = "") {
+    this.searchField.value = value;
+    this.searchField.focus();
+    this._onFilterProps();
+  },
+
+  /**
+   * Called when the user enters a search term in the filter style search box.
+   */
+  _onFilterProps: function () {
+    if (this._filterChangedTimeout) {
+      clearTimeout(this._filterChangedTimeout);
+    }
+
+    let filterTimeout = (this.searchField.value.length > 0)
+      ? FILTER_CHANGED_TIMEOUT : 0;
+    this.searchClearButton.hidden = this.searchField.value.length === 0;
+
+    this._filterChangedTimeout = setTimeout(() => {
+      if (this.searchField.value.length > 0) {
+        this.searchField.setAttribute("filled", true);
+      } else {
+        this.searchField.removeAttribute("filled");
+      }
+
+      this.refreshPanel();
+      this._filterChangeTimeout = null;
+    }, filterTimeout);
+  },
+
+  /**
+   * Handle the search box's keypress event. If the escape key is pressed,
+   * clear the search box field.
+   */
+  _onFilterKeyPress: function (event) {
+    if (event.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE &&
+        this._onClearSearch()) {
+      event.preventDefault();
+      event.stopPropagation();
+    }
+  },
+
+  /**
+   * Called when the user clicks on the clear button in the filter style search
+   * box. Returns true if the search box is cleared and false otherwise.
+   */
+  _onClearSearch: function () {
+    if (this.searchField.value) {
+      this.setFilterProps("");
+      return true;
+    }
+
+    return false;
+  },
+
+  /**
+   * Create property views of accessible properties
+  */
+  _createPropertyViews: function () {
+    let deferred = promise.defer();
+
+    this.numProperties = 0;
+    let fragment = this.styleDocument.createDocumentFragment();
+
+    this.addAccessibleProperties();
+
+    this._createViewsProcess = new UpdateProcess(
+      this.styleWindow, CssAccessibilityView.propertyNames, {
+        onItem: (propertyName) => {
+          // Per-item callback.
+          let propView = new PropertyView(this, propertyName.name, propertyName.element);
+
+          if (!propView.alreadyCached) {
+            fragment.appendChild(propView.buildMain());
+            fragment.appendChild(propView.buildSelectorContainer());
+
+            if (propView.visible) {
+              this.numProperties++;
+            }
+
+            this.propertyViews.push(propView);
+          }
+        },
+        onCancel: () => {
+          deferred.reject("_createPropertyViews cancelled");
+        },
+        onDone: () => {
+          // Completed callback.
+          this.element.appendChild(fragment);
+          this.noResults.hidden = this.numProperties > 0;
+          deferred.resolve(undefined);
+        }
+      }
+    );
+
+    this._createViewsProcess.schedule();
+    return deferred.promise;
+  },
+
+  /**
+   * Refresh the panel content.
+   */
+  refreshPanel: function () {
+    if (!this.viewedElement) {
+      return promise.resolve();
+    }
+
+    // Capture the current viewed element to return from the promise handler
+    // early if it changed
+    let viewedElement = this.viewedElement;
+
+    return promise.all([
+      this._createPropertyViews(),
+      this.accessibleProperties()
+    ]).then(([, accessibility]) => {
+      if (viewedElement !== this.viewedElement) {
+        return promise.resolve();
+      }
+
+      this._accessibility = accessibility;
+
+      if (this._refreshProcess) {
+        this._refreshProcess.cancel();
+      }
+
+      this.noResults.hidden = true;
+
+      // Reset visible property count
+      this.numProperties = 0;
+
+      let deferred = promise.defer();
+      this._refreshProcess = new UpdateProcess(
+        this.styleWindow, this.propertyViews, {
+          onItem: (propView) => {
+            propView.refresh();
+          },
+          onDone: () => {
+            this._refreshProcess = null;
+            this.noResults.hidden = this.numProperties > 0;
+
+            if (this.searchField.value.length > 0 &&
+                !this.numProperties) {
+              this.searchField.classList
+                              .add("devtools-style-searchbox-no-match");
+            } else {
+              this.searchField.classList
+                              .remove("devtools-style-searchbox-no-match");
+            }
+
+            this.inspector.emit("accessibility-view-refreshed");
+            deferred.resolve(undefined);
+          }
+        }
+      );
+      this._refreshProcess.schedule();
+      return deferred.promise;
+    }).then(null, (err) => console.error(err));
+  },
+
+  /**
+   * Copy the current selection to the clipboard
+   */
+  copySelection: function () {
+    try {
+      let win = this.styleDocument.defaultView;
+      let text = win.getSelection().toString().trim();
+
+      clipboardHelper.copyString(text);
+    } catch (e) {
+      console.error(e);
+    }
+  },
+
+  /**
+   * Destructor for CssAccessibilityView.
+   */
+  destroy: function () {
+    this.viewedElement = null;
+    this._outputParser = null;
+
+    gDevTools.off("pref-changed", this._handlePrefChange);
+
+    // Cancel property view construction
+    if (this._createViewsProcess) {
+      this._createViewsProcess.cancel();
+    }
+    if (this._refreshProcess) {
+      this._refreshProcess.cancel();
+    }
+
+    // Remove bounded listeners
+    this.styleDocument.removeEventListener("mousedown", this.focusWindow);
+    this.element.removeEventListener("copy", this._onCopy);
+    this.searchField.removeEventListener("input", this._onFilterProps);
+    this.searchField.removeEventListener("keypress", this._onFilterKeyPress);
+    this.searchClearButton.removeEventListener("click", this._onClearSearch);
+
+    // Remove identified nodes
+    this.root = null;
+    this.element = null;
+    this.panel = null;
+    this.searchField = null;
+    this.searchClearButton = null;
+
+    // Destory all property views created
+    for (let propView of this.propertyViews) {
+      propView.destroy();
+    }
+    this.propertyViews = null;
+
+    this.inspector = null;
+    this.styleDocument = null;
+    this.styleWindow = null;
+
+    this._isDestroyed = true;
+  }
+};
+
+function PropertyInfo(tree, name) {
+  this.tree = tree;
+  this.name = name;
+}
+
+PropertyInfo.prototype = {
+  get value() {
+    if (this.tree._accessibility) {
+      let value = this.tree._accessibility[this.name];
+      return value;
+    }
+    return null;
+  }
+};
+
+/**
+ * A container to give easy access to property data from the template engine.
+ *
+ * @param {CssAccessibilityView} tree
+ *        The CssAccessibilityView instance we are working with.
+ * @param {String} name
+ *        The Accssible property name for which this PropertyView
+ *        instance will render the rules.
+ */
+function PropertyView(tree, name, element) {
+  this.tree = tree;
+  this.name = name;
+  this.propelement = element;
+  this.getRTLAttr = tree.getRTLAttr;
+
+  this._propertyInfo = new PropertyInfo(tree, name);
+}
+
+PropertyView.prototype = {
+  // The parent element which contains the open attribute
+  element: null,
+
+  // Destination for property names
+  nameNode: null,
+
+  // Destination for property values
+  valueNode: null,
+
+  // Are values expanded?
+  valueExpanded: false,
+
+  // Values container node
+  valuesContainer: null,
+
+  // Values expander node
+  valueExpander: null,
+
+  // The previously selected element used for the selector view caches
+  prevViewedElement: null,
+
+  /**
+   * Get the accessible attributes value for the current property.
+   *
+   * @return {String} the accessible attribute value for current property
+   */
+  get value() {
+    return this.propertyInfo.value;
+  },
+
+  /**
+   * An easy way to access the AccessibilityPropertyInfo behind this PropertyView.
+   */
+  get propertyInfo() {
+    return this._propertyInfo;
+  },
+
+  /**
+   * Does the property have more than one value
+   */
+  get hasMultipleValues() {
+    return !this.isSingleValue;
+  },
+
+  get valueKeys() {
+    return Object.keys(this.value);
+  },
+
+  // The name and aria properties should always have a drop down
+  get isSingleValue() {
+    return this.valueKeys.length === 1 && !this.isAria;
+  },
+
+  get isAria() {
+    return this.name.includes("Aria");
+  },
+
+  get isName() {
+    return this.name.includes("Name");
+  },
+
+  /**
+   * Should this property be visible?
+   */
+  get visible() {
+    if (!this.tree.viewedElement) {
+      return false;
+    }
+
+    if (this.tree.viewedElement !== this.propelement) {
+      return false;
+    }
+
+    let searchTerm = this.tree.searchField.value.toLowerCase();
+    let isValidSearchTerm = searchTerm.trim().length > 0;
+
+    if (isValidSearchTerm) {
+      let found = false;
+      for (let key of this.valueKeys) {
+        if (key.toLowerCase().indexOf(searchTerm) !== -1 ||
+          this.value[key].toLowerCase().indexOf(searchTerm) !== -1) {
+          found = true;
+        }
+      }
+      return found;
+    }
+
+    return true;
+  },
+
+  get alreadyCached() {
+    for (let key in this.tree.propertyViews) {
+      let property = this.tree.propertyViews[key];
+      if (this.name === property.name &&
+        this.propelement === property.propelement) {
+        return true;
+      }
+    }
+    return false;
+  },
+
+  /**
+   * Returns the className that should be assigned to the propertyView.
+   *
+   * @return {String}
+   */
+  get propertyHeaderClassName() {
+    if (this.visible && this.value) {
+      return "property-view";
+    }
+    return "property-view-hidden";
+  },
+
+  /**
+   * Returns the className that should be assigned to the propertyView content
+   * container.
+   *
+   * @return {String}
+   */
+  get propertyContentClassName() {
+    if (this.visible && this.value) {
+      return "property-content";
+    }
+    return "property-content-hidden";
+  },
+
+  /**
+   * Build the markup for on accessbile property
+   *
+   * @return {Element}
+   */
+  buildMain: function () {
+    let doc = this.tree.styleDocument;
+
+    // Build the container element
+    this.onValuesContainerToggle = this.onValuesContainerToggle.bind(this);
+    this.element = doc.createElementNS(HTML_NS, "div");
+    this.element.setAttribute("class", this.propertyHeaderClassName);
+    this.element.addEventListener("dblclick", this.onValuesContainerToggle, false);
+
+    this.element.setAttribute("title", this.name);
+
+    // Make it keyboard navigable
+    this.element.setAttribute("tabindex", "0");
+    this.onKeyDown = (event) => {
+      let keyEvent = Ci.nsIDOMKeyEvent;
+      if (event.keyCode === keyEvent.DOM_VK_RETURN ||
+        event.keyCode === keyEvent.DOM_VK_SPACE) {
+        this.onValuesContainerToggle(event);
+      }
+    };
+    this.element.addEventListener("keydown", this.onKeyDown, false);
+
+    let nameContainer = doc.createElementNS(HTML_NS, "div");
+    nameContainer.className = "property-name-container";
+    this.element.appendChild(nameContainer);
+
+    // Build the twisty expand/collapse
+    this.valueExpander = doc.createElementNS(HTML_NS, "div");
+    this.valueExpander.className = "expander theme-twisty";
+    this.valueExpander.addEventListener("click", this.onValuesContainerToggle, false);
+    nameContainer.appendChild(this.valueExpander);
+
+    // Build the accessible name element
+    this.nameNode = doc.createElementNS(HTML_NS, "div");
+    this.nameNode.setAttribute("class", "property-name theme-fg-color5");
+    // Reset its tabindex attribute otherwise, if an ellipsis is applied
+    // it will be reachable via TABing
+    this.nameNode.setAttribute("tabindex", "");
+    this.nameNode.textContent = this.nameNode.title = this.name;
+    // Make it hand over the focus to the container
+    this.onFocus = () => this.element.focus();
+    this.nameNode.addEventListener("click", this.onFocus, false);
+    nameContainer.appendChild(this.nameNode);
+
+    let valueContainer = doc.createElementNS(HTML_NS, "div");
+    valueContainer.className = "property-value-container";
+    this.element.appendChild(valueContainer);
+
+    // Build the accessible value element
+    this.valueNode = doc.createElementNS(HTML_NS, "div");
+    this.valueNode.setAttribute("class", ".property-namevalue theme-fg-color1");
+    // Reset its tabindex attribute otherwise, if an ellipsis is applied
+    // it will be reachable via TABing
+    this.valueNode.setAttribute("tabindex", "");
+    this.valueNode.setAttribute("dir", "ltr");
+    // Make it hand over the focus to the container
+    this.valueNode.addEventListener("click", this.onFocus, false);
+    valueContainer.appendChild(this.valueNode);
+
+    return this.element;
+  },
+
+  buildSelectorContainer: function () {
+    let doc = this.tree.styleDocument;
+    let element = doc.createElementNS(HTML_NS, "div");
+    element.setAttribute("class", this.propertyContentClassName);
+    this.valuesContainer = doc.createElementNS(HTML_NS, "div");
+    this.valuesContainer.setAttribute("class", "matchedValue");
+    element.appendChild(this.valuesContainer);
+
+    return element;
+  },
+
+  /**
+   * Refresh the panel's Accessibility property value.
+   */
+  refresh: function () {
+    this.element.className = this.propertyHeaderClassName;
+    this.element.nextElementSibling.className = this.propertyContentClassName;
+
+    if (this.prevViewedElement !== this.tree.viewedElement) {
+      this.prevViewedElement = this.tree.viewedElement;
+    }
+
+    if (!this.element.className.includes("hidden")) {
+      this.tree.numProperties++;
+
+      this.valueNode.innerHTML = "";
+
+      if (this.tree._accessibility.Name && this.isName) {
+        let outputParser = this.tree._outputParser;
+        let frag = outputParser.parseAccessibleProperty("name:", this.tree._accessibleName);
+
+        this.valueNode.title = this.tree._accessibleName;
+        this.valueNode.appendChild(frag);
+      }
+
+      if (this.isSingleValue && !this.isName) {
+        let outputParser = this.tree._outputParser;
+        let key = this.valueKeys[0];
+        let val = this.value[key];
+
+        let frag = outputParser.parseAccessibleProperty(this.name, val);
+
+        this.valueNode.title = key + ":" + val;
+        this.valueNode.appendChild(frag);
+      } else {
+        this.refreshValuesContainer();
+      }
+    }
+  },
+
+  /**
+   * Refresh the panel's value container for selected property
+   */
+  refreshValuesContainer: function () {
+    let hasMultipleValues = this.hasMultipleValues;
+    this.valuesContainer.parentNode.hidden = !hasMultipleValues;
+
+    if (hasMultipleValues) {
+      this.valueExpander.classList.add("expandable");
+    } else {
+      this.valueExpander.classList.remove("expandable");
+    }
+
+    if (this.valueExpanded && hasMultipleValues) {
+      return promise.all(this.valueKeys)
+        .then(values => {
+          if (!this.valueExpanded) {
+            return promise.resolve(undefined);
+          }
+
+          this.containerValuesResponse = values;
+
+          return this._buildValuesContainer().then(() => {
+            this.valueExpander.setAttribute("open", "");
+            this.tree.inspector.emit("accessibility-view-property-expanded");
+          });
+        }).then(null, console.error);
+    }
+
+    this.valuesContainer.innerHTML = "";
+    this.valueExpander.removeAttribute("open");
+    this.tree.inspector.emit("accessibility-view-property-collapsed");
+    return promise.resolve(undefined);
+  },
+
+  get containerValues() {
+    return this.containerValuesResponse;
+  },
+
+  overriden: function (nameKey) {
+    if (this.name !== "Name") {
+      return "";
+    }
+
+    let title = false;
+    let ariaLabel = false;
+
+    for (let selector of this.containerValues) {
+      if (selector === "title") {title = true;}
+      if (selector === "aria-label") {ariaLabel = true;}
+    }
+
+    return title && ariaLabel && nameKey === "title" ? "overriden" : "";
+  },
+
+  _buildValuesContainer: function () {
+    let promises = [];
+    let frag = this.element.ownerDocument.createDocumentFragment();
+    let outputParser = this.tree._outputParser;
+
+    for (let selector of this.containerValues) {
+      let value = this.value[selector];
+      let labeled = !selector.includes("unlabelled");
+
+      let p = createChild(frag, "p");
+      let valueSpan = createChild(p, "span", {
+        class: "theme-fg-color1" + " " + this.overriden(selector),
+        tabindex: 0
+      });
+
+      valueSpan.title = labeled ? selector + " : " + value : value;
+      valueSpan.appendChild(outputParser.parseAccessibleProperty(selector, value, labeled));
+      promises.push(selector);
+    }
+
+    this.valuesContainer.innerHTML = "";
+    this.valuesContainer.appendChild(frag);
+    return promise.all(promises);
+  },
+
+  /**
+   * The action when a user expands a value container
+   *
+   * @param {Event} event
+   *        Used to determine the class name of the targets click
+   *        event.
+   */
+  onValuesContainerToggle: function (event) {
+    if (event.shiftKey) {
+      return;
+    }
+    this.valueExpanded = !this.valueExpanded;
+    this.refreshValuesContainer();
+    event.preventDefault();
+  },
+
+  /**
+   * Destroy this property view, removing event listeners
+   */
+  destroy: function () {
+    this.element.removeEventListener("dblclick", this.onValuesContainerToggle, false);
+    this.element.removeEventListener("keydown", this.onKeyDown, false);
+    this.element = null;
+
+    this.valueExpander.removeEventListener("click", this.onValuesContainerToggle,
+                                             false);
+    this.valueExpander = null;
+
+    this.nameNode.removeEventListener("click", this.onFocus, false);
+    this.nameNode = null;
+
+    this.valueNode.removeEventListener("click", this.onFocus, false);
+    this.valueNode = null;
+  }
+};
+
+function AccessibilityViewTool(inspector, window) {
+  this.inspector = inspector;
+  this.document = window.document;
+
+  this.view = new CssAccessibilityView(this.inspector, this.document,
+    this.inspector.pageStyle);
+
+  this.onSelected = this.onSelected.bind(this);
+  this.refresh = this.refresh.bind(this);
+  this.onPanelSelected = this.onPanelSelected.bind(this);
+  this.onMutations = this.onMutations.bind(this);
+  this.onResized = this.onResized.bind(this);
+
+  this.inspector.selection.on("detached", this.onSelected);
+  this.inspector.selection.on("new-node-front", this.onSelected);
+  this.inspector.selection.on("pseudoclass", this.refresh);
+  this.inspector.sidebar.on("accessibilityview-selected", this.onPanelSelected);
+  this.inspector.pageStyle.on("stylesheet-updated", this.refresh);
+  this.inspector.walker.on("mutations", this.onMutations);
+  this.inspector.walker.on("resize", this.onResized);
+
+  this.view.selectElement(null);
+
+  this.onSelected();
+}
+
+AccessibilityViewTool.prototype = {
+  isSidebarActive: function () {
+    if (!this.view) {
+      return false;
+    }
+    return this.inspector.sidebar.getCurrentTabID() == "accessibilityview";
+  },
+
+  onSelected: function (event) {
+    // Ignore the event if the view has been destroyed, or if it's inactive.
+    // But only if the current selection isn't null. If it's been set to null,
+    // let the update go through as this is needed to empty the view on
+    // navigation.
+    if (!this.view) {
+      return;
+    }
+
+    let isInactive = !this.isSidebarActive() &&
+                     this.inspector.selection.nodeFront;
+    if (isInactive) {
+      return;
+    }
+
+    this.view.setPageStyle(this.inspector.pageStyle);
+
+    if (!this.inspector.selection.isConnected() ||
+        !this.inspector.selection.isElementNode()) {
+      this.view.selectElement(null);
+      return;
+    }
+
+    if (!event || event == "new-node-front") {
+      let done = this.inspector.updating("accessibility-view");
+      this.view.selectElement(this.inspector.selection.node).then(() => {
+        done();
+      });
+    }
+  },
+
+  refresh: function () {
+    if (this.isSidebarActive()) {
+      this.view.refreshPanel();
+    }
+  },
+
+  onPanelSelected: function () {
+    if (this.inspector.selection.nodeFront === this.view.viewedElement) {
+      this.refresh();
+    } else {
+      this.onSelected();
+    }
+  },
+
+  /**
+   * When markup mutations occur, if an attribute of the selected node changes,
+   * we need to refresh the view as that might change the node's styles.
+   */
+  onMutations: function (mutations) {
+    for (let {type, target} of mutations) {
+      if (target === this.inspector.selection.nodeFront &&
+          type === "attributes") {
+        this.refresh();
+        break;
+      }
+    }
+  },
+
+  /**
+   * When the window gets resized, this may cause media-queries to match, and
+   * therefore, different styles may apply.
+   */
+  onResized: function () {
+    this.refresh();
+  },
+
+  destroy: function () {
+    this.inspector.walker.off("mutations", this.onMutations);
+    this.inspector.walker.off("resize", this.onResized);
+    this.inspector.sidebar.off("accessibilityview-selected", this.refresh);
+    this.inspector.selection.off("pseudoclass", this.refresh);
+    this.inspector.selection.off("new-node-front", this.onSelected);
+    this.inspector.selection.off("detached", this.onSelected);
+    this.inspector.sidebar.off("accessibilityview-selected", this.onPanelSelected);
+    if (this.inspector.pageStyle) {
+      this.inspector.pageStyle.off("stylesheet-updated", this.refresh);
+    }
+
+    this.view.destroy();
+
+    this.view = this.document = this.inspector = null;
+  }
+};
+
+exports.CssAccessibilityView = CssAccessibilityView;
+exports.AccessibilityViewTool = AccessibilityViewTool;
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/accessibility/moz.build
@@ -0,0 +1,11 @@
+# -*- Mode: python; c-basic-offset: 4; 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(
+    'accessibility.js',
+)
+
+# BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
--- a/devtools/client/inspector/inspector-panel.js
+++ b/devtools/client/inspector/inspector-panel.js
@@ -24,16 +24,17 @@ const {initCssProperties} = require("dev
 const nodeConstants = require("devtools/shared/dom-node-constants");
 const Telemetry = require("devtools/client/shared/telemetry");
 
 const Menu = require("devtools/client/framework/menu");
 const MenuItem = require("devtools/client/framework/menu-item");
 
 loader.lazyRequireGetter(this, "CSS", "CSS");
 
+loader.lazyRequireGetter(this, "AccessibilityViewTool", "devtools/client/inspector/accessibility/accessibility", true);
 loader.lazyRequireGetter(this, "CommandUtils", "devtools/client/shared/developer-toolbar", true);
 loader.lazyRequireGetter(this, "ComputedViewTool", "devtools/client/inspector/computed/computed", true);
 loader.lazyRequireGetter(this, "FontInspector", "devtools/client/inspector/fonts/fonts", true);
 loader.lazyRequireGetter(this, "HTMLBreadcrumbs", "devtools/client/inspector/breadcrumbs", true);
 loader.lazyRequireGetter(this, "InspectorSearch", "devtools/client/inspector/inspector-search", true);
 loader.lazyRequireGetter(this, "MarkupView", "devtools/client/inspector/markup/markup", true);
 loader.lazyRequireGetter(this, "RuleViewTool", "devtools/client/inspector/rules/rules", true);
 loader.lazyRequireGetter(this, "ToolSidebar", "devtools/client/inspector/toolsidebar", true);
@@ -74,16 +75,25 @@ loader.lazyGetter(this, "clipboardHelper
  *      Fired when the computed rules view updates to a new node
  * - computed-view-property-expanded
  *      Fired when a property is expanded in the computed rules view
  * - computed-view-property-collapsed
  *      Fired when a property is collapsed in the computed rules view
  * - computed-view-sourcelinks-updated
  *      Fired when the stylesheet source links have been updated (when switching
  *      to source-mapped files)
+* - accessibility-view-property-expanded
+*      Fired when a property is expanded in the accessiblity attributes view
+* - accessibility-view-property-collapsed
+*      Fired when a property is collapsed in the accessibility attributes view
+ * - accessibility-view-sourcelinks-updated
+ *      Fired when the stylesheet source links have been updated (when switching
+ *      to source-mapped files)
+ * - accessibility-view-refreshed
+ *      Fired when the accessibility view updates to a new node
  * - rule-view-refreshed
  *      Fired when the rule view updates to a new node
  * - rule-view-sourcelinks-updated
  *      Fired when the stylesheet source links have been updated (when switching
  *      to source-mapped files)
  */
 function InspectorPanel(iframeWindow, toolbox) {
   this._toolbox = toolbox;
@@ -362,17 +372,18 @@ InspectorPanel.prototype = {
 
     let shortcuts = new KeyShortcuts({
       window: this.panelDoc.defaultView,
     });
     let key = strings.GetStringFromName("inspector.searchHTML.key");
     shortcuts.on(key, (name, event) => {
       // Prevent overriding same shortcut from the computed/rule views
       if (event.target.closest("#sidebar-panel-ruleview") ||
-          event.target.closest("#sidebar-panel-computedview")) {
+          event.target.closest("#sidebar-panel-computedview") ||
+          event.target.closest("#sidebar-panel-accessibilityview")) {
         return;
       }
       event.preventDefault();
       this.searchBox.focus();
     });
   },
 
   get searchSuggestions() {
@@ -424,28 +435,34 @@ InspectorPanel.prototype = {
 
     // Append all side panels
     this.sidebar.addExistingTab(
       "ruleview",
       strings.GetStringFromName("inspector.sidebar.ruleViewTitle"),
       defaultTab == "ruleview");
 
     this.sidebar.addExistingTab(
+      "accessibilityview",
+      strings.GetStringFromName("inspector.sidebar.accessibilityViewTitle"),
+      defaultTab == "accessibilityview");
+
+    this.sidebar.addExistingTab(
       "computedview",
       strings.GetStringFromName("inspector.sidebar.computedViewTitle"),
       defaultTab == "computedview");
 
     this._setDefaultSidebar = (event, toolId) => {
       Services.prefs.setCharPref("devtools.inspector.activeSidebar", toolId);
     };
 
     this.sidebar.on("select", this._setDefaultSidebar);
 
     this.ruleview = new RuleViewTool(this, this.panelWin);
     this.computedview = new ComputedViewTool(this, this.panelWin);
+    this.accessibilityview = new AccessibilityViewTool(this, this.panelWin);
 
     if (this.target.form.animationsActor) {
       this.sidebar.addFrameTab(
         "animationinspector",
         strings.GetStringFromName("inspector.sidebar.animationInspectorTitle"),
         "chrome://devtools/content/animationinspector/animation-inspector.xhtml",
         defaultTab == "animationinspector");
     }
@@ -775,16 +792,20 @@ InspectorPanel.prototype = {
     if (this.ruleview) {
       this.ruleview.destroy();
     }
 
     if (this.computedview) {
       this.computedview.destroy();
     }
 
+    if (this.accessibilityview) {
+      this.accessibilityview.destroy();
+    }
+
     if (this.fontInspector) {
       this.fontInspector.destroy();
     }
 
     let cssPropertiesDestroyer = this._cssPropertiesLoaded.then(({front}) => {
       if (front) {
         front.destroy();
       }
--- a/devtools/client/inspector/inspector.xul
+++ b/devtools/client/inspector/inspector.xul
@@ -4,16 +4,17 @@
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 <?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
 <?xml-stylesheet href="chrome://devtools/content/shared/widgets/widgets.css" type="text/css"?>
 <?xml-stylesheet href="chrome://devtools/content/inspector/inspector.css" type="text/css"?>
 <?xml-stylesheet href="chrome://devtools/skin/widgets.css" type="text/css"?>
 <?xml-stylesheet href="chrome://devtools/skin/inspector.css" type="text/css"?>
 <?xml-stylesheet href="chrome://devtools/skin/rules.css" type="text/css"?>
 <?xml-stylesheet href="chrome://devtools/skin/computed.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/accessibility.css" type="text/css"?>
 <?xml-stylesheet href="chrome://devtools/skin/fonts.css" type="text/css"?>
 <?xml-stylesheet href="chrome://devtools/skin/layout.css" type="text/css"?>
 <?xml-stylesheet href="chrome://devtools/skin/animationinspector.css" type="text/css"?>
 <?xml-stylesheet href="resource://devtools/client/shared/components/sidebar-toggle.css" type="text/css"?>
 <?xml-stylesheet href="resource://devtools/client/shared/components/tabs/tabs.css" type="text/css"?>
 <?xml-stylesheet href="resource://devtools/client/shared/components/tabs/tabbar.css" type="text/css"?>
 <?xml-stylesheet href="resource://devtools/client/inspector/components/side-panel.css" type="text/css"?>
 <?xml-stylesheet href="resource://devtools/client/inspector/components/inspector-tab-panel.css" type="text/css"?>
@@ -91,16 +92,37 @@
         </html:div>
 
         <html:div id="ruleview-container" class="ruleview">
           <html:div id="ruleview-container-focusable" tabindex="-1">
           </html:div>
         </html:div>
       </html:div>
 
+      <html:div id="sidebar-panel-accessibilityview" class="devtools-monospace theme-sidebar inspector-tabpanel">
+        <html:div id="accessibilityview-toolbar" class="devtools-toolbar">
+          <html:div class="devtools-searchbox">
+            <html:input id="accessibilityview-searchbox"
+                        class="devtools-filterinput devtools-rule-searchbox"
+                        type="search"
+                        placeholder="&filterPropPlaceholder;"/>
+            <html:button id="accessibilityview-searchinput-clear" class="devtools-searchinput-clear"></html:button>
+          </html:div>
+        </html:div>
+
+        <html:div id="accessibilityview-container" class="accessibilityview">
+          <html:div id="accessibility-container-focusable" tabindex="-1">
+            <html:div id="accessibilityPropertyContainer"></html:div>
+            <html:div id="accessibilityNoResults" hidden="">
+              &noAccessibilityPropertiesFound;
+            </html:div>
+          </html:div>
+        </html:div>
+      </html:div>
+
       <html:div id="sidebar-panel-computedview" class="devtools-monospace theme-sidebar inspector-tabpanel">
         <html:div id="computedview-container">
           <html:div id="computedview-container-focusable" tabindex="-1">
             <html:div id="layout-wrapper" tabindex="0">
               <html:div id="layout-header">
                 <html:div id="layout-expander" class="expander theme-twisty expandable" open=""></html:div>
                 <html:span>&layoutViewTitle;</html:span>
                 <html:button class="devtools-button" id="layout-geometry-editor" title="&geometry.button.tooltip;"></html:button>
--- a/devtools/client/inspector/moz.build
+++ b/devtools/client/inspector/moz.build
@@ -1,14 +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/.
 
 DIRS += [
     'components',
+    'accessibility',
     'computed',
     'fonts',
     'layout',
     'markup',
     'rules',
     'shared'
 ]
 
--- a/devtools/client/inspector/shared/style-inspector-menu.js
+++ b/devtools/client/inspector/shared/style-inspector-menu.js
@@ -24,18 +24,18 @@ loader.lazyGetter(this, "_strings", () =
 });
 
 const PREF_ENABLE_MDN_DOCS_TOOLTIP =
   "devtools.inspector.mdnDocsTooltip.enabled";
 
 /**
  * Style inspector context menu
  *
- * @param {RuleView|ComputedView} view
- *        RuleView or ComputedView instance controlling this menu
+ * @param {RuleView|ComputedView|AccessibilityView} view
+ *        RuleView or ComputedView or AccessibilityView instance controlling this menu
  * @param {Object} options
  *        Option menu configuration
  */
 function StyleInspectorMenu(view, options) {
   this.view = view;
   this.inspector = this.view.inspector;
   this.styleDocument = this.view.styleDocument;
   this.styleWindow = this.view.styleWindow;
--- a/devtools/client/inspector/shared/style-inspector-overlays.js
+++ b/devtools/client/inspector/shared/style-inspector-overlays.js
@@ -48,18 +48,18 @@ exports.VIEW_NODE_VALUE_TYPE = VIEW_NODE
 const VIEW_NODE_IMAGE_URL_TYPE = 4;
 exports.VIEW_NODE_IMAGE_URL_TYPE = VIEW_NODE_IMAGE_URL_TYPE;
 const VIEW_NODE_LOCATION_TYPE = 5;
 exports.VIEW_NODE_LOCATION_TYPE = VIEW_NODE_LOCATION_TYPE;
 
 /**
  * Manages all highlighters in the style-inspector.
  *
- * @param {CssRuleView|CssComputedView} view
- *        Either the rule-view or computed-view panel
+ * @param {CssRuleView|CssComputedView|CssAccessibilityView} view
+ *        Either the rule-view or computed-view or accessibility-view panel
  */
 function HighlightersOverlay(view) {
   this.view = view;
 
   let {CssRuleView} = require("devtools/client/inspector/rules/rules");
   this.isRuleView = view instanceof CssRuleView;
 
   this.highlighterUtils = this.view.inspector.toolbox.highlighterUtils;
--- a/devtools/client/jar.mn
+++ b/devtools/client/jar.mn
@@ -298,16 +298,17 @@ devtools.jar:
     skin/images/vview-lock@2x.png (themes/images/vview-lock@2x.png)
     skin/images/vview-open-inspector.png (themes/images/vview-open-inspector.png)
     skin/images/vview-open-inspector@2x.png (themes/images/vview-open-inspector@2x.png)
     skin/images/sort-arrows.svg (themes/images/sort-arrows.svg)
     skin/images/cubic-bezier-swatch.png (themes/images/cubic-bezier-swatch.png)
     skin/images/cubic-bezier-swatch@2x.png (themes/images/cubic-bezier-swatch@2x.png)
     skin/fonts.css (themes/fonts.css)
     skin/computed.css (themes/computed.css)
+    skin/accessibility.css (themes/accessibility.css)
     skin/images/arrow-e.png (themes/images/arrow-e.png)
     skin/images/arrow-e@2x.png (themes/images/arrow-e@2x.png)
     skin/projecteditor/projecteditor.css (themes/projecteditor/projecteditor.css)
     skin/images/search-clear-failed.svg (themes/images/search-clear-failed.svg)
     skin/images/search-clear-light.svg (themes/images/search-clear-light.svg)
     skin/images/search-clear-dark.svg (themes/images/search-clear-dark.svg)
     skin/tooltip/arrow-horizontal-dark.png (themes/tooltip/arrow-horizontal-dark.png)
     skin/tooltip/arrow-horizontal-dark@2x.png (themes/tooltip/arrow-horizontal-dark@2x.png)
--- a/devtools/client/locales/en-US/inspector.properties
+++ b/devtools/client/locales/en-US/inspector.properties
@@ -339,16 +339,22 @@ markupView.scrollInto.key=s
 inspector.sidebar.fontInspectorTitle=Fonts
 
 # LOCALIZATION NOTE (inspector.sidebar.ruleViewTitle):
 # This is the title shown in a tab in the side panel of the Inspector panel
 # that corresponds to the tool displaying the list of CSS rules used
 # in the page.
 inspector.sidebar.ruleViewTitle=Rules
 
+# LOCALIZATION NOTE (inspector.sidebar.accessiblityViewTitle):
+# This is the title shown in a tab in the side panel of the Inspector panel
+# that corresponds to the tool displaying the list of accessible rules used
+# in the page.
+inspector.sidebar.accessibilityViewTitle=Acessibility
+
 # LOCALIZATION NOTE (inspector.sidebar.computedViewTitle):
 # This is the title shown in a tab in the side panel of the Inspector panel
 # that corresponds to the tool displaying the list of computed CSS values
 # used in the page.
 inspector.sidebar.computedViewTitle=Computed
 
 # LOCALIZATION NOTE (inspector.sidebar.layoutViewTitle):
 # This is the title shown in a tab in the side panel of the Inspector panel
--- a/devtools/client/locales/en-US/styleinspector.dtd
+++ b/devtools/client/locales/en-US/styleinspector.dtd
@@ -12,27 +12,41 @@
   -  that specifies whether the styles that are not from the user's stylesheet
   -  should be displayed or not. -->
 <!ENTITY browserStylesLabel    "Browser styles">
 
 <!-- LOCALIZATION NOTE (filterStylesPlaceholder): This is the placeholder that goes in
   -  the search box when no search term has been entered. -->
 <!ENTITY filterStylesPlaceholder      "Filter Styles">
 
+<!-- LOCALIZATION NOTE (filterPropPlaceholder): This is the placeholder that goes in
+  -  the search box when no search term has been entered. -->
+<!ENTITY filterPropPlaceholder     "Filter Properties">
+
 <!-- LOCALIZATION NOTE (addRuleButtonTooltip): This is the tooltip shown when
   -  hovering the `Add new rule` button in the rules view toolbar. This should
   -  match ruleView.contextmenu.addNewRule in styleinspector.properties -->
 <!ENTITY addRuleButtonTooltip  "Add new rule">
 
 <!-- LOCALIZATION NOTE (selectedElementLabel): This is the label for the path of
   -  the highlighted element in the web page. This path is based on the document
   -  tree. -->
 <!ENTITY selectedElementLabel  "Selected element:">
 
 <!-- LOCALIZATION NOTE (togglePseudoClassPanel): This is the tooltip
   -  shown when hovering over the `Toggle Pseudo Class Panel` button in the
   -  rule view toolbar. -->
 <!ENTITY togglePseudoClassPanel  "Toggle pseudo-classes">
 
+<!-- LOCALIZATION NOTE (noAccessibilityPropertiesFound): In the case where there are no
+  -  Accesibility properties to display e.g. due to search criteria this message is
+  -  displayed. -->
+<!ENTITY noAccessibilityPropertiesFound     "No Accessibility properties found.">
+
 <!-- LOCALIZATION NOTE (noPropertiesFound): In the case where there are no CSS
   -  properties to display e.g. due to search criteria this message is
   -  displayed. -->
 <!ENTITY noPropertiesFound     "No CSS properties found.">
+
+<!-- FIXME: notes -->
+<!ENTITY computedViewTitle     "Computed">
+<!ENTITY accessibilityViewTitle     "Accessibility">
+<!ENTITY ruleViewTitle         "Rules">
--- a/devtools/client/shared/output-parser.js
+++ b/devtools/client/shared/output-parser.js
@@ -83,16 +83,33 @@ OutputParser.prototype = {
       return this._parse(value, options);
     }
     this._appendTextNode(value);
 
     return this._toDOM();
   },
 
   /**
+   * Parse an Accessibility value given a property name.
+   *
+   * @param  {String} name
+   *         Accessibility Property Name
+   * @param  {String} value
+   *         Accessibility Property value
+   * @param  {Bool} labeled
+   *         Output is name of field and value
+   * @return {DocumentFragment}
+   *         A document fragment containing property value.
+   */
+  parseAccessibleProperty: function (name, value, labeled = false) {
+    labeled ? this._appendTextNode(name + ": " + value) : this._appendTextNode(value);
+    return this._toDOM();
+  },
+
+  /**
    * Given an initial FUNCTION token, read tokens from |tokenStream|
    * and collect all the (non-comment) text.  Return the collected
    * text.  The function token and the close paren are included in the
    * result.
    *
    * @param  {CSSToken} initialToken
    *         The FUNCTION token.
    * @param  {String} text
new file mode 100644
--- /dev/null
+++ b/devtools/client/themes/accessibility.css
@@ -0,0 +1,70 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+#sidebar-panel-accessibilityview {
+  margin: 0;
+  display : flex;
+  flex-direction: column;
+  width: 100%;
+  height: 100%;
+}
+
+#accessibilityview-container {
+  overflow: auto;
+  height: 100%;
+}
+
+#accessibility-container-focusable {
+  height: 100%;
+  outline: none;
+}
+
+#accessibilityview-toolbar {
+  display: flex;
+}
+
+#accessibilityPropertyContainer {
+  -moz-user-select: text;
+  overflow: auto;
+  flex: auto;
+}
+
+.property-view-hidden,
+.property-content-hidden {
+  display: none;
+}
+
+.property-valueless {
+  background-repeat: no-repeat;
+  background-size: 5px 8px;
+  overflow-x: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  background-position: 2px center;
+  padding-left: 10px;
+  outline: 0;
+}
+
+#accessibilityNoResults {
+   height: 100%;
+}
+
+.matchedValue > p {
+  clear: both;
+  margin: 0 2px 0 0;
+  padding: 2px;
+  overflow-x: hidden;
+  border-style: dotted;
+  border-color: rgba(128,128,128,0.4);
+  border-width: 1px 1px 0 1px;
+}
+
+.matchedValue > p:last-of-type {
+  border-bottom-width: 1px;
+}
+
+.overriden {
+  text-decoration: line-through;
+}
--- a/devtools/client/themes/inspector.css
+++ b/devtools/client/themes/inspector.css
@@ -102,13 +102,14 @@
   background-image: url("chrome://devtools/skin/images/add.svg");
   list-style-image: url("chrome://devtools/skin/images/add.svg");
   -moz-user-focus: normal;
 }
 
 /* "no results" warning message displayed in the ruleview and in the computed view */
 
 #ruleview-no-results,
-#computedview-no-results {
+#computedview-no-results,
+#accessibilityNoResults {
   color: var(--theme-body-color-inactive);
   text-align: center;
   margin: 5px;
 }
new file mode 100644
--- /dev/null
+++ b/devtools/shared/locales/en-US/accessibilityinspector.properties
@@ -0,0 +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/.
+
+# LOCALIZATION NOTE These strings are used inside the Accessibility Inspector.
+#
+# The correct localization of this file might be to keep it in
+# English, or another language commonly spoken among web developers.
+# You want to make that choice consistent across the developer tools.
+# A good criteria is the language in which you'd find the best
+# documentation on web development on the web.
+
+
+# LOCALIZATION NOTE (panelTitle): This is the panel title
+panelTitle=Accessibility Inspector
\ No newline at end of file