Commit 8fce8ab1 authored by Erik Luo's avatar Erik Luo Committed by Commit Bot

DevTools: allow Console message keyboard navigation

This makes Consoles messages focusable and navigable using:
ArrowUp/Down/Left/Right, Home, End

Screenshot: https://imgur.com/a/6JWY0iU

Bug: 865674
Change-Id: I4166bf4d8d57856c6d1f7d51ef29e2dea0c088eb
Reviewed-on: https://chromium-review.googlesource.com/1144453
Commit-Queue: Erik Luo <luoe@chromium.org>
Reviewed-by: default avatarJoel Einbinder <einbinder@chromium.org>
Cr-Commit-Position: refs/heads/master@{#588237}
parent fbe6ad8d
Tests that console messages are navigable with the keyboard.
Message count: 100
Running: testBetweenViewportAndExternal
Setting focus in prompt:
TEXTAREA:Code editor
Shift+Tab:
DIV:console-key-navigation.js:20 Message #99
Shift+Tab:
BUTTON:Console settings
Tab:
DIV:console-key-navigation.js:20 Message #99
Tab:
TEXTAREA:Code editor
Running: testBetweenViewportAndExternalWithSelectedItemNotInDOM
Setting focus in prompt:
TEXTAREA:Code editor
Shift+Tab:
DIV:console-key-navigation.js:20 Message #99
Scrolling to top of viewport
DIV#console-messages.monospace
Shift+Tab:
BUTTON:Console settings
Tab:
DIV#console-messages.monospace
Setting focus in prompt:
TEXTAREA:Code editor
Shift+Tab:
DIV#console-messages.monospace
Scrolling to top of viewport
DIV#console-messages.monospace
Tab:
TEXTAREA:Code editor
Running: testMoveAcrossLogsWithinViewport
Force selecting index 99
DIV#console-messages.monospace
Home:
DIV:console-key-navigation.js:20 Message #0
ArrowDown:
DIV:console-key-navigation.js:20 Message #1
End:
DIV:console-key-navigation.js:20 Message #99
ArrowUp:
DIV:console-key-navigation.js:20 Message #98
Running: testViewportDoesNotChangeFocusOnScroll
Force selecting index 98
DIV:console-key-navigation.js:20 Message #98
Scrolling to top of viewport
DIV#console-messages.monospace
Scrolling to bottom of viewport
DIV:console-key-navigation.js:20 Message #98
Running: testViewportDoesNotStealFocusOnScroll
Force selecting index 99
DIV:console-key-navigation.js:20 Message #99
Setting focus in prompt:
TEXTAREA:Code editor
Scrolling to top of viewport
TEXTAREA:Code editor
Scrolling to bottom of viewport
TEXTAREA:Code editor
Running: testNewLogsShouldNotMoveFocus
Setting focus in prompt:
TEXTAREA:Code editor
Message count: 101
TEXTAREA:Code editor
Running: testClearingConsoleFocusesPrompt
Console cleared:
TEXTAREA:Code editor
// 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 messages are navigable with the keyboard.\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;
// Log some messages.
const logCount = 100;
await TestRunner.evaluateInPagePromise(`
for (var i = 0; i < ${logCount}; ++i)
console.log("Message #" + i);
`);
await ConsoleTestRunner.waitForConsoleMessagesPromise(logCount);
await ConsoleTestRunner.waitForPendingViewportUpdates();
TestRunner.runTestSuite([
function testBetweenViewportAndExternal(next) {
TestRunner.addResult(`Setting focus in prompt:`);
prompt.focus();
dumpFocus();
shiftPress('Tab');
dumpFocus();
shiftPress('Tab');
dumpFocus();
press('Tab');
dumpFocus();
press('Tab');
dumpFocus();
next();
},
function testBetweenViewportAndExternalWithSelectedItemNotInDOM(next) {
TestRunner.addResult(`Setting focus in prompt:`);
prompt.focus();
dumpFocus();
shiftPress('Tab');
dumpFocus();
scrollViewportToTop();
dumpFocus();
shiftPress('Tab');
dumpFocus();
press('Tab');
dumpFocus();
TestRunner.addResult(`\nSetting focus in prompt:`);
prompt.focus();
dumpFocus();
shiftPress('Tab');
dumpFocus();
scrollViewportToTop();
dumpFocus();
press('Tab');
dumpFocus();
next();
},
function testMoveAcrossLogsWithinViewport(next) {
forceSelect(logCount - 1);
dumpFocus();
press('Home');
dumpFocus();
press('ArrowDown');
dumpFocus();
press('End');
dumpFocus();
press('ArrowUp');
dumpFocus();
next();
},
function testViewportDoesNotChangeFocusOnScroll(next) {
forceSelect(logCount - 2);
dumpFocus();
scrollViewportToTop();
dumpFocus();
scrollViewportToBottom();
dumpFocus();
next();
},
function testViewportDoesNotStealFocusOnScroll(next) {
forceSelect(logCount - 1);
dumpFocus();
TestRunner.addResult(`Setting focus in prompt:`);
prompt.focus();
dumpFocus();
scrollViewportToTop();
dumpFocus();
scrollViewportToBottom();
dumpFocus();
next();
},
async function testNewLogsShouldNotMoveFocus(next) {
TestRunner.addResult(`Setting focus in prompt:`);
prompt.focus();
dumpFocus();
await TestRunner.evaluateInPagePromise(`console.log("New Message")`);
await ConsoleTestRunner.waitForConsoleMessagesPromise(logCount + 1);
await ConsoleTestRunner.waitForPendingViewportUpdates();
dumpFocus();
next();
},
function testClearingConsoleFocusesPrompt(next) {
TestRunner.addResult(`\nConsole cleared:`);
consoleView._consoleCleared();
dumpFocus();
next();
}
]);
// Utilities.
function scrollViewportToTop() {
TestRunner.addResult(`\nScrolling to top of viewport`);
viewport.setStickToBottom(false);
viewport.element.scrollTop = 0;
viewport.refresh();
}
function scrollViewportToBottom() {
TestRunner.addResult(`\nScrolling to bottom of viewport`);
viewport.element.scrollTop = viewport.element.scrollHeight + 1;
viewport.refresh();
}
function forceSelect(index) {
TestRunner.addResult(`\nForce selecting index ${index}`);
viewport._virtualSelectedIndex = index;
viewport.element.focus();
viewport._updateFocusedItem();
}
function press(key) {
TestRunner.addResult(`\n${key}:`);
eventSender.keyDown(key);
}
function shiftPress(key) {
TestRunner.addResult(`\nShift+${key}:`);
eventSender.keyDown(key, ['shiftKey']);
}
function dumpFocus() {
var element = document.deepActiveElement();
if (!element) {
TestRunner.addResult('null');
return;
}
var name = element.tagName;
if (element.id)
name += '#' + element.id;
if (element.getAttribute('aria-label'))
name += ':' + element.getAttribute('aria-label');
else if (element.title)
name += ':' + element.title;
else if (element.textContent && element.textContent.length < 50) {
name += ':' + element.textContent.replace('\u200B', '');
} else if (element.className)
name += '.' + element.className.split(' ').join('.');
TestRunner.addResult(name);
}
})();
......@@ -627,6 +627,7 @@ Console.ConsoleView = class extends UI.VBox {
}
_consoleCleared() {
const hadFocus = this._viewport.element.hasFocus();
this._cancelBuildHiddenCache();
this._currentMatchRangeIndex = -1;
this._consoleMessages = [];
......@@ -639,6 +640,8 @@ Console.ConsoleView = class extends UI.VBox {
this._linkifier.reset();
this._badgePool.reset();
this._filter.clear();
if (hadFocus)
this._prompt.focus();
}
_handleContextMenuEvent(event) {
......
......@@ -1069,6 +1069,7 @@ Console.ConsoleViewMessage = class {
return this._element;
this._element = createElement('div');
this._element.tabIndex = -1;
this.updateMessageElement();
return this._element;
}
......
......@@ -58,6 +58,14 @@ Console.ConsoleViewport = class {
this.element.addEventListener('scroll', this._onScroll.bind(this), false);
this.element.addEventListener('copy', this._onCopy.bind(this), false);
this.element.addEventListener('dragstart', this._onDragStart.bind(this), false);
this._keyboardNavigationEnabled = Runtime.experiments.isEnabled('consoleKeyboardNavigation');
if (this._keyboardNavigationEnabled) {
this.element.addEventListener('focusin', this._onFocusIn.bind(this), false);
this.element.addEventListener('focusout', this._onFocusOut.bind(this), false);
this.element.addEventListener('keydown', this._onKeyDown.bind(this), false);
}
this._virtualSelectedIndex = -1;
this.element.tabIndex = -1;
this._firstActiveIndex = -1;
this._lastActiveIndex = -1;
......@@ -112,6 +120,35 @@ Console.ConsoleViewport = class {
event.clipboardData.setData('text/plain', text);
}
/**
* @param {!Event} event
*/
_onFocusIn(event) {
// Make default selection when moving from external (e.g. prompt) to the container.
if (this._virtualSelectedIndex === -1 && this._isOutsideViewport(/** @type {?Element} */ (event.relatedTarget)) &&
event.target === this.element)
this._virtualSelectedIndex = this._itemCount - 1;
this._updateFocusedItem();
}
/**
* @param {!Event} event
*/
_onFocusOut(event) {
// Remove selection when focus moves to external location (e.g. prompt).
if (this._isOutsideViewport(/** @type {?Element} */ (event.relatedTarget)))
this._virtualSelectedIndex = -1;
this._updateFocusedItem();
}
/**
* @param {?Element} element
* @return {boolean}
*/
_isOutsideViewport(element) {
return !!element && (element !== this.element && !element.isDescendant(this._contentElement));
}
/**
* @param {!Event} event
*/
......@@ -125,6 +162,63 @@ Console.ConsoleViewport = class {
return true;
}
/**
* @param {!Event} event
*/
_onKeyDown(event) {
if (UI.isEditing() || !this._itemCount || this.element.hasSelection())
return;
switch (event.key) {
case 'ArrowUp':
this._virtualSelectedIndex--;
if (this._virtualSelectedIndex < 0)
this._virtualSelectedIndex = this._itemCount - 1;
break;
case 'ArrowDown':
this._virtualSelectedIndex++;
if (this._virtualSelectedIndex >= this._itemCount)
this._virtualSelectedIndex = 0;
break;
case 'Home':
this._virtualSelectedIndex = 0;
break;
case 'End':
this._virtualSelectedIndex = this._itemCount - 1;
break;
default:
return;
}
event.consume(true);
this.scrollItemIntoView(this._virtualSelectedIndex);
this._updateFocusedItem();
}
_updateFocusedItem() {
const selectedElement = this.renderedElementAt(this._virtualSelectedIndex);
const changed = this._lastSelectedElement !== selectedElement;
const containerHasFocus = this.element === this.element.ownerDocument.deepActiveElement();
if (this._lastSelectedElement && changed)
this._lastSelectedElement.classList.remove('console-selected');
if (selectedElement && (changed || containerHasFocus)) {
selectedElement.classList.add('console-selected');
focusWithoutScroll(selectedElement);
}
if (this._itemCount && !this._contentElement.hasFocus())
this.element.tabIndex = 0;
else
this.element.tabIndex = -1;
this._lastSelectedElement = selectedElement;
/**
* @suppress {checkTypes}
* @param {!Element} element
*/
function focusWithoutScroll(element) {
// TODO(luoe): Closure has an outdated typedef for Element.prototype.focus.
element.focus({preventScroll: true});
}
}
/**
* @return {!Element}
*/
......@@ -135,6 +229,8 @@ Console.ConsoleViewport = class {
invalidate() {
delete this._cachedProviderElements;
this._itemCount = this._provider.itemCount();
if (this._virtualSelectedIndex > this._itemCount - 1)
this._virtualSelectedIndex = this._itemCount - 1;
this._rebuildCumulativeHeights();
this.refresh();
}
......@@ -343,6 +439,8 @@ Console.ConsoleViewport = class {
this._bottomGapElement.style.height = '0px';
this._firstActiveIndex = -1;
this._lastActiveIndex = -1;
if (this._keyboardNavigationEnabled)
this._updateFocusedItem();
return;
}
......@@ -404,8 +502,12 @@ Console.ConsoleViewport = class {
for (let i = 0; i < willBeHidden.length; ++i)
willBeHidden[i].willHide();
prepare();
for (let i = 0; i < willBeHidden.length; ++i)
let hadFocus = false;
for (let i = 0; i < willBeHidden.length; ++i) {
if (this._keyboardNavigationEnabled)
hadFocus = hadFocus || willBeHidden[i].element().hasFocus();
willBeHidden[i].element().remove();
}
const wasShown = [];
let anchor = this._contentElement.firstChild;
......@@ -423,6 +525,12 @@ Console.ConsoleViewport = class {
for (let i = 0; i < wasShown.length; ++i)
wasShown[i].wasShown();
this._renderedItems = Array.from(itemsToRender);
if (this._keyboardNavigationEnabled) {
if (hadFocus)
this.element.focus();
this._updateFocusedItem();
}
}
/**
......@@ -530,9 +638,7 @@ Console.ConsoleViewport = class {
* @return {?Element}
*/
renderedElementAt(index) {
if (index < this._firstActiveIndex)
return null;
if (index > this._lastActiveIndex)
if (index === -1 || index < this._firstActiveIndex || index > this._lastActiveIndex)
return null;
return this._renderedItems[index - this._firstActiveIndex].element();
}
......
......@@ -33,12 +33,14 @@
--message-border-color: rgb(240, 240, 240);
--warning-border-color: hsl(50, 100%, 88%);
--error-border-color: hsl(0, 100%, 92%);
--error-text-color: red;
}
.-theme-with-dark-background .console-view {
--message-border-color: rgb(58, 58, 58);
--warning-border-color: rgb(102, 85, 0);
--error-border-color: rgb(92, 0, 0);
--error-text-color: hsl(0, 100%, 75%);
}
.console-toolbar-container {
......@@ -197,16 +199,16 @@
}
.console-message-wrapper.console-adjacent-user-command-result:not(.console-error-level):not(.console-warning-level) {
border-top: none;
border-top-width: 0px;
}
.console-message-wrapper.console-error-level,
.console-message-wrapper.console-error-level + .console-message-wrapper:not(.console-warning-level) {
.console-message-wrapper.console-error-level:not(.console-selected) + .console-message-wrapper:not(.console-warning-level):not(.console-selected) {
border-top-color: var(--error-border-color);
}
.console-message-wrapper.console-warning-level,
.console-message-wrapper.console-warning-level + .console-message-wrapper:not(.console-error-level) {
.console-message-wrapper.console-warning-level:not(.console-selected) + .console-message-wrapper:not(.console-error-level):not(.console-selected) {
border-top-color: var(--warning-border-color);
}
......@@ -222,6 +224,47 @@
border-bottom-color: var(--warning-border-color);
}
.console-message-wrapper.console-adjacent-user-command-result:not(.console-error-level):not(.console-warning-level).console-selected:focus {
border-top-width: 1px;
}
.console-message-wrapper.console-adjacent-user-command-result:not(.console-error-level):not(.console-warning-level).console-selected:focus .console-message {
padding-top: 2px;
min-height: 16px;
}
.console-message-wrapper.console-adjacent-user-command-result:not(.console-error-level):not(.console-warning-level).console-selected:focus .command-result-icon {
top: 3px;
}
.console-message-wrapper.console-selected:focus,
.console-message-wrapper.console-selected:focus:last-of-type {
border-top-color: hsl(214, 67%, 88%);
border-bottom-color: hsl(214, 67%, 88%);
background-color: hsl(214, 48%, 95%);
}
.console-message-wrapper.console-error-level.console-selected:focus,
.console-message-wrapper.console-error-level.console-selected:focus:last-of-type {
--error-text-color: rgb(200, 0, 0);
}
.-theme-with-dark-background .console-message-wrapper.console-error-level.console-selected:focus,
.-theme-with-dark-background .console-message-wrapper.console-error-level.console-selected:focus:last-of-type {
--error-text-color: hsl(0, 100%, 75%);
}
.-theme-with-dark-background .console-message-wrapper.console-selected:focus,
.-theme-with-dark-background .console-message-wrapper.console-selected:focus:last-of-type {
border-top-color: hsl(214, 47%, 48%);
border-bottom-color: hsl(214, 47%, 48%);
background-color: hsl(214, 19%, 20%);
}
.console-message-wrapper.console-selected:focus + .console-message-wrapper {
border-top-color: transparent;
}
.console-message-wrapper .nesting-level-marker {
width: 14px;
flex: 0 0 auto;
......@@ -268,18 +311,13 @@
.console-error-level .console-message-text,
.console-error-level .console-view-object-properties-section {
color: red !important;
color: var(--error-text-color) !important;
}
.console-system-type.console-info-level {
color: blue;
}
.-theme-with-dark-background .console-error-level .console-message-text,
.-theme-with-dark-background .console-error-level .console-view-object-properties-section {
color: hsl(0, 100%, 75%) !important;
}
.-theme-with-dark-background .console-verbose-level:not(.console-warning-level) .console-message-text,
.-theme-with-dark-background .console-system-type.console-info-level {
color: hsl(220, 100%, 65%) !important;
......
......@@ -27,11 +27,11 @@
}
.styles-section[data-keyboard-focus="true"]:focus {
background-color: hsl(214, 67%, 95%);
background-color: hsl(214, 48%, 95%);
}
.styles-section.read-only[data-keyboard-focus="true"]:focus {
background-color: hsl(215, 25%, 91%);
background-color: hsl(215, 25%, 87%);
}
.styles-section .simple-selector.filter-match {
......
......@@ -108,7 +108,8 @@ Main.Main = class {
Runtime.experiments.register('applyCustomStylesheet', 'Allow custom UI themes');
Runtime.experiments.register('blackboxJSFramesOnTimeline', 'Blackbox JavaScript frames on Timeline', true);
Runtime.experiments.register('colorContrastRatio', 'Color contrast ratio line in color picker', true);
Runtime.experiments.register('consoleBelowPrompt', 'Eager evaluation');
Runtime.experiments.register('consoleBelowPrompt', 'Console eager evaluation');
Runtime.experiments.register('consoleKeyboardNavigation', 'Console keyboard navigation', true);
Runtime.experiments.register('emptySourceMapAutoStepping', 'Empty sourcemap auto-stepping');
Runtime.experiments.register('inputEventsOnTimelineOverview', 'Input events on Timeline overview', true);
Runtime.experiments.register('nativeHeapProfiler', 'Native memory sampling heap profiler', true);
......@@ -143,7 +144,7 @@ Main.Main = class {
if (testPath.indexOf('network/') !== -1)
Runtime.experiments.enableForTest('networkSearch');
if (testPath.indexOf('console/viewport-testing/') !== -1)
Runtime.experiments.enableForTest('consoleBelowPrompt');
Runtime.experiments.enableForTest('consoleKeyboardNavigation');
if (testPath.indexOf('console/') !== -1)
Runtime.experiments.enableForTest('pinnedExpressions');
}
......
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