blob: 56d37a467cef10c75378dedc80446bbe5c47529e [file] [log] [blame]
Blink Reformat4c46d092018-04-07 15:32:371// Copyright 2016 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.
4/**
5 * @unrestricted
6 */
7Accessibility.AXNodeSubPane = class extends Accessibility.AccessibilitySubPane {
8 constructor() {
9 super(ls`Computed Properties`);
10
11 this.contentElement.classList.add('ax-subpane');
12
13 this._noNodeInfo = this.createInfo(ls`No accessibility node`);
14 this._ignoredInfo = this.createInfo(ls`Accessibility node not exposed`, 'ax-ignored-info hidden');
15
16 this._treeOutline = this.createTreeOutline();
17 this._ignoredReasonsTree = this.createTreeOutline();
18
19 this.element.classList.add('accessibility-computed');
20 this.registerRequiredCSS('accessibility/accessibilityNode.css');
21 }
22
23 /**
24 * @param {?Accessibility.AccessibilityNode} axNode
25 * @override
26 */
27 setAXNode(axNode) {
28 if (this._axNode === axNode)
29 return;
30 this._axNode = axNode;
31
32 const treeOutline = this._treeOutline;
33 treeOutline.removeChildren();
34 const ignoredReasons = this._ignoredReasonsTree;
35 ignoredReasons.removeChildren();
36
37 if (!axNode) {
38 treeOutline.element.classList.add('hidden');
39 this._ignoredInfo.classList.add('hidden');
40 ignoredReasons.element.classList.add('hidden');
41
42 this._noNodeInfo.classList.remove('hidden');
43 this.element.classList.add('ax-ignored-node-pane');
44
45 return;
46 }
47
48 if (axNode.ignored()) {
49 this._noNodeInfo.classList.add('hidden');
50 treeOutline.element.classList.add('hidden');
51 this.element.classList.add('ax-ignored-node-pane');
52
53 this._ignoredInfo.classList.remove('hidden');
54 ignoredReasons.element.classList.remove('hidden');
55 /**
56 * @param {!Protocol.Accessibility.AXProperty} property
57 */
58 function addIgnoredReason(property) {
59 ignoredReasons.appendChild(new Accessibility.AXNodeIgnoredReasonTreeElement(
60 property, /** @type {!Accessibility.AccessibilityNode} */ (axNode)));
61 }
62 const ignoredReasonsArray = /** @type {!Array<!Protocol.Accessibility.AXProperty>} */ (axNode.ignoredReasons());
63 for (const reason of ignoredReasonsArray)
64 addIgnoredReason(reason);
65 if (!ignoredReasons.firstChild())
66 ignoredReasons.element.classList.add('hidden');
67 return;
68 }
69 this.element.classList.remove('ax-ignored-node-pane');
70
71 this._ignoredInfo.classList.add('hidden');
72 ignoredReasons.element.classList.add('hidden');
73 this._noNodeInfo.classList.add('hidden');
74
75 treeOutline.element.classList.remove('hidden');
76
77 /**
78 * @param {!Protocol.Accessibility.AXProperty} property
79 */
80 function addProperty(property) {
81 treeOutline.appendChild(new Accessibility.AXNodePropertyTreePropertyElement(
82 property, /** @type {!Accessibility.AccessibilityNode} */ (axNode)));
83 }
84
85 for (const property of axNode.coreProperties())
86 addProperty(property);
87
88 const roleProperty = /** @type {!Protocol.Accessibility.AXProperty} */ ({name: 'role', value: axNode.role()});
89 addProperty(roleProperty);
90 for (const property of /** @type {!Array.<!Protocol.Accessibility.AXProperty>} */ (axNode.properties()))
91 addProperty(property);
92 }
93
94 /**
95 * @override
96 * @param {?SDK.DOMNode} node
97 */
98 setNode(node) {
99 super.setNode(node);
100 this._axNode = null;
101 }
102};
103
104/**
105 * @unrestricted
106 */
107Accessibility.AXNodePropertyTreeElement = class extends UI.TreeElement {
108 /**
109 * @param {!Accessibility.AccessibilityNode} axNode
110 */
111 constructor(axNode) {
112 // Pass an empty title, the title gets made later in onattach.
113 super('');
114 this._axNode = axNode;
115 }
116
117 /**
118 * @param {?Protocol.Accessibility.AXValueType} type
119 * @param {string} value
120 * @return {!Element}
121 */
122 static createSimpleValueElement(type, value) {
123 let valueElement;
124 const AXValueType = Protocol.Accessibility.AXValueType;
125 if (!type || type === AXValueType.ValueUndefined || type === AXValueType.ComputedString)
126 valueElement = createElement('span');
127 else
128 valueElement = createElementWithClass('span', 'monospace');
129 let valueText;
130 const isStringProperty = type && Accessibility.AXNodePropertyTreeElement.StringProperties.has(type);
131 if (isStringProperty) {
132 // Render \n as a nice unicode cr symbol.
133 valueText = '"' + value.replace(/\n/g, '\u21B5') + '"';
134 valueElement._originalTextContent = value;
135 } else {
136 valueText = String(value);
137 }
138
139 if (type && type in Accessibility.AXNodePropertyTreeElement.TypeStyles)
140 valueElement.classList.add(Accessibility.AXNodePropertyTreeElement.TypeStyles[type]);
141
142 valueElement.setTextContentTruncatedIfNeeded(valueText || '');
143
144 valueElement.title = String(value) || '';
145
146 return valueElement;
147 }
148
149 /**
150 * @param {string} tooltip
151 * @return {!Element}
152 */
153 static createExclamationMark(tooltip) {
Joel Einbinder7fbe24c2019-01-24 05:19:01154 const exclamationElement = createElement('span', 'dt-icon-label');
Blink Reformat4c46d092018-04-07 15:32:37155 exclamationElement.type = 'smallicon-warning';
156 exclamationElement.title = tooltip;
157 return exclamationElement;
158 }
159
160 /**
161 * @param {string} name
162 */
163 appendNameElement(name) {
164 const nameElement = createElement('span');
165 const AXAttributes = Accessibility.AccessibilityStrings.AXAttributes;
166 if (name in AXAttributes) {
Mandy Chenba6de382019-06-07 21:38:50167 nameElement.textContent = AXAttributes[name].name;
Blink Reformat4c46d092018-04-07 15:32:37168 nameElement.title = AXAttributes[name].description;
169 nameElement.classList.add('ax-readable-name');
170 } else {
171 nameElement.textContent = name;
172 nameElement.classList.add('ax-name');
173 nameElement.classList.add('monospace');
174 }
175 this.listItemElement.appendChild(nameElement);
176 }
177
178 /**
179 * @param {!Protocol.Accessibility.AXValue} value
180 */
181 appendValueElement(value) {
182 const AXValueType = Protocol.Accessibility.AXValueType;
183 if (value.type === AXValueType.Idref || value.type === AXValueType.Node || value.type === AXValueType.IdrefList ||
184 value.type === AXValueType.NodeList) {
185 this.appendRelatedNodeListValueElement(value);
186 return;
187 } else if (value.sources) {
188 const sources = value.sources;
189 for (let i = 0; i < sources.length; i++) {
190 const source = sources[i];
191 const child = new Accessibility.AXValueSourceTreeElement(source, this._axNode);
192 this.appendChild(child);
193 }
194 this.expand();
195 }
196 const element = Accessibility.AXNodePropertyTreeElement.createSimpleValueElement(value.type, String(value.value));
197 this.listItemElement.appendChild(element);
198 }
199
200 /**
201 * @param {!Protocol.Accessibility.AXRelatedNode} relatedNode
202 * @param {number} index
203 */
204 appendRelatedNode(relatedNode, index) {
205 const deferredNode =
206 new SDK.DeferredDOMNode(this._axNode.accessibilityModel().target(), relatedNode.backendDOMNodeId);
207 const nodeTreeElement = new Accessibility.AXRelatedNodeSourceTreeElement({deferredNode: deferredNode}, relatedNode);
208 this.appendChild(nodeTreeElement);
209 }
210
211 /**
212 * @param {!Protocol.Accessibility.AXRelatedNode} relatedNode
213 */
214 appendRelatedNodeInline(relatedNode) {
215 const deferredNode =
216 new SDK.DeferredDOMNode(this._axNode.accessibilityModel().target(), relatedNode.backendDOMNodeId);
217 const linkedNode = new Accessibility.AXRelatedNodeElement({deferredNode: deferredNode}, relatedNode);
218 this.listItemElement.appendChild(linkedNode.render());
219 }
220
221 /**
222 * @param {!Protocol.Accessibility.AXValue} value
223 */
224 appendRelatedNodeListValueElement(value) {
225 if (value.relatedNodes.length === 1 && !value.value) {
226 this.appendRelatedNodeInline(value.relatedNodes[0]);
227 return;
228 }
229
230 value.relatedNodes.forEach(this.appendRelatedNode, this);
231 if (value.relatedNodes.length <= 3)
232 this.expand();
233 else
234 this.collapse();
235 }
236};
237
238
239/** @type {!Object<string, string>} */
240Accessibility.AXNodePropertyTreeElement.TypeStyles = {
241 attribute: 'ax-value-string',
242 boolean: 'object-value-boolean',
243 booleanOrUndefined: 'object-value-boolean',
244 computedString: 'ax-readable-string',
245 idref: 'ax-value-string',
246 idrefList: 'ax-value-string',
247 integer: 'object-value-number',
248 internalRole: 'ax-internal-role',
249 number: 'ax-value-number',
250 role: 'ax-role',
251 string: 'ax-value-string',
252 tristate: 'object-value-boolean',
253 valueUndefined: 'ax-value-undefined'
254};
255
256/** @type {!Set.<!Protocol.Accessibility.AXValueType>} */
257Accessibility.AXNodePropertyTreeElement.StringProperties = new Set([
258 Protocol.Accessibility.AXValueType.String, Protocol.Accessibility.AXValueType.ComputedString,
259 Protocol.Accessibility.AXValueType.IdrefList, Protocol.Accessibility.AXValueType.Idref
260]);
261
262/**
263 * @unrestricted
264 */
265Accessibility.AXNodePropertyTreePropertyElement = class extends Accessibility.AXNodePropertyTreeElement {
266 /**
267 * @param {!Protocol.Accessibility.AXProperty} property
268 * @param {!Accessibility.AccessibilityNode} axNode
269 */
270 constructor(property, axNode) {
271 super(axNode);
272
273 this._property = property;
274 this.toggleOnClick = true;
275 this.selectable = false;
276
277 this.listItemElement.classList.add('property');
278 }
279
280 /**
281 * @override
282 */
283 onattach() {
284 this._update();
285 }
286
287 _update() {
288 this.listItemElement.removeChildren();
289
290 this.appendNameElement(this._property.name);
291
Mathias Bynens7d8cd342019-09-17 13:32:10292 this.listItemElement.createChild('span', 'separator').textContent = ':\xA0';
Blink Reformat4c46d092018-04-07 15:32:37293
294 this.appendValueElement(this._property.value);
295 }
296};
297
298/**
299 * @unrestricted
300 */
301Accessibility.AXValueSourceTreeElement = class extends Accessibility.AXNodePropertyTreeElement {
302 /**
303 * @param {!Protocol.Accessibility.AXValueSource} source
304 * @param {!Accessibility.AccessibilityNode} axNode
305 */
306 constructor(source, axNode) {
307 super(axNode);
308 this._source = source;
309 this.selectable = false;
310 }
311
312 /**
313 * @override
314 */
315 onattach() {
316 this._update();
317 }
318
319 /**
320 * @param {!Protocol.Accessibility.AXRelatedNode} relatedNode
321 * @param {number} index
322 * @param {string} idref
323 */
324 appendRelatedNodeWithIdref(relatedNode, index, idref) {
325 const deferredNode =
326 new SDK.DeferredDOMNode(this._axNode.accessibilityModel().target(), relatedNode.backendDOMNodeId);
327 const nodeTreeElement =
328 new Accessibility.AXRelatedNodeSourceTreeElement({deferredNode: deferredNode, idref: idref}, relatedNode);
329 this.appendChild(nodeTreeElement);
330 }
331
332 /**
333 * @param {!Protocol.Accessibility.AXValue} value
334 */
335 appendIDRefValueElement(value) {
336 const relatedNodes = value.relatedNodes;
337
338 const idrefs = value.value.trim().split(/\s+/);
339 if (idrefs.length === 1) {
340 const idref = idrefs[0];
341 const matchingNode = relatedNodes.find(node => node.idref === idref);
342 if (matchingNode)
343 this.appendRelatedNodeWithIdref(matchingNode, 0, idref);
344 else
345 this.listItemElement.appendChild(new Accessibility.AXRelatedNodeElement({idref: idref}).render());
346
347 } else {
348 // TODO(aboxhall): exclamation mark if not idreflist type
349 for (let i = 0; i < idrefs.length; ++i) {
350 const idref = idrefs[i];
351 const matchingNode = relatedNodes.find(node => node.idref === idref);
352 if (matchingNode)
353 this.appendRelatedNodeWithIdref(matchingNode, i, idref);
354 else
355 this.appendChild(new Accessibility.AXRelatedNodeSourceTreeElement({idref: idref}));
356 }
357 }
358 }
359
360 /**
361 * @param {!Protocol.Accessibility.AXValue} value
362 * @override
363 */
364 appendRelatedNodeListValueElement(value) {
365 const relatedNodes = value.relatedNodes;
366 const numNodes = relatedNodes.length;
367
368 if (value.type === Protocol.Accessibility.AXValueType.IdrefList ||
369 value.type === Protocol.Accessibility.AXValueType.Idref)
370 this.appendIDRefValueElement(value);
371 else
372 super.appendRelatedNodeListValueElement(value);
373
374
375 if (numNodes <= 3)
376 this.expand();
377 else
378 this.collapse();
379 }
380
381 /**
382 * @param {!Protocol.Accessibility.AXValueSource} source
383 */
384 appendSourceNameElement(source) {
385 const nameElement = createElement('span');
386 const AXValueSourceType = Protocol.Accessibility.AXValueSourceType;
387 const type = source.type;
388 switch (type) {
389 case AXValueSourceType.Attribute:
390 case AXValueSourceType.Placeholder:
391 case AXValueSourceType.RelatedElement:
392 if (source.nativeSource) {
393 const AXNativeSourceTypes = Accessibility.AccessibilityStrings.AXNativeSourceTypes;
394 const nativeSource = source.nativeSource;
Mandy Chenba6de382019-06-07 21:38:50395 nameElement.textContent = AXNativeSourceTypes[nativeSource].name;
396 nameElement.title = AXNativeSourceTypes[nativeSource].description;
Blink Reformat4c46d092018-04-07 15:32:37397 nameElement.classList.add('ax-readable-name');
398 break;
399 }
400 nameElement.textContent = source.attribute;
401 nameElement.classList.add('ax-name');
402 nameElement.classList.add('monospace');
403 break;
404 default:
405 const AXSourceTypes = Accessibility.AccessibilityStrings.AXSourceTypes;
406 if (type in AXSourceTypes) {
Mandy Chenba6de382019-06-07 21:38:50407 nameElement.textContent = AXSourceTypes[type].name;
408 nameElement.title = AXSourceTypes[type].description;
Blink Reformat4c46d092018-04-07 15:32:37409 nameElement.classList.add('ax-readable-name');
410 } else {
411 console.warn(type, 'not in AXSourceTypes');
Mandy Chenba6de382019-06-07 21:38:50412 nameElement.textContent = type;
Blink Reformat4c46d092018-04-07 15:32:37413 }
414 }
415 this.listItemElement.appendChild(nameElement);
416 }
417
418 _update() {
419 this.listItemElement.removeChildren();
420
421 if (this._source.invalid) {
422 const exclamationMark = Accessibility.AXNodePropertyTreeElement.createExclamationMark(ls`Invalid source.`);
423 this.listItemElement.appendChild(exclamationMark);
424 this.listItemElement.classList.add('ax-value-source-invalid');
425 } else if (this._source.superseded) {
426 this.listItemElement.classList.add('ax-value-source-unused');
427 }
428
429 this.appendSourceNameElement(this._source);
430
Mathias Bynens7d8cd342019-09-17 13:32:10431 this.listItemElement.createChild('span', 'separator').textContent = ':\xA0';
Blink Reformat4c46d092018-04-07 15:32:37432
433 if (this._source.attributeValue) {
434 this.appendValueElement(this._source.attributeValue);
Mathias Bynens7d8cd342019-09-17 13:32:10435 this.listItemElement.createTextChild('\xA0');
Blink Reformat4c46d092018-04-07 15:32:37436 } else if (this._source.nativeSourceValue) {
437 this.appendValueElement(this._source.nativeSourceValue);
Mathias Bynens7d8cd342019-09-17 13:32:10438 this.listItemElement.createTextChild('\xA0');
Blink Reformat4c46d092018-04-07 15:32:37439 if (this._source.value)
440 this.appendValueElement(this._source.value);
441 } else if (this._source.value) {
442 this.appendValueElement(this._source.value);
443 } else {
444 const valueElement = Accessibility.AXNodePropertyTreeElement.createSimpleValueElement(
445 Protocol.Accessibility.AXValueType.ValueUndefined, ls`Not specified`);
446 this.listItemElement.appendChild(valueElement);
447 this.listItemElement.classList.add('ax-value-source-unused');
448 }
449
450 if (this._source.value && this._source.superseded)
451 this.listItemElement.classList.add('ax-value-source-superseded');
452 }
453};
454
455/**
456 * @unrestricted
457 */
458Accessibility.AXRelatedNodeSourceTreeElement = class extends UI.TreeElement {
459 /**
460 * @param {{deferredNode: (!SDK.DeferredDOMNode|undefined), idref: (string|undefined)}} node
461 * @param {!Protocol.Accessibility.AXRelatedNode=} value
462 */
463 constructor(node, value) {
464 super('');
465
466 this._value = value;
467 this._axRelatedNodeElement = new Accessibility.AXRelatedNodeElement(node, value);
468 this.selectable = false;
469 }
470
471 /**
472 * @override
473 */
474 onattach() {
475 this.listItemElement.appendChild(this._axRelatedNodeElement.render());
476 if (!this._value)
477 return;
478
479 if (this._value.text) {
480 this.listItemElement.appendChild(Accessibility.AXNodePropertyTreeElement.createSimpleValueElement(
481 Protocol.Accessibility.AXValueType.ComputedString, this._value.text));
482 }
483 }
484};
485
486/**
487 * @unrestricted
488 */
489Accessibility.AXRelatedNodeElement = class {
490 /**
491 * @param {{deferredNode: (!SDK.DeferredDOMNode|undefined), idref: (string|undefined)}} node
492 * @param {!Protocol.Accessibility.AXRelatedNode=} value
493 */
494 constructor(node, value) {
495 this._deferredNode = node.deferredNode;
496 this._idref = node.idref;
497 this._value = value;
498 }
499
500 /**
501 * @return {!Element}
502 */
503 render() {
504 const element = createElement('span');
505 let valueElement;
506
507 if (this._deferredNode) {
508 valueElement = createElement('span');
509 element.appendChild(valueElement);
Alice Boxhall6ac43432018-11-22 08:24:18510 this._deferredNode.resolvePromise().then(node => {
Jeff Fisher3f5f19c2019-08-28 19:10:02511 Common.Linkifier.linkify(node, {preventKeyboardFocus: true})
512 .then(linkfied => valueElement.appendChild(linkfied));
Alice Boxhall6ac43432018-11-22 08:24:18513 });
Blink Reformat4c46d092018-04-07 15:32:37514 } else if (this._idref) {
515 element.classList.add('invalid');
516 valueElement = Accessibility.AXNodePropertyTreeElement.createExclamationMark(ls`No node with this ID.`);
517 valueElement.createTextChild(this._idref);
518 element.appendChild(valueElement);
519 }
520
521 return element;
522 }
523};
524
525/**
526 * @unrestricted
527 */
528Accessibility.AXNodeIgnoredReasonTreeElement = class extends Accessibility.AXNodePropertyTreeElement {
529 /**
530 * @param {!Protocol.Accessibility.AXProperty} property
531 * @param {!Accessibility.AccessibilityNode} axNode
532 */
533 constructor(property, axNode) {
534 super(axNode);
535 this._property = property;
536 this._axNode = axNode;
537 this.toggleOnClick = true;
538 this.selectable = false;
539 }
540
541 /**
542 * @param {?string} reason
543 * @param {?Accessibility.AccessibilityNode} axNode
544 * @return {?Element}
545 */
546 static createReasonElement(reason, axNode) {
547 let reasonElement = null;
548 switch (reason) {
549 case 'activeModalDialog':
Mathias Bynens7d8cd342019-09-17 13:32:10550 reasonElement = UI.formatLocalized('Element is hidden by active modal dialog:\xA0', []);
Blink Reformat4c46d092018-04-07 15:32:37551 break;
552 case 'ancestorIsLeafNode':
Mathias Bynens7d8cd342019-09-17 13:32:10553 reasonElement = UI.formatLocalized('Ancestor\'s children are all presentational:\xA0', []);
Blink Reformat4c46d092018-04-07 15:32:37554 break;
555 case 'ariaHiddenElement': {
556 const ariaHiddenSpan = createElement('span', 'source-code').textContent = 'aria-hidden';
557 reasonElement = UI.formatLocalized('Element is %s.', [ariaHiddenSpan]);
558 break;
559 }
560 case 'ariaHiddenSubtree': {
561 const ariaHiddenSpan = createElement('span', 'source-code').textContent = 'aria-hidden';
562 const trueSpan = createElement('span', 'source-code').textContent = 'true';
Mathias Bynens7d8cd342019-09-17 13:32:10563 reasonElement = UI.formatLocalized('%s is %s on ancestor:\xA0', [ariaHiddenSpan, trueSpan]);
Blink Reformat4c46d092018-04-07 15:32:37564 break;
565 }
566 case 'emptyAlt':
567 reasonElement = UI.formatLocalized('Element has empty alt text.', []);
568 break;
569 case 'emptyText':
570 reasonElement = UI.formatLocalized('No text content.', []);
571 break;
572 case 'inertElement':
573 reasonElement = UI.formatLocalized('Element is inert.', []);
574 break;
575 case 'inertSubtree':
Mathias Bynens7d8cd342019-09-17 13:32:10576 reasonElement = UI.formatLocalized('Element is in an inert subtree from\xA0', []);
Blink Reformat4c46d092018-04-07 15:32:37577 break;
578 case 'inheritsPresentation':
Mathias Bynens7d8cd342019-09-17 13:32:10579 reasonElement = UI.formatLocalized('Element inherits presentational role from\xA0', []);
Blink Reformat4c46d092018-04-07 15:32:37580 break;
581 case 'labelContainer':
Mathias Bynens7d8cd342019-09-17 13:32:10582 reasonElement = UI.formatLocalized('Part of label element:\xA0', []);
Blink Reformat4c46d092018-04-07 15:32:37583 break;
584 case 'labelFor':
Mathias Bynens7d8cd342019-09-17 13:32:10585 reasonElement = UI.formatLocalized('Label for\xA0', []);
Blink Reformat4c46d092018-04-07 15:32:37586 break;
587 case 'notRendered':
588 reasonElement = UI.formatLocalized('Element is not rendered.', []);
589 break;
590 case 'notVisible':
591 reasonElement = UI.formatLocalized('Element is not visible.', []);
592 break;
593 case 'presentationalRole': {
594 const rolePresentationSpan = createElement('span', 'source-code').textContent = 'role=' + axNode.role().value;
595 reasonElement = UI.formatLocalized('Element has %s.', [rolePresentationSpan]);
596 break;
597 }
598 case 'probablyPresentational':
599 reasonElement = UI.formatLocalized('Element is presentational.', []);
600 break;
601 case 'staticTextUsedAsNameFor':
Mathias Bynens7d8cd342019-09-17 13:32:10602 reasonElement = UI.formatLocalized('Static text node is used as name for\xA0', []);
Blink Reformat4c46d092018-04-07 15:32:37603 break;
604 case 'uninteresting':
605 reasonElement = UI.formatLocalized('Element not interesting for accessibility.', []);
606 break;
607 }
608 if (reasonElement)
609 reasonElement.classList.add('ax-reason');
610 return reasonElement;
611 }
612
613 /**
614 * @override
615 */
616 onattach() {
617 this.listItemElement.removeChildren();
618
619 this._reasonElement =
620 Accessibility.AXNodeIgnoredReasonTreeElement.createReasonElement(this._property.name, this._axNode);
621 this.listItemElement.appendChild(this._reasonElement);
622
623 const value = this._property.value;
624 if (value.type === Protocol.Accessibility.AXValueType.Idref)
625 this.appendRelatedNodeListValueElement(value);
626 }
627};