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