Commit 475f5490 authored by Erik Luo's avatar Erik Luo Committed by Commit Bot

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/1317925Reviewed-by: default avatarJoel Einbinder <einbinder@chromium.org>
Commit-Queue: Erik Luo <luoe@chromium.org>
Cr-Commit-Position: refs/heads/master@{#606196}
parent 41093c0d
...@@ -19,6 +19,7 @@ Linked url: http://www.chromium.org/ ...@@ -19,6 +19,7 @@ Linked url: http://www.chromium.org/
Text: http://www.chromium.org/some?v=114:56:57 Text: http://www.chromium.org/some?v=114:56:57
Linked url: http://www.chromium.org/some?v=114 Linked url: http://www.chromium.org/some?v=114
Line: 55, Column: 56
Text: http://www.example.com/düsseldorf?neighbourhood=Lörick Text: http://www.example.com/düsseldorf?neighbourhood=Lörick
Linked url: http://www.example.com/düsseldorf?neighbourhood=Lörick Linked url: http://www.example.com/düsseldorf?neighbourhood=Lörick
...@@ -79,7 +80,8 @@ Linked url: http://www.chromium.org ...@@ -79,7 +80,8 @@ Linked url: http://www.chromium.org
Text: www.chromium.org? Text: www.chromium.org?
Linked url: http://www.chromium.org Linked url: http://www.chromium.org
The string "at triggerError (http://localhost/show/:22:11) " linkifies to url: http://localhost/show/
The lineNumber is 21 Text: at triggerError (http://localhost/show/:22:11)
The columnNumber is 10 Linked url: http://localhost/show/
Line: 21, Column: 10
...@@ -34,6 +34,7 @@ ...@@ -34,6 +34,7 @@
console.log("www.chromium.org..."); console.log("www.chromium.org...");
console.log("www.chromium.org!"); console.log("www.chromium.org!");
console.log("www.chromium.org?"); console.log("www.chromium.org?");
console.log("at triggerError (http://localhost/show/:22:11)");
`); `);
TestRunner.addResult('Dump urls in messages'); TestRunner.addResult('Dump urls in messages');
...@@ -46,22 +47,18 @@ ...@@ -46,22 +47,18 @@
var links = element.querySelectorAll('.devtools-link'); var links = element.querySelectorAll('.devtools-link');
for (var link of links) { for (var link of links) {
var info = Components.Linkifier._linkInfo(link); var info = Components.Linkifier._linkInfo(link);
if (info && info.url) if (info && info.url) {
TestRunner.addResult('Linked url: ' + info.url); TestRunner.addResult('Linked url: ' + info.url);
if (info.lineNumber !== null || info.columnNumber !== null)
TestRunner.addResult(`Line: ${info.lineNumber}, Column: ${info.columnNumber}`);
}
} }
} }
var linkifyMe = 'at triggerError (http://localhost/show/:22:11)';
var fragment = Console.ConsoleViewMessage._linkifyStringAsFragment(linkifyMe);
var anchor = fragment.querySelector('.devtools-link');
var info = Components.Linkifier._linkInfo(anchor);
TestRunner.addResult('The string "' + linkifyMe + ' " linkifies to url: ' + (info && info.url));
TestRunner.addResult('The lineNumber is ' + (info && info.lineNumber));
TestRunner.addResult('The columnNumber is ' + (info && info.columnNumber));
// Ensures urls with lots of slashes does not bog down the regex. // Ensures urls with lots of slashes does not bog down the regex.
Console.ConsoleViewMessage._linkifyStringAsFragment('/'.repeat(1000)); const dummyMessage = viewMessages[0];
Console.ConsoleViewMessage._linkifyStringAsFragment('/a/'.repeat(1000)); Console.ConsoleViewMessage.prototype._linkifyStringAsFragment.call(dummyMessage, '/'.repeat(1000));
Console.ConsoleViewMessage.prototype._linkifyStringAsFragment.call(dummyMessage, '/a/'.repeat(1000));
TestRunner.completeTest(); TestRunner.completeTest();
})(); })();
Tests that console links are keyboard navigable.
Running: testNavigatingLinks
Evaluating: console.log("Text around www.chromium.org/1a multiple links, www.chromium.org/1b");console.log("www.chromium.org/2");
Message count: 2
Setting focus in prompt:
Shift+Tab:
Viewport virtual selection: 1
activeElement: SPAN.devtools-link
active text: www.chromium.org/2
ArrowUp:
Viewport virtual selection: 1
activeElement: DIV.console-message-wrapper.console-from-api.console-info-level.console-selected
active text: console-key-links.js:18 www.chromium.org/2
ArrowUp:
Viewport virtual selection: 0
activeElement: DIV.console-message-wrapper.console-from-api.console-info-level.console-selected
active text: console-key-links.js:18 Text around www.chromium.org/1a multiple links, www.chromium.org/1b
ArrowDown:
Viewport virtual selection: 0
activeElement: SPAN.devtools-link
active text: www.chromium.org/1a
ArrowDown:
Viewport virtual selection: 0
activeElement: SPAN.devtools-link
active text: www.chromium.org/1b
ArrowDown:
Viewport virtual selection: 1
activeElement: DIV.console-message-wrapper.console-from-api.console-info-level.console-selected
active text: console-key-links.js:18 www.chromium.org/2
ArrowDown:
Viewport virtual selection: 1
activeElement: SPAN.devtools-link
active text: www.chromium.org/2
// Copyright 2018 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.
(async function() {
TestRunner.addResult(`Tests that console links are keyboard navigable.\n`);
await TestRunner.loadModule('console_test_runner');
await TestRunner.showPanel('console');
ConsoleTestRunner.fixConsoleViewportDimensions(600, 200);
await ConsoleTestRunner.waitUntilConsoleEditorLoaded();
const consoleView = Console.ConsoleView.instance();
const viewport = consoleView._viewport;
const prompt = consoleView._prompt;
TestRunner.runTestSuite([
async function testNavigatingLinks(next) {
await clearAndLog(`console.log("Text around www.chromium.org/1a multiple links, www.chromium.org/1b");console.log("www.chromium.org/2");`, 2);
await ConsoleTestRunner.waitForRemoteObjectsConsoleMessagesPromise();
TestRunner.addResult(`Setting focus in prompt:`);
prompt.focus();
shiftPress('Tab');
dumpFocus(true, 1, true);
press('ArrowUp');
dumpFocus(true, 1, true);
press('ArrowUp');
dumpFocus(true, 1, true);
press('ArrowDown');
dumpFocus(true, 1, true);
press('ArrowDown');
dumpFocus(true, 1, true);
press('ArrowDown');
dumpFocus(true, 1, true);
press('ArrowDown');
dumpFocus(true, 1, true);
next();
},
]);
// Utilities.
async function clearAndLog(expression, expectedCount = 1) {
consoleView._consoleCleared();
TestRunner.addResult(`Evaluating: ${expression}`);
await TestRunner.evaluateInPagePromise(expression);
await ConsoleTestRunner.waitForConsoleMessagesPromise(expectedCount);
await ConsoleTestRunner.waitForPendingViewportUpdates();
}
function press(key) {
TestRunner.addResult(`\n${key}:`);
eventSender.keyDown(key);
}
function shiftPress(key) {
TestRunner.addResult(`\nShift+${key}:`);
eventSender.keyDown(key, ['shiftKey']);
}
function dumpFocus(activeElement, messageIndex = 0, skipObjectCheck) {
const firstMessage = consoleView._visibleViewMessages[messageIndex];
const hasTrace = !!firstMessage.element().querySelector('.console-message-stack-trace-toggle');
const hasHiddenStackTrace = firstMessage.element().querySelector('.console-message-stack-trace-wrapper > div.hidden');
const hasCollapsedObject = firstMessage.element().querySelector('.console-view-object-properties-section:not(.expanded)');
const hasExpandedObject = firstMessage.element().querySelector('.console-view-object-properties-section.expanded');
TestRunner.addResult(`Viewport virtual selection: ${viewport._virtualSelectedIndex}`);
if (!skipObjectCheck) {
if (hasCollapsedObject) {
TestRunner.addResult(`Has object: collapsed`);
} else if (hasExpandedObject) {
TestRunner.addResult(`Has object: expanded`);
}
}
if (hasTrace) {
TestRunner.addResult(`Is trace expanded: ${!hasHiddenStackTrace ? 'YES' : 'NO'}`);
}
if (firstMessage instanceof Console.ConsoleGroupViewMessage) {
const expanded = !firstMessage.collapsed();
TestRunner.addResult(`Is group expanded: ${expanded ? 'YES' : 'NO'}`);
}
if (!activeElement)
return;
var element = document.deepActiveElement();
if (!element) {
TestRunner.addResult('null');
return;
}
var name = `activeElement: ${element.tagName}`;
if (element.id)
name += '#' + element.id;
else if (element.className)
name += '.' + element.className.split(' ').join('.');
if (element.deepTextContent())
name += '\nactive text: ' + element.deepTextContent();
TestRunner.addResult(name);
}
})();
...@@ -402,10 +402,18 @@ Components.Linkifier = class { ...@@ -402,10 +402,18 @@ Components.Linkifier = class {
revealable: null, revealable: null,
fallback: null fallback: null
}; };
if (!preventClick) if (!preventClick) {
link.addEventListener('click', Components.Linkifier._handleClick, false); link.addEventListener('click', event => {
else if (Components.Linkifier._handleClick(event))
event.consume(true);
}, false);
link.addEventListener('keydown', event => {
if (isEnterKey(event) && Components.Linkifier._handleClick(event))
event.consume(true);
}, false);
} else {
link.classList.add('devtools-link-prevent-click'); link.classList.add('devtools-link-prevent-click');
}
return link; return link;
} }
...@@ -486,15 +494,18 @@ Components.Linkifier = class { ...@@ -486,15 +494,18 @@ Components.Linkifier = class {
/** /**
* @param {!Event} event * @param {!Event} event
* @return {boolean}
*/ */
static _handleClick(event) { static _handleClick(event) {
const link = /** @type {!Element} */ (event.currentTarget); const link = /** @type {!Element} */ (event.currentTarget);
event.consume(true);
if (UI.isBeingEdited(/** @type {!Node} */ (event.target)) || link.hasSelection()) if (UI.isBeingEdited(/** @type {!Node} */ (event.target)) || link.hasSelection())
return; return false;
const actions = Components.Linkifier._linkActions(link); const actions = Components.Linkifier._linkActions(link);
if (actions.length) if (actions.length) {
actions[0].handler.call(null); actions[0].handler.call(null);
return true;
}
return false;
} }
/** /**
......
...@@ -45,8 +45,8 @@ Console.ConsoleViewMessage = class { ...@@ -45,8 +45,8 @@ Console.ConsoleViewMessage = class {
this._repeatCount = 1; this._repeatCount = 1;
this._closeGroupDecorationCount = 0; this._closeGroupDecorationCount = 0;
this._nestingLevel = nestingLevel; this._nestingLevel = nestingLevel;
/** @type {!Array<!UI.TreeOutline>} */ /** @type {!Array<{element: !Element, selectFirst: function()}>} */
this._treeOutlines = []; this._selectableChildren = [];
/** @type {?DataGrid.DataGrid} */ /** @type {?DataGrid.DataGrid} */
this._dataGrid = null; this._dataGrid = null;
...@@ -498,7 +498,7 @@ Console.ConsoleViewMessage = class { ...@@ -498,7 +498,7 @@ Console.ConsoleViewMessage = class {
for (let i = 0; i < parameters.length; ++i) { for (let i = 0; i < parameters.length; ++i) {
// Inline strings when formatting. // Inline strings when formatting.
if (shouldFormatMessage && parameters[i].type === 'string') if (shouldFormatMessage && parameters[i].type === 'string')
formattedResult.appendChild(Console.ConsoleViewMessage._linkifyStringAsFragment(parameters[i].description)); formattedResult.appendChild(this._linkifyStringAsFragment(parameters[i].description));
else else
formattedResult.appendChild(this._formatParameter(parameters[i], false, true)); formattedResult.appendChild(this._formatParameter(parameters[i], false, true));
if (i < parameters.length - 1) if (i < parameters.length - 1)
...@@ -614,7 +614,7 @@ Console.ConsoleViewMessage = class { ...@@ -614,7 +614,7 @@ Console.ConsoleViewMessage = class {
section.element.classList.add('console-view-object-properties-section'); section.element.classList.add('console-view-object-properties-section');
section.enableContextMenu(); section.enableContextMenu();
section.setShowSelectionOnKeyboardFocus(true, true); section.setShowSelectionOnKeyboardFocus(true, true);
this._treeOutlines.push(section); this._selectableChildren.push(section);
return section.element; return section.element;
} }
...@@ -685,7 +685,7 @@ Console.ConsoleViewMessage = class { ...@@ -685,7 +685,7 @@ Console.ConsoleViewMessage = class {
const renderResult = await UI.Renderer.render(/** @type {!Object} */ (node)); const renderResult = await UI.Renderer.render(/** @type {!Object} */ (node));
if (renderResult) { if (renderResult) {
if (renderResult.tree) if (renderResult.tree)
this._treeOutlines.push(renderResult.tree); this._selectableChildren.push(renderResult.tree);
result.appendChild(renderResult.node); result.appendChild(renderResult.node);
} else { } else {
result.appendChild(this._formatParameterAsObject(remoteObject, false)); result.appendChild(this._formatParameterAsObject(remoteObject, false));
...@@ -705,7 +705,7 @@ Console.ConsoleViewMessage = class { ...@@ -705,7 +705,7 @@ Console.ConsoleViewMessage = class {
*/ */
_formatParameterAsString(output) { _formatParameterAsString(output) {
const span = createElement('span'); const span = createElement('span');
span.appendChild(Console.ConsoleViewMessage._linkifyStringAsFragment(output.description || '')); span.appendChild(this._linkifyStringAsFragment(output.description || ''));
const result = createElement('span'); const result = createElement('span');
result.createChild('span', 'object-value-string-quote').textContent = '"'; result.createChild('span', 'object-value-string-quote').textContent = '"';
...@@ -721,8 +721,7 @@ Console.ConsoleViewMessage = class { ...@@ -721,8 +721,7 @@ Console.ConsoleViewMessage = class {
_formatParameterAsError(output) { _formatParameterAsError(output) {
const result = createElement('span'); const result = createElement('span');
const errorSpan = this._tryFormatAsError(output.description || ''); const errorSpan = this._tryFormatAsError(output.description || '');
result.appendChild( result.appendChild(errorSpan ? errorSpan : this._linkifyStringAsFragment(output.description || ''));
errorSpan ? errorSpan : Console.ConsoleViewMessage._linkifyStringAsFragment(output.description || ''));
return result; return result;
} }
...@@ -875,13 +874,13 @@ Console.ConsoleViewMessage = class { ...@@ -875,13 +874,13 @@ Console.ConsoleViewMessage = class {
if (typeof b === 'undefined') if (typeof b === 'undefined')
return a; return a;
if (!currentStyle) { if (!currentStyle) {
a.appendChild(Console.ConsoleViewMessage._linkifyStringAsFragment(String(b))); a.appendChild(this._linkifyStringAsFragment(String(b)));
return a; return a;
} }
const lines = String(b).split('\n'); const lines = String(b).split('\n');
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
const line = lines[i]; const line = lines[i];
const lineFragment = Console.ConsoleViewMessage._linkifyStringAsFragment(line); const lineFragment = this._linkifyStringAsFragment(line);
const wrapper = createElement('span'); const wrapper = createElement('span');
wrapper.style.setProperty('contain', 'paint'); wrapper.style.setProperty('contain', 'paint');
wrapper.style.setProperty('display', 'inline-block'); wrapper.style.setProperty('display', 'inline-block');
...@@ -1047,9 +1046,9 @@ Console.ConsoleViewMessage = class { ...@@ -1047,9 +1046,9 @@ Console.ConsoleViewMessage = class {
* @return {number} * @return {number}
*/ */
_focusedChildIndex() { _focusedChildIndex() {
if (!this._treeOutlines.length) if (!this._selectableChildren.length)
return -1; return -1;
return this._treeOutlines.findIndex(child => child.element.hasFocus()); return this._selectableChildren.findIndex(child => child.element.hasFocus());
} }
/** /**
...@@ -1076,7 +1075,7 @@ Console.ConsoleViewMessage = class { ...@@ -1076,7 +1075,7 @@ Console.ConsoleViewMessage = class {
return true; return true;
} }
} }
if (!this._treeOutlines.length) if (!this._selectableChildren.length)
return false; return false;
if (event.key === 'ArrowLeft') { if (event.key === 'ArrowLeft') {
...@@ -1085,7 +1084,7 @@ Console.ConsoleViewMessage = class { ...@@ -1085,7 +1084,7 @@ Console.ConsoleViewMessage = class {
} }
if (event.key === 'ArrowRight') { if (event.key === 'ArrowRight') {
if (isWrapperFocused) { if (isWrapperFocused) {
this._treeOutlines[0].selectFirst(); this._selectableChildren[0].selectFirst();
return true; return true;
} }
} }
...@@ -1094,16 +1093,16 @@ Console.ConsoleViewMessage = class { ...@@ -1094,16 +1093,16 @@ Console.ConsoleViewMessage = class {
this._element.focus(); this._element.focus();
return true; return true;
} else if (focusedChildIndex > 0) { } else if (focusedChildIndex > 0) {
this._treeOutlines[focusedChildIndex - 1].selectFirst(); this._selectableChildren[focusedChildIndex - 1].selectFirst();
return true; return true;
} }
} }
if (event.key === 'ArrowDown') { if (event.key === 'ArrowDown') {
if (isWrapperFocused) { if (isWrapperFocused) {
this._treeOutlines[0].selectFirst(); this._selectableChildren[0].selectFirst();
return true; return true;
} else if (focusedChildIndex < this._treeOutlines.length - 1) { } else if (focusedChildIndex < this._selectableChildren.length - 1) {
this._treeOutlines[focusedChildIndex + 1].selectFirst(); this._selectableChildren[focusedChildIndex + 1].selectFirst();
return true; return true;
} }
} }
...@@ -1111,8 +1110,8 @@ Console.ConsoleViewMessage = class { ...@@ -1111,8 +1110,8 @@ Console.ConsoleViewMessage = class {
} }
focusLastChildOrSelf() { focusLastChildOrSelf() {
if (this._treeOutlines.length) if (this._selectableChildren.length)
this._treeOutlines[this._treeOutlines.length - 1].selectFirst(); this._selectableChildren[this._selectableChildren.length - 1].selectFirst();
else if (this._element) else if (this._element)
this._element.focus(); this._element.focus();
} }
...@@ -1439,15 +1438,14 @@ Console.ConsoleViewMessage = class { ...@@ -1439,15 +1438,14 @@ Console.ConsoleViewMessage = class {
const formattedResult = createElement('span'); const formattedResult = createElement('span');
let start = 0; let start = 0;
for (let i = 0; i < links.length; ++i) { for (let i = 0; i < links.length; ++i) {
formattedResult.appendChild( formattedResult.appendChild(this._linkifyStringAsFragment(string.substring(start, links[i].positionLeft)));
Console.ConsoleViewMessage._linkifyStringAsFragment(string.substring(start, links[i].positionLeft)));
formattedResult.appendChild(this._linkifier.linkifyScriptLocation( formattedResult.appendChild(this._linkifier.linkifyScriptLocation(
debuggerModel.target(), null, links[i].url, links[i].lineNumber, links[i].columnNumber)); debuggerModel.target(), null, links[i].url, links[i].lineNumber, links[i].columnNumber));
start = links[i].positionRight; start = links[i].positionRight;
} }
if (start !== string.length) if (start !== string.length)
formattedResult.appendChild(Console.ConsoleViewMessage._linkifyStringAsFragment(string.substring(start))); formattedResult.appendChild(this._linkifyStringAsFragment(string.substring(start)));
return formattedResult; return formattedResult;
...@@ -1502,9 +1500,12 @@ Console.ConsoleViewMessage = class { ...@@ -1502,9 +1500,12 @@ Console.ConsoleViewMessage = class {
* @param {string} string * @param {string} string
* @return {!DocumentFragment} * @return {!DocumentFragment}
*/ */
static _linkifyStringAsFragment(string) { _linkifyStringAsFragment(string) {
return Console.ConsoleViewMessage.linkifyWithCustomLinkifier(string, (text, url, lineNumber, columnNumber) => { 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;
}); });
} }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment