DevTools: keyboard navigable links in Console

(Behind experiment)
- Navigate to linkified substrings via keyboard
- Press Enter on a selected link to navigate to it

This CL does NOT cover anchors.
Links produced outside of Console will be addressed in a followup,
e.g. BrowserConsole's Network logs, JSPresentationUtils' stack trace

Bug: 865674
Change-Id: Ib0e1c39823801a9403dcdcf8b68d884197ed9690
Reviewed-on: https://chromium-review.googlesource.com/c/1317925
Reviewed-by: Joel Einbinder <[email protected]>
Commit-Queue: Erik Luo <[email protected]>
Cr-Original-Commit-Position: refs/heads/master@{#606196}
Cr-Mirrored-From: https://chromium.googlesource.com/chromium/src
Cr-Mirrored-Commit: 475f54909aa11ee1d5cdcbf3f355499c694704ad
diff --git a/front_end/console/ConsoleViewMessage.js b/front_end/console/ConsoleViewMessage.js
index 24ea094..091e2b6 100644
--- a/front_end/console/ConsoleViewMessage.js
+++ b/front_end/console/ConsoleViewMessage.js
@@ -45,8 +45,8 @@
     this._repeatCount = 1;
     this._closeGroupDecorationCount = 0;
     this._nestingLevel = nestingLevel;
-    /** @type {!Array<!UI.TreeOutline>} */
-    this._treeOutlines = [];
+    /** @type {!Array<{element: !Element, selectFirst: function()}>} */
+    this._selectableChildren = [];
 
     /** @type {?DataGrid.DataGrid} */
     this._dataGrid = null;
@@ -498,7 +498,7 @@
     for (let i = 0; i < parameters.length; ++i) {
       // Inline strings when formatting.
       if (shouldFormatMessage && parameters[i].type === 'string')
-        formattedResult.appendChild(Console.ConsoleViewMessage._linkifyStringAsFragment(parameters[i].description));
+        formattedResult.appendChild(this._linkifyStringAsFragment(parameters[i].description));
       else
         formattedResult.appendChild(this._formatParameter(parameters[i], false, true));
       if (i < parameters.length - 1)
@@ -614,7 +614,7 @@
     section.element.classList.add('console-view-object-properties-section');
     section.enableContextMenu();
     section.setShowSelectionOnKeyboardFocus(true, true);
-    this._treeOutlines.push(section);
+    this._selectableChildren.push(section);
     return section.element;
   }
 
@@ -685,7 +685,7 @@
       const renderResult = await UI.Renderer.render(/** @type {!Object} */ (node));
       if (renderResult) {
         if (renderResult.tree)
-          this._treeOutlines.push(renderResult.tree);
+          this._selectableChildren.push(renderResult.tree);
         result.appendChild(renderResult.node);
       } else {
         result.appendChild(this._formatParameterAsObject(remoteObject, false));
@@ -705,7 +705,7 @@
    */
   _formatParameterAsString(output) {
     const span = createElement('span');
-    span.appendChild(Console.ConsoleViewMessage._linkifyStringAsFragment(output.description || ''));
+    span.appendChild(this._linkifyStringAsFragment(output.description || ''));
 
     const result = createElement('span');
     result.createChild('span', 'object-value-string-quote').textContent = '"';
@@ -721,8 +721,7 @@
   _formatParameterAsError(output) {
     const result = createElement('span');
     const errorSpan = this._tryFormatAsError(output.description || '');
-    result.appendChild(
-        errorSpan ? errorSpan : Console.ConsoleViewMessage._linkifyStringAsFragment(output.description || ''));
+    result.appendChild(errorSpan ? errorSpan : this._linkifyStringAsFragment(output.description || ''));
     return result;
   }
 
@@ -875,13 +874,13 @@
       if (typeof b === 'undefined')
         return a;
       if (!currentStyle) {
-        a.appendChild(Console.ConsoleViewMessage._linkifyStringAsFragment(String(b)));
+        a.appendChild(this._linkifyStringAsFragment(String(b)));
         return a;
       }
       const lines = String(b).split('\n');
       for (let i = 0; i < lines.length; i++) {
         const line = lines[i];
-        const lineFragment = Console.ConsoleViewMessage._linkifyStringAsFragment(line);
+        const lineFragment = this._linkifyStringAsFragment(line);
         const wrapper = createElement('span');
         wrapper.style.setProperty('contain', 'paint');
         wrapper.style.setProperty('display', 'inline-block');
@@ -1047,9 +1046,9 @@
    * @return {number}
    */
   _focusedChildIndex() {
-    if (!this._treeOutlines.length)
+    if (!this._selectableChildren.length)
       return -1;
-    return this._treeOutlines.findIndex(child => child.element.hasFocus());
+    return this._selectableChildren.findIndex(child => child.element.hasFocus());
   }
 
   /**
@@ -1076,7 +1075,7 @@
         return true;
       }
     }
-    if (!this._treeOutlines.length)
+    if (!this._selectableChildren.length)
       return false;
 
     if (event.key === 'ArrowLeft') {
@@ -1085,7 +1084,7 @@
     }
     if (event.key === 'ArrowRight') {
       if (isWrapperFocused) {
-        this._treeOutlines[0].selectFirst();
+        this._selectableChildren[0].selectFirst();
         return true;
       }
     }
@@ -1094,16 +1093,16 @@
         this._element.focus();
         return true;
       } else if (focusedChildIndex > 0) {
-        this._treeOutlines[focusedChildIndex - 1].selectFirst();
+        this._selectableChildren[focusedChildIndex - 1].selectFirst();
         return true;
       }
     }
     if (event.key === 'ArrowDown') {
       if (isWrapperFocused) {
-        this._treeOutlines[0].selectFirst();
+        this._selectableChildren[0].selectFirst();
         return true;
-      } else if (focusedChildIndex < this._treeOutlines.length - 1) {
-        this._treeOutlines[focusedChildIndex + 1].selectFirst();
+      } else if (focusedChildIndex < this._selectableChildren.length - 1) {
+        this._selectableChildren[focusedChildIndex + 1].selectFirst();
         return true;
       }
     }
@@ -1111,8 +1110,8 @@
   }
 
   focusLastChildOrSelf() {
-    if (this._treeOutlines.length)
-      this._treeOutlines[this._treeOutlines.length - 1].selectFirst();
+    if (this._selectableChildren.length)
+      this._selectableChildren[this._selectableChildren.length - 1].selectFirst();
     else if (this._element)
       this._element.focus();
   }
@@ -1439,15 +1438,14 @@
     const formattedResult = createElement('span');
     let start = 0;
     for (let i = 0; i < links.length; ++i) {
-      formattedResult.appendChild(
-          Console.ConsoleViewMessage._linkifyStringAsFragment(string.substring(start, links[i].positionLeft)));
+      formattedResult.appendChild(this._linkifyStringAsFragment(string.substring(start, links[i].positionLeft)));
       formattedResult.appendChild(this._linkifier.linkifyScriptLocation(
           debuggerModel.target(), null, links[i].url, links[i].lineNumber, links[i].columnNumber));
       start = links[i].positionRight;
     }
 
     if (start !== string.length)
-      formattedResult.appendChild(Console.ConsoleViewMessage._linkifyStringAsFragment(string.substring(start)));
+      formattedResult.appendChild(this._linkifyStringAsFragment(string.substring(start)));
 
     return formattedResult;
 
@@ -1502,9 +1500,12 @@
    * @param {string} string
    * @return {!DocumentFragment}
    */
-  static _linkifyStringAsFragment(string) {
+  _linkifyStringAsFragment(string) {
     return Console.ConsoleViewMessage.linkifyWithCustomLinkifier(string, (text, url, lineNumber, columnNumber) => {
-      return Components.Linkifier.linkifyURL(url, {text, lineNumber, columnNumber});
+      const linkElement = Components.Linkifier.linkifyURL(url, {text, lineNumber, columnNumber});
+      linkElement.tabIndex = -1;
+      this._selectableChildren.push({element: linkElement, selectFirst: () => linkElement.focus()});
+      return linkElement;
     });
   }