Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / third_party / WebKit / Source / devtools / front_end / accessibility / AccessibilitySidebarView.js
blobcf767943b27eb7b6b759cab29307139ae434ffb2
1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 /**
6  * @constructor
7  * @extends {WebInspector.ThrottledWidget}
8  */
9 WebInspector.AccessibilitySidebarView = function()
11     WebInspector.ThrottledWidget.call(this);
12     this._computedTextSubPane = null;
13     this._axNodeSubPane = null;
14     this._node = null;
15     this._sidebarPaneStack = null;
16     WebInspector.context.addFlavorChangeListener(WebInspector.DOMNode, this._pullNode, this);
17     this._pullNode();
20 WebInspector.AccessibilitySidebarView.prototype = {
21     /**
22      * @return {?WebInspector.DOMNode}
23      */
24     node: function()
25     {
26         return this._node;
27     },
29     /**
30      * @override
31      * @protected
32      * @return {!Promise.<?>}
33      */
34     doUpdate: function()
35     {
36         /**
37          * @param {?AccessibilityAgent.AXNode} accessibilityNode
38          * @this {WebInspector.AccessibilitySidebarView}
39          */
40         function accessibilityNodeCallback(accessibilityNode)
41         {
42             if (this._computedTextSubPane)
43                 this._computedTextSubPane.setAXNode(accessibilityNode);
44             if (this._axNodeSubPane)
45                 this._axNodeSubPane.setAXNode(accessibilityNode);
46         }
47         var node = this.node();
48         return WebInspector.AccessibilityModel.fromTarget(node.target()).getAXNode(node.id)
49             .then(accessibilityNodeCallback.bind(this))
50     },
52     /**
53      * @override
54      */
55     wasShown: function()
56     {
57         WebInspector.ThrottledWidget.prototype.wasShown.call(this);
59         if (!this._sidebarPaneStack) {
60             this._computedTextSubPane = new WebInspector.AXComputedTextSubPane();
61             this._computedTextSubPane.setNode(this.node());
62             this._computedTextSubPane.show(this.element);
63             this._computedTextSubPane.expand();
65             this._axNodeSubPane = new WebInspector.AXNodeSubPane();
66             this._axNodeSubPane.setNode(this.node());
67             this._axNodeSubPane.show(this.element);
68             this._axNodeSubPane.expand();
70             this._sidebarPaneStack = new WebInspector.SidebarPaneStack();
71             this._sidebarPaneStack.element.classList.add("flex-auto");
72             this._sidebarPaneStack.show(this.element);
73             this._sidebarPaneStack.addPane(this._computedTextSubPane);
74             this._sidebarPaneStack.addPane(this._axNodeSubPane);
75         }
77         WebInspector.targetManager.addModelListener(WebInspector.DOMModel, WebInspector.DOMModel.Events.AttrModified, this._onAttrChange, this);
78         WebInspector.targetManager.addModelListener(WebInspector.DOMModel, WebInspector.DOMModel.Events.AttrRemoved, this._onAttrChange, this);
79         WebInspector.targetManager.addModelListener(WebInspector.DOMModel, WebInspector.DOMModel.Events.CharacterDataModified, this._onNodeChange, this);
80         WebInspector.targetManager.addModelListener(WebInspector.DOMModel, WebInspector.DOMModel.Events.ChildNodeCountUpdated, this._onNodeChange, this);
81     },
83     /**
84      * @override
85      */
86     willHide: function()
87     {
88         WebInspector.targetManager.removeModelListener(WebInspector.DOMModel, WebInspector.DOMModel.Events.AttrModified, this._onAttrChange, this);
89         WebInspector.targetManager.removeModelListener(WebInspector.DOMModel, WebInspector.DOMModel.Events.AttrRemoved, this._onAttrChange, this);
90         WebInspector.targetManager.removeModelListener(WebInspector.DOMModel, WebInspector.DOMModel.Events.CharacterDataModified, this._onNodeChange, this);
91         WebInspector.targetManager.removeModelListener(WebInspector.DOMModel, WebInspector.DOMModel.Events.ChildNodeCountUpdated, this._onNodeChange, this);
92     },
94     _pullNode: function()
95     {
96         this._node = WebInspector.context.flavor(WebInspector.DOMNode);
97         if (this._computedTextSubPane)
98             this._computedTextSubPane.setNode(this._node);
99         if (this._axNodeSubPane)
100             this._axNodeSubPane.setNode(this._node);
101         this.update();
102     },
104     /**
105      * @param {!WebInspector.Event} event
106      */
107     _onAttrChange: function(event)
108     {
109         if (!this.node())
110             return;
111         var node = event.data.node;
112         if (this.node() !== node)
113             return;
114         this.update();
115     },
117     /**
118      * @param {!WebInspector.Event} event
119      */
120     _onNodeChange: function(event)
121     {
122         if (!this.node())
123             return;
124         var node = event.data;
125         if (this.node() !== node)
126             return;
127         this.update();
128     },
131     __proto__: WebInspector.ThrottledWidget.prototype
135  * @constructor
136  * @extends {WebInspector.SidebarPane}
137  * @param {string} name
138  */
139 WebInspector.AccessibilitySubPane = function(name)
141     WebInspector.SidebarPane.call(this, name);
143     this._axNode = null;
144     this.registerRequiredCSS("accessibility/accessibilityNode.css");
147 WebInspector.AccessibilitySubPane.prototype = {
148     /**
149      * @param {?AccessibilityAgent.AXNode} axNode
150      * @protected
151      */
152     setAXNode: function(axNode)
153     {
154     },
156     /**
157      * @return {?WebInspector.DOMNode}
158      */
159     node: function()
160     {
161         return this._node;
162     },
164     /**
165      * @param {?WebInspector.DOMNode} node
166      */
167     setNode: function(node)
168     {
169         this._node = node;
170     },
172     /**
173      * @param {string} textContent
174      * @param {string=} className
175      * @return {!Element}
176      */
177     createInfo: function(textContent, className)
178     {
179         var classNameOrDefault = className || "info";
180         var info = createElementWithClass("div", classNameOrDefault);
181         info.textContent = textContent;
182         this.element.appendChild(info);
183         return info;
184     },
186     /**
187      * @param {string=} className
188      * @return {!TreeOutline}
189      */
190     createTreeOutline: function(className)
191     {
192         var treeOutline = new TreeOutlineInShadow(className);
193         treeOutline.registerRequiredCSS("accessibility/accessibilityNode.css");
194         treeOutline.registerRequiredCSS("components/objectValue.css");
195         treeOutline.element.classList.add("hidden");
196         this.element.appendChild(treeOutline.element);
197         return treeOutline;
198     },
200     __proto__: WebInspector.SidebarPane.prototype
204  * @constructor
205  * @extends {WebInspector.AccessibilitySubPane}
206  */
207 WebInspector.AXComputedTextSubPane = function()
209     WebInspector.AccessibilitySubPane.call(this, WebInspector.UIString("Computed Text"));
211     this._computedTextElement = this.element.createChild("div", "ax-computed-text hidden");
213     this._noTextInfo = this.createInfo(WebInspector.UIString("Node has no text alternative."));
214     this._treeOutline = this.createTreeOutline();
218 WebInspector.AXComputedTextSubPane.prototype = {
219     /**
220      * @param {?AccessibilityAgent.AXNode} axNode
221      * @override
222      */
223     setAXNode: function(axNode)
224     {
225         if (this._axNode === axNode)
226             return;
227         this._axNode = axNode;
229         var treeOutline = this._treeOutline;
230         treeOutline.removeChildren();
231         var target = this.node().target();
233         if (!axNode || axNode.ignored) {
234             this._computedTextElement.classList.add("hidden");
235             treeOutline.element.classList.add("hidden");
237             this._noTextInfo.classList.remove("hidden");
238             return;
239         }
240         this._computedTextElement.removeChildren();
242         // TODO(aboxhall): include contents where appropriate (requires protocol change)
243         this._computedTextElement.classList.toggle("hidden", !axNode.name || !axNode.name.value);
244         if (axNode.name && axNode.name.value)
245             this._computedTextElement.createChild("div").textContent = axNode.name.value;
247         var foundProperty = false;
248         /**
249          * @param {!AccessibilityAgent.AXProperty} property
250          */
251         function addProperty(property)
252         {
253             foundProperty = true;
254             treeOutline.appendChild(new WebInspector.AXNodePropertyTreeElement(property, target));
255         }
257         if ("value" in axNode && axNode.value.type === "string")
258             addProperty(/** @type {!AccessibilityAgent.AXProperty} */ ({name: "value", value: axNode.value}));
260         var propertiesArray = /** @type {!Array.<!AccessibilityAgent.AXProperty> } */ (axNode.properties);
261         for (var property of propertiesArray) {
262             if (property.name == AccessibilityAgent.AXWidgetAttributes.Valuetext) {
263                 addProperty(property);
264                 break;
265             }
266         }
268         treeOutline.element.classList.toggle("hidden", !foundProperty)
269         this._noTextInfo.classList.toggle("hidden", !treeOutline.element.classList.contains("hidden") || !this._computedTextElement.classList.contains("hidden"));
270     },
272     __proto__: WebInspector.AccessibilitySubPane.prototype
276  * @constructor
277  * @extends {WebInspector.AccessibilitySubPane}
278  */
279 WebInspector.AXNodeSubPane = function()
281     WebInspector.AccessibilitySubPane.call(this, WebInspector.UIString("Accessibility Node"));
283     this._noNodeInfo = this.createInfo(WebInspector.UIString("No accessibility node"));
284     this._ignoredInfo = this.createInfo(WebInspector.UIString("Accessibility node not exposed"), "ax-ignored-info hidden");
286     this._treeOutline = this.createTreeOutline();
287     this._ignoredReasonsTree = this.createTreeOutline();
291 WebInspector.AXNodeSubPane.prototype = {
292     /**
293      * @param {?AccessibilityAgent.AXNode} axNode
294      * @override
295      */
296     setAXNode: function(axNode)
297     {
298         if (this._axNode === axNode)
299             return;
300         this._axNode = axNode;
302         var treeOutline = this._treeOutline;
303         treeOutline.removeChildren();
304         var ignoredReasons = this._ignoredReasonsTree;
305         ignoredReasons.removeChildren();
306         var target = this.node().target();
308         if (!axNode) {
309             treeOutline.element.classList.add("hidden");
310             this._ignoredInfo.classList.add("hidden");
311             ignoredReasons.element.classList.add("hidden");
313             this._noNodeInfo.classList.remove("hidden");
314             this.element.classList.add("ax-ignored-node-pane");
316             return;
317         } else if (axNode.ignored) {
318             this._noNodeInfo.classList.add("hidden");
319             treeOutline.element.classList.add("hidden");
320             this.element.classList.add("ax-ignored-node-pane");
322             this._ignoredInfo.classList.remove("hidden");
323             ignoredReasons.element.classList.remove("hidden");
324             /**
325              * @param {!AccessibilityAgent.AXProperty} property
326              */
327             function addIgnoredReason(property)
328             {
329                 ignoredReasons.appendChild(new WebInspector.AXNodeIgnoredReasonTreeElement(property, axNode, target));
330             }
331             var ignoredReasonsArray = /** @type {!Array.<!Object>} */(axNode.ignoredReasons);
332             for (var reason of ignoredReasonsArray)
333                 addIgnoredReason(reason);
334             if (!ignoredReasons.firstChild())
335                 ignoredReasons.element.classList.add("hidden");
336             return;
337         }
338         this.element.classList.remove("ax-ignored-node-pane");
340         this._ignoredInfo.classList.add("hidden");
341         ignoredReasons.element.classList.add("hidden");
342         this._noNodeInfo.classList.add("hidden");
344         treeOutline.element.classList.remove("hidden");
346         /**
347          * @param {!AccessibilityAgent.AXProperty} property
348          */
349         function addProperty(property)
350         {
351             treeOutline.appendChild(new WebInspector.AXNodePropertyTreeElement(property, target));
352         }
354         for (var propertyName of ["name", "description", "help", "value"]) {
355             if (propertyName in axNode) {
356                 var defaultProperty = /** @type {!AccessibilityAgent.AXProperty} */ ({name: propertyName, value: axNode[propertyName]});
357                 addProperty(defaultProperty);
358             }
359         }
361         var roleProperty = /** @type {!AccessibilityAgent.AXProperty} */ ({name: "role", value: axNode.role});
362         addProperty(roleProperty);
364         var propertyMap = {};
365         var propertiesArray = /** @type {!Array.<!AccessibilityAgent.AXProperty>} */ (axNode.properties);
366         for (var property of propertiesArray)
367             propertyMap[property.name] = property;
369         for (var propertySet of [AccessibilityAgent.AXWidgetAttributes, AccessibilityAgent.AXWidgetStates, AccessibilityAgent.AXGlobalStates, AccessibilityAgent.AXLiveRegionAttributes, AccessibilityAgent.AXRelationshipAttributes]) {
370             for (var propertyKey in propertySet) {
371                 var property = propertySet[propertyKey];
372                 if (property in propertyMap)
373                     addProperty(propertyMap[property]);
374             }
375         }
376     },
378     __proto__: WebInspector.AccessibilitySubPane.prototype
382  * @constructor
383  * @extends {TreeElement}
384  * @param {!AccessibilityAgent.AXProperty} property
385  * @param {!WebInspector.Target} target
386  */
387 WebInspector.AXNodePropertyTreeElement = function(property, target)
389     this._property = property;
390     this._target = target;
392     // Pass an empty title, the title gets made later in onattach.
393     TreeElement.call(this, "");
394     this.toggleOnClick = true;
395     this.selectable = false;
398 WebInspector.AXNodePropertyTreeElement.prototype = {
399     /**
400      * @override
401      */
402     onattach: function()
403     {
404         this._update();
405     },
408     _update: function()
409     {
410         this._nameElement = WebInspector.AXNodePropertyTreeElement.createNameElement(this._property.name);
412         var value = this._property.value;
413         if (value.type === "idref") {
414             this._valueElement = WebInspector.AXNodePropertyTreeElement.createRelationshipValueElement(value, this._target);
415         } else if (value.type === "idrefList") {
416             var relatedNodes = value.relatedNodeArrayValue;
417             var numNodes = relatedNodes.length;
418             var description = "(" + numNodes + (numNodes == 1 ? " node" : " nodes") + ")";
419             value.value = description;
420             for (var i = 0; i < relatedNodes.length; i++) {
421                 var backendId = relatedNodes[i].backendNodeId;
422                 var deferredNode = new WebInspector.DeferredDOMNode(this._target, relatedNodes[i].backendNodeId);
423                 var child = new WebInspector.AXRelatedNodeTreeElement(deferredNode);
424                 this.appendChild(child);
425             }
426             this._valueElement = WebInspector.AXNodePropertyTreeElement.createValueElement(value, this.listItemElement);
427             if (relatedNodes.length <= 3)
428                 this.expand();
429             else
430                 this.collapse();
431         } else {
432             this._valueElement = WebInspector.AXNodePropertyTreeElement.createValueElement(value, this.listItemElement);
433         }
435         var separatorElement = createElementWithClass("span", "separator");
436         separatorElement.textContent = ": ";
438         this.listItemElement.removeChildren();
439         this.listItemElement.appendChildren(this._nameElement, separatorElement, this._valueElement);
440     },
442     __proto__: TreeElement.prototype
446  * @param {!TreeElement} treeNode
447  * @param {?AccessibilityAgent.AXNode} axNode
448  * @param {!WebInspector.Target} target
449  */
450 WebInspector.AXNodePropertyTreeElement.populateWithNode = function(treeNode, axNode, target)
455  * @param {?string} name
456  * @return {!Element}
457  */
458 WebInspector.AXNodePropertyTreeElement.createNameElement = function(name)
460     var nameElement = createElement("span");
461     var AXAttributes = WebInspector.AccessibilityStrings.AXAttributes;
462     if (name in AXAttributes) {
463         nameElement.textContent = WebInspector.UIString(AXAttributes[name].name);
464         nameElement.title = AXAttributes[name].description;
465         nameElement.classList.add("ax-readable-name");
466     } else {
467         nameElement.textContent = name;
468         nameElement.classList.add("ax-name");
469     }
470     return nameElement;
474  * @param {!AccessibilityAgent.AXValue} value
475  * @param {!WebInspector.Target} target
476  * @return {?Element}
477  */
478 WebInspector.AXNodePropertyTreeElement.createRelationshipValueElement = function(value, target)
480     var deferredNode = new WebInspector.DeferredDOMNode(target, value.relatedNodeValue.backendNodeId);
481     var valueElement = createElement("span");
483     /**
484      * @param {?WebInspector.DOMNode} node
485      */
486     function onNodeResolved(node)
487     {
488         valueElement.appendChild(WebInspector.DOMPresentationUtils.linkifyNodeReference(node));
489     }
490     deferredNode.resolve(onNodeResolved);
492     return valueElement;
496  * @param {!AccessibilityAgent.AXValue} value
497  * @param {!Element} parentElement
498  * @return {!Element}
499  */
500 WebInspector.AXNodePropertyTreeElement.createValueElement = function(value, parentElement)
502     var valueElement = createElementWithClass("span", "monospace");
503     var type = value.type;
504     var prefix;
505     var valueText;
506     var suffix;
507     if (type === "string") {
508         // Render \n as a nice unicode cr symbol.
509         prefix = "\"";
510         valueText = value.value.replace(/\n/g, "\u21B5");
511         suffix = "\"";
512         valueElement._originalTextContent = "\"" + value.value + "\"";
513     } else {
514         valueText = String(value.value);
515     }
517     if (type in WebInspector.AXNodePropertyTreeElement.TypeStyles)
518         valueElement.classList.add(WebInspector.AXNodePropertyTreeElement.TypeStyles[type]);
520     valueElement.setTextContentTruncatedIfNeeded(valueText || "");
521     if (prefix)
522         valueElement.insertBefore(createTextNode(prefix), valueElement.firstChild);
523     if (suffix)
524         valueElement.createTextChild(suffix);
526     valueElement.title = String(value.value) || "";
528     return valueElement;
532  * @constructor
533  * @extends {TreeElement}
534  * @param {!WebInspector.DeferredDOMNode} deferredNode
535  */
536 WebInspector.AXRelatedNodeTreeElement = function(deferredNode)
538     this._deferredNode = deferredNode;
540     TreeElement.call(this, "");
543 WebInspector.AXRelatedNodeTreeElement.prototype = {
544     onattach: function()
545     {
546         this._update();
547     },
549     _update: function()
550     {
551         var valueElement = createElement("div");
552         this.listItemElement.appendChild(valueElement);
554         /**
555          * @param {?WebInspector.DOMNode} node
556          */
557         function onNodeResolved(node)
558         {
559             valueElement.appendChild(WebInspector.DOMPresentationUtils.linkifyNodeReference(node));
560         }
561         this._deferredNode.resolve(onNodeResolved);
562     },
564     __proto__: TreeElement.prototype
567 /** @type {!Object<string, string>} */
568 WebInspector.AXNodePropertyTreeElement.TypeStyles = {
569     boolean: "object-value-boolean",
570     booleanOrUndefined: "object-value-boolean",
571     tristate: "object-value-boolean",
572     number: "object-value-number",
573     integer: "object-value-number",
574     string: "object-value-string",
575     role: "ax-role",
576     internalRole: "ax-internal-role"
580  * @constructor
581  * @extends {TreeElement}
582  * @param {!AccessibilityAgent.AXProperty} property
583  * @param {?AccessibilityAgent.AXNode} axNode
584  * @param {!WebInspector.Target} target
585  */
586 WebInspector.AXNodeIgnoredReasonTreeElement = function(property, axNode, target)
588     this._property = property;
589     this._axNode = axNode;
590     this._target = target;
592     // Pass an empty title, the title gets made later in onattach.
593     TreeElement.call(this, "");
594     this.toggleOnClick = true;
595     this.selectable = false;
598 WebInspector.AXNodeIgnoredReasonTreeElement.prototype = {
599     /**
600      * @override
601      */
602     onattach: function()
603     {
604         this.listItemElement.removeChildren();
606         this._reasonElement = WebInspector.AXNodeIgnoredReasonTreeElement.createReasonElement(this._property.name, this._axNode);
607         this.listItemElement.appendChild(this._reasonElement);
609         var value = this._property.value;
610         if (value.type === "idref") {
611             this._valueElement = WebInspector.AXNodePropertyTreeElement.createRelationshipValueElement(value, this._target);
612             this.listItemElement.appendChild(this._valueElement);
613         }
614     },
616     __proto__: TreeElement.prototype
620  * @param {?string} reason
621  * @param {?AccessibilityAgent.AXNode} axNode
622  * @return {?Element}
623  */
624 WebInspector.AXNodeIgnoredReasonTreeElement.createReasonElement = function(reason, axNode)
626     var reasonElement = null;
627     switch(reason) {
628     case "activeModalDialog":
629         reasonElement = WebInspector.formatLocalized("Element is hidden by active modal dialog: ", [], "");
630         break;
631     case "ancestorDisallowsChild":
632         reasonElement = WebInspector.formatLocalized("Element is not permitted as child of ", [], "");
633         break;
634     // http://www.w3.org/TR/wai-aria/roles#childrenArePresentational
635     case "ancestorIsLeafNode":
636         reasonElement = WebInspector.formatLocalized("Ancestor's children are all presentational: ", [], "");
637         break;
638     case "ariaHidden":
639         var ariaHiddenSpan = createElement("span", "source-code").textContent = "aria-hidden";
640         reasonElement = WebInspector.formatLocalized("Element is %s.", [ ariaHiddenSpan ], "");
641         break;
642     case "ariaHiddenRoot":
643         var ariaHiddenSpan = createElement("span", "source-code").textContent = "aria-hidden";
644         var trueSpan = createElement("span", "source-code").textContent = "true";
645         reasonElement = WebInspector.formatLocalized("%s is %s on ancestor: ", [ ariaHiddenSpan, trueSpan ], "");
646         break;
647     case "emptyAlt":
648         reasonElement = WebInspector.formatLocalized("Element has empty alt text.", [], "");
649         break;
650     case "emptyText":
651         reasonElement = WebInspector.formatLocalized("No text content.", [], "");
652         break;
653     case "inert":
654         reasonElement = WebInspector.formatLocalized("Element is inert.", [], "");
655         break;
656     case "inheritsPresentation":
657         reasonElement = WebInspector.formatLocalized("Element inherits presentational role from ", [], "");
658         break;
659     case "labelContainer":
660         reasonElement = WebInspector.formatLocalized("Part of label element: ", [], "");
661         break;
662     case "labelFor":
663         reasonElement = WebInspector.formatLocalized("Label for ", [], "");
664         break;
665     case "notRendered":
666         reasonElement = WebInspector.formatLocalized("Element is not rendered.", [], "");
667         break;
668     case "notVisible":
669         reasonElement = WebInspector.formatLocalized("Element is not visible.", [], "");
670         break;
671     case "presentationalRole":
672         var rolePresentationSpan = createElement("span", "source-code").textContent = "role=" + axNode.role.value;
673         reasonElement = WebInspector.formatLocalized("Element has %s.", [ rolePresentationSpan ], "");
674         break;
675     case "probablyPresentational":
676         reasonElement = WebInspector.formatLocalized("Element is presentational.", [], "");
677         break;
678     case "staticTextUsedAsNameFor":
679         reasonElement = WebInspector.formatLocalized("Static text node is used as name for ", [], "");
680         break;
681     case "uninteresting":
682         reasonElement = WebInspector.formatLocalized("Element not interesting for accessibility.", [], "")
683         break;
684     }
685     if (reasonElement)
686         reasonElement.classList.add("ax-reason");
687     return reasonElement;