blob: cf767943b27eb7b6b759cab29307139ae434ffb2 [file] [edit]
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @constructor
* @extends {WebInspector.ThrottledWidget}
*/
WebInspector.AccessibilitySidebarView = function()
{
WebInspector.ThrottledWidget.call(this);
this._computedTextSubPane = null;
this._axNodeSubPane = null;
this._node = null;
this._sidebarPaneStack = null;
WebInspector.context.addFlavorChangeListener(WebInspector.DOMNode, this._pullNode, this);
this._pullNode();
}
WebInspector.AccessibilitySidebarView.prototype = {
/**
* @return {?WebInspector.DOMNode}
*/
node: function()
{
return this._node;
},
/**
* @override
* @protected
* @return {!Promise.<?>}
*/
doUpdate: function()
{
/**
* @param {?AccessibilityAgent.AXNode} accessibilityNode
* @this {WebInspector.AccessibilitySidebarView}
*/
function accessibilityNodeCallback(accessibilityNode)
{
if (this._computedTextSubPane)
this._computedTextSubPane.setAXNode(accessibilityNode);
if (this._axNodeSubPane)
this._axNodeSubPane.setAXNode(accessibilityNode);
}
var node = this.node();
return WebInspector.AccessibilityModel.fromTarget(node.target()).getAXNode(node.id)
.then(accessibilityNodeCallback.bind(this))
},
/**
* @override
*/
wasShown: function()
{
WebInspector.ThrottledWidget.prototype.wasShown.call(this);
if (!this._sidebarPaneStack) {
this._computedTextSubPane = new WebInspector.AXComputedTextSubPane();
this._computedTextSubPane.setNode(this.node());
this._computedTextSubPane.show(this.element);
this._computedTextSubPane.expand();
this._axNodeSubPane = new WebInspector.AXNodeSubPane();
this._axNodeSubPane.setNode(this.node());
this._axNodeSubPane.show(this.element);
this._axNodeSubPane.expand();
this._sidebarPaneStack = new WebInspector.SidebarPaneStack();
this._sidebarPaneStack.element.classList.add("flex-auto");
this._sidebarPaneStack.show(this.element);
this._sidebarPaneStack.addPane(this._computedTextSubPane);
this._sidebarPaneStack.addPane(this._axNodeSubPane);
}
WebInspector.targetManager.addModelListener(WebInspector.DOMModel, WebInspector.DOMModel.Events.AttrModified, this._onAttrChange, this);
WebInspector.targetManager.addModelListener(WebInspector.DOMModel, WebInspector.DOMModel.Events.AttrRemoved, this._onAttrChange, this);
WebInspector.targetManager.addModelListener(WebInspector.DOMModel, WebInspector.DOMModel.Events.CharacterDataModified, this._onNodeChange, this);
WebInspector.targetManager.addModelListener(WebInspector.DOMModel, WebInspector.DOMModel.Events.ChildNodeCountUpdated, this._onNodeChange, this);
},
/**
* @override
*/
willHide: function()
{
WebInspector.targetManager.removeModelListener(WebInspector.DOMModel, WebInspector.DOMModel.Events.AttrModified, this._onAttrChange, this);
WebInspector.targetManager.removeModelListener(WebInspector.DOMModel, WebInspector.DOMModel.Events.AttrRemoved, this._onAttrChange, this);
WebInspector.targetManager.removeModelListener(WebInspector.DOMModel, WebInspector.DOMModel.Events.CharacterDataModified, this._onNodeChange, this);
WebInspector.targetManager.removeModelListener(WebInspector.DOMModel, WebInspector.DOMModel.Events.ChildNodeCountUpdated, this._onNodeChange, this);
},
_pullNode: function()
{
this._node = WebInspector.context.flavor(WebInspector.DOMNode);
if (this._computedTextSubPane)
this._computedTextSubPane.setNode(this._node);
if (this._axNodeSubPane)
this._axNodeSubPane.setNode(this._node);
this.update();
},
/**
* @param {!WebInspector.Event} event
*/
_onAttrChange: function(event)
{
if (!this.node())
return;
var node = event.data.node;
if (this.node() !== node)
return;
this.update();
},
/**
* @param {!WebInspector.Event} event
*/
_onNodeChange: function(event)
{
if (!this.node())
return;
var node = event.data;
if (this.node() !== node)
return;
this.update();
},
__proto__: WebInspector.ThrottledWidget.prototype
};
/**
* @constructor
* @extends {WebInspector.SidebarPane}
* @param {string} name
*/
WebInspector.AccessibilitySubPane = function(name)
{
WebInspector.SidebarPane.call(this, name);
this._axNode = null;
this.registerRequiredCSS("accessibility/accessibilityNode.css");
}
WebInspector.AccessibilitySubPane.prototype = {
/**
* @param {?AccessibilityAgent.AXNode} axNode
* @protected
*/
setAXNode: function(axNode)
{
},
/**
* @return {?WebInspector.DOMNode}
*/
node: function()
{
return this._node;
},
/**
* @param {?WebInspector.DOMNode} node
*/
setNode: function(node)
{
this._node = node;
},
/**
* @param {string} textContent
* @param {string=} className
* @return {!Element}
*/
createInfo: function(textContent, className)
{
var classNameOrDefault = className || "info";
var info = createElementWithClass("div", classNameOrDefault);
info.textContent = textContent;
this.element.appendChild(info);
return info;
},
/**
* @param {string=} className
* @return {!TreeOutline}
*/
createTreeOutline: function(className)
{
var treeOutline = new TreeOutlineInShadow(className);
treeOutline.registerRequiredCSS("accessibility/accessibilityNode.css");
treeOutline.registerRequiredCSS("components/objectValue.css");
treeOutline.element.classList.add("hidden");
this.element.appendChild(treeOutline.element);
return treeOutline;
},
__proto__: WebInspector.SidebarPane.prototype
}
/**
* @constructor
* @extends {WebInspector.AccessibilitySubPane}
*/
WebInspector.AXComputedTextSubPane = function()
{
WebInspector.AccessibilitySubPane.call(this, WebInspector.UIString("Computed Text"));
this._computedTextElement = this.element.createChild("div", "ax-computed-text hidden");
this._noTextInfo = this.createInfo(WebInspector.UIString("Node has no text alternative."));
this._treeOutline = this.createTreeOutline();
};
WebInspector.AXComputedTextSubPane.prototype = {
/**
* @param {?AccessibilityAgent.AXNode} axNode
* @override
*/
setAXNode: function(axNode)
{
if (this._axNode === axNode)
return;
this._axNode = axNode;
var treeOutline = this._treeOutline;
treeOutline.removeChildren();
var target = this.node().target();
if (!axNode || axNode.ignored) {
this._computedTextElement.classList.add("hidden");
treeOutline.element.classList.add("hidden");
this._noTextInfo.classList.remove("hidden");
return;
}
this._computedTextElement.removeChildren();
// TODO(aboxhall): include contents where appropriate (requires protocol change)
this._computedTextElement.classList.toggle("hidden", !axNode.name || !axNode.name.value);
if (axNode.name && axNode.name.value)
this._computedTextElement.createChild("div").textContent = axNode.name.value;
var foundProperty = false;
/**
* @param {!AccessibilityAgent.AXProperty} property
*/
function addProperty(property)
{
foundProperty = true;
treeOutline.appendChild(new WebInspector.AXNodePropertyTreeElement(property, target));
}
if ("value" in axNode && axNode.value.type === "string")
addProperty(/** @type {!AccessibilityAgent.AXProperty} */ ({name: "value", value: axNode.value}));
var propertiesArray = /** @type {!Array.<!AccessibilityAgent.AXProperty> } */ (axNode.properties);
for (var property of propertiesArray) {
if (property.name == AccessibilityAgent.AXWidgetAttributes.Valuetext) {
addProperty(property);
break;
}
}
treeOutline.element.classList.toggle("hidden", !foundProperty)
this._noTextInfo.classList.toggle("hidden", !treeOutline.element.classList.contains("hidden") || !this._computedTextElement.classList.contains("hidden"));
},
__proto__: WebInspector.AccessibilitySubPane.prototype
};
/**
* @constructor
* @extends {WebInspector.AccessibilitySubPane}
*/
WebInspector.AXNodeSubPane = function()
{
WebInspector.AccessibilitySubPane.call(this, WebInspector.UIString("Accessibility Node"));
this._noNodeInfo = this.createInfo(WebInspector.UIString("No accessibility node"));
this._ignoredInfo = this.createInfo(WebInspector.UIString("Accessibility node not exposed"), "ax-ignored-info hidden");
this._treeOutline = this.createTreeOutline();
this._ignoredReasonsTree = this.createTreeOutline();
};
WebInspector.AXNodeSubPane.prototype = {
/**
* @param {?AccessibilityAgent.AXNode} axNode
* @override
*/
setAXNode: function(axNode)
{
if (this._axNode === axNode)
return;
this._axNode = axNode;
var treeOutline = this._treeOutline;
treeOutline.removeChildren();
var ignoredReasons = this._ignoredReasonsTree;
ignoredReasons.removeChildren();
var target = this.node().target();
if (!axNode) {
treeOutline.element.classList.add("hidden");
this._ignoredInfo.classList.add("hidden");
ignoredReasons.element.classList.add("hidden");
this._noNodeInfo.classList.remove("hidden");
this.element.classList.add("ax-ignored-node-pane");
return;
} else if (axNode.ignored) {
this._noNodeInfo.classList.add("hidden");
treeOutline.element.classList.add("hidden");
this.element.classList.add("ax-ignored-node-pane");
this._ignoredInfo.classList.remove("hidden");
ignoredReasons.element.classList.remove("hidden");
/**
* @param {!AccessibilityAgent.AXProperty} property
*/
function addIgnoredReason(property)
{
ignoredReasons.appendChild(new WebInspector.AXNodeIgnoredReasonTreeElement(property, axNode, target));
}
var ignoredReasonsArray = /** @type {!Array.<!Object>} */(axNode.ignoredReasons);
for (var reason of ignoredReasonsArray)
addIgnoredReason(reason);
if (!ignoredReasons.firstChild())
ignoredReasons.element.classList.add("hidden");
return;
}
this.element.classList.remove("ax-ignored-node-pane");
this._ignoredInfo.classList.add("hidden");
ignoredReasons.element.classList.add("hidden");
this._noNodeInfo.classList.add("hidden");
treeOutline.element.classList.remove("hidden");
/**
* @param {!AccessibilityAgent.AXProperty} property
*/
function addProperty(property)
{
treeOutline.appendChild(new WebInspector.AXNodePropertyTreeElement(property, target));
}
for (var propertyName of ["name", "description", "help", "value"]) {
if (propertyName in axNode) {
var defaultProperty = /** @type {!AccessibilityAgent.AXProperty} */ ({name: propertyName, value: axNode[propertyName]});
addProperty(defaultProperty);
}
}
var roleProperty = /** @type {!AccessibilityAgent.AXProperty} */ ({name: "role", value: axNode.role});
addProperty(roleProperty);
var propertyMap = {};
var propertiesArray = /** @type {!Array.<!AccessibilityAgent.AXProperty>} */ (axNode.properties);
for (var property of propertiesArray)
propertyMap[property.name] = property;
for (var propertySet of [AccessibilityAgent.AXWidgetAttributes, AccessibilityAgent.AXWidgetStates, AccessibilityAgent.AXGlobalStates, AccessibilityAgent.AXLiveRegionAttributes, AccessibilityAgent.AXRelationshipAttributes]) {
for (var propertyKey in propertySet) {
var property = propertySet[propertyKey];
if (property in propertyMap)
addProperty(propertyMap[property]);
}
}
},
__proto__: WebInspector.AccessibilitySubPane.prototype
};
/**
* @constructor
* @extends {TreeElement}
* @param {!AccessibilityAgent.AXProperty} property
* @param {!WebInspector.Target} target
*/
WebInspector.AXNodePropertyTreeElement = function(property, target)
{
this._property = property;
this._target = target;
// Pass an empty title, the title gets made later in onattach.
TreeElement.call(this, "");
this.toggleOnClick = true;
this.selectable = false;
}
WebInspector.AXNodePropertyTreeElement.prototype = {
/**
* @override
*/
onattach: function()
{
this._update();
},
_update: function()
{
this._nameElement = WebInspector.AXNodePropertyTreeElement.createNameElement(this._property.name);
var value = this._property.value;
if (value.type === "idref") {
this._valueElement = WebInspector.AXNodePropertyTreeElement.createRelationshipValueElement(value, this._target);
} else if (value.type === "idrefList") {
var relatedNodes = value.relatedNodeArrayValue;
var numNodes = relatedNodes.length;
var description = "(" + numNodes + (numNodes == 1 ? " node" : " nodes") + ")";
value.value = description;
for (var i = 0; i < relatedNodes.length; i++) {
var backendId = relatedNodes[i].backendNodeId;
var deferredNode = new WebInspector.DeferredDOMNode(this._target, relatedNodes[i].backendNodeId);
var child = new WebInspector.AXRelatedNodeTreeElement(deferredNode);
this.appendChild(child);
}
this._valueElement = WebInspector.AXNodePropertyTreeElement.createValueElement(value, this.listItemElement);
if (relatedNodes.length <= 3)
this.expand();
else
this.collapse();
} else {
this._valueElement = WebInspector.AXNodePropertyTreeElement.createValueElement(value, this.listItemElement);
}
var separatorElement = createElementWithClass("span", "separator");
separatorElement.textContent = ": ";
this.listItemElement.removeChildren();
this.listItemElement.appendChildren(this._nameElement, separatorElement, this._valueElement);
},
__proto__: TreeElement.prototype
}
/**
* @param {!TreeElement} treeNode
* @param {?AccessibilityAgent.AXNode} axNode
* @param {!WebInspector.Target} target
*/
WebInspector.AXNodePropertyTreeElement.populateWithNode = function(treeNode, axNode, target)
{
}
/**
* @param {?string} name
* @return {!Element}
*/
WebInspector.AXNodePropertyTreeElement.createNameElement = function(name)
{
var nameElement = createElement("span");
var AXAttributes = WebInspector.AccessibilityStrings.AXAttributes;
if (name in AXAttributes) {
nameElement.textContent = WebInspector.UIString(AXAttributes[name].name);
nameElement.title = AXAttributes[name].description;
nameElement.classList.add("ax-readable-name");
} else {
nameElement.textContent = name;
nameElement.classList.add("ax-name");
}
return nameElement;
}
/**
* @param {!AccessibilityAgent.AXValue} value
* @param {!WebInspector.Target} target
* @return {?Element}
*/
WebInspector.AXNodePropertyTreeElement.createRelationshipValueElement = function(value, target)
{
var deferredNode = new WebInspector.DeferredDOMNode(target, value.relatedNodeValue.backendNodeId);
var valueElement = createElement("span");
/**
* @param {?WebInspector.DOMNode} node
*/
function onNodeResolved(node)
{
valueElement.appendChild(WebInspector.DOMPresentationUtils.linkifyNodeReference(node));
}
deferredNode.resolve(onNodeResolved);
return valueElement;
}
/**
* @param {!AccessibilityAgent.AXValue} value
* @param {!Element} parentElement
* @return {!Element}
*/
WebInspector.AXNodePropertyTreeElement.createValueElement = function(value, parentElement)
{
var valueElement = createElementWithClass("span", "monospace");
var type = value.type;
var prefix;
var valueText;
var suffix;
if (type === "string") {
// Render \n as a nice unicode cr symbol.
prefix = "\"";
valueText = value.value.replace(/\n/g, "\u21B5");
suffix = "\"";
valueElement._originalTextContent = "\"" + value.value + "\"";
} else {
valueText = String(value.value);
}
if (type in WebInspector.AXNodePropertyTreeElement.TypeStyles)
valueElement.classList.add(WebInspector.AXNodePropertyTreeElement.TypeStyles[type]);
valueElement.setTextContentTruncatedIfNeeded(valueText || "");
if (prefix)
valueElement.insertBefore(createTextNode(prefix), valueElement.firstChild);
if (suffix)
valueElement.createTextChild(suffix);
valueElement.title = String(value.value) || "";
return valueElement;
}
/**
* @constructor
* @extends {TreeElement}
* @param {!WebInspector.DeferredDOMNode} deferredNode
*/
WebInspector.AXRelatedNodeTreeElement = function(deferredNode)
{
this._deferredNode = deferredNode;
TreeElement.call(this, "");
};
WebInspector.AXRelatedNodeTreeElement.prototype = {
onattach: function()
{
this._update();
},
_update: function()
{
var valueElement = createElement("div");
this.listItemElement.appendChild(valueElement);
/**
* @param {?WebInspector.DOMNode} node
*/
function onNodeResolved(node)
{
valueElement.appendChild(WebInspector.DOMPresentationUtils.linkifyNodeReference(node));
}
this._deferredNode.resolve(onNodeResolved);
},
__proto__: TreeElement.prototype
};
/** @type {!Object<string, string>} */
WebInspector.AXNodePropertyTreeElement.TypeStyles = {
boolean: "object-value-boolean",
booleanOrUndefined: "object-value-boolean",
tristate: "object-value-boolean",
number: "object-value-number",
integer: "object-value-number",
string: "object-value-string",
role: "ax-role",
internalRole: "ax-internal-role"
};
/**
* @constructor
* @extends {TreeElement}
* @param {!AccessibilityAgent.AXProperty} property
* @param {?AccessibilityAgent.AXNode} axNode
* @param {!WebInspector.Target} target
*/
WebInspector.AXNodeIgnoredReasonTreeElement = function(property, axNode, target)
{
this._property = property;
this._axNode = axNode;
this._target = target;
// Pass an empty title, the title gets made later in onattach.
TreeElement.call(this, "");
this.toggleOnClick = true;
this.selectable = false;
}
WebInspector.AXNodeIgnoredReasonTreeElement.prototype = {
/**
* @override
*/
onattach: function()
{
this.listItemElement.removeChildren();
this._reasonElement = WebInspector.AXNodeIgnoredReasonTreeElement.createReasonElement(this._property.name, this._axNode);
this.listItemElement.appendChild(this._reasonElement);
var value = this._property.value;
if (value.type === "idref") {
this._valueElement = WebInspector.AXNodePropertyTreeElement.createRelationshipValueElement(value, this._target);
this.listItemElement.appendChild(this._valueElement);
}
},
__proto__: TreeElement.prototype
};
/**
* @param {?string} reason
* @param {?AccessibilityAgent.AXNode} axNode
* @return {?Element}
*/
WebInspector.AXNodeIgnoredReasonTreeElement.createReasonElement = function(reason, axNode)
{
var reasonElement = null;
switch(reason) {
case "activeModalDialog":
reasonElement = WebInspector.formatLocalized("Element is hidden by active modal dialog: ", [], "");
break;
case "ancestorDisallowsChild":
reasonElement = WebInspector.formatLocalized("Element is not permitted as child of ", [], "");
break;
// http://www.w3.org/TR/wai-aria/roles#childrenArePresentational
case "ancestorIsLeafNode":
reasonElement = WebInspector.formatLocalized("Ancestor's children are all presentational: ", [], "");
break;
case "ariaHidden":
var ariaHiddenSpan = createElement("span", "source-code").textContent = "aria-hidden";
reasonElement = WebInspector.formatLocalized("Element is %s.", [ ariaHiddenSpan ], "");
break;
case "ariaHiddenRoot":
var ariaHiddenSpan = createElement("span", "source-code").textContent = "aria-hidden";
var trueSpan = createElement("span", "source-code").textContent = "true";
reasonElement = WebInspector.formatLocalized("%s is %s on ancestor: ", [ ariaHiddenSpan, trueSpan ], "");
break;
case "emptyAlt":
reasonElement = WebInspector.formatLocalized("Element has empty alt text.", [], "");
break;
case "emptyText":
reasonElement = WebInspector.formatLocalized("No text content.", [], "");
break;
case "inert":
reasonElement = WebInspector.formatLocalized("Element is inert.", [], "");
break;
case "inheritsPresentation":
reasonElement = WebInspector.formatLocalized("Element inherits presentational role from ", [], "");
break;
case "labelContainer":
reasonElement = WebInspector.formatLocalized("Part of label element: ", [], "");
break;
case "labelFor":
reasonElement = WebInspector.formatLocalized("Label for ", [], "");
break;
case "notRendered":
reasonElement = WebInspector.formatLocalized("Element is not rendered.", [], "");
break;
case "notVisible":
reasonElement = WebInspector.formatLocalized("Element is not visible.", [], "");
break;
case "presentationalRole":
var rolePresentationSpan = createElement("span", "source-code").textContent = "role=" + axNode.role.value;
reasonElement = WebInspector.formatLocalized("Element has %s.", [ rolePresentationSpan ], "");
break;
case "probablyPresentational":
reasonElement = WebInspector.formatLocalized("Element is presentational.", [], "");
break;
case "staticTextUsedAsNameFor":
reasonElement = WebInspector.formatLocalized("Static text node is used as name for ", [], "");
break;
case "uninteresting":
reasonElement = WebInspector.formatLocalized("Element not interesting for accessibility.", [], "")
break;
}
if (reasonElement)
reasonElement.classList.add("ax-reason");
return reasonElement;
}