Commit cc2fb34c authored by Erik Luo's avatar Erik Luo Committed by Commit Bot

DevTools: fix stick-to-bottom for Console with keyboard navigation

Keyboard navigation in Console presents new interactions that our
stick-to-bottom (STB) logic should care about.

Prompt
- Before, STB only cared about TextChanged events, but now cares
  about focusin, since users can Tab from messages to prompt

Objects
- Expanding an object results in noticeable scroll-anchoring jumps,
  STB should turn off due to expanding. Expanding a small object
  that can fit in viewport should guarantee it is visible.

Bug: 865674
Change-Id: I47872110d4bf2261b4940b565f8c926333b66836
Reviewed-on: https://chromium-review.googlesource.com/c/1330350
Commit-Queue: Erik Luo <luoe@chromium.org>
Reviewed-by: default avatarJoel Einbinder <einbinder@chromium.org>
Cr-Commit-Position: refs/heads/master@{#613239}
parent e316cafe
......@@ -183,6 +183,8 @@ Console.ConsoleView = class extends UI.VBox {
this._messagesElement.addEventListener('clipboard-paste', this._messagesPasted.bind(this), true);
this._viewportThrottler = new Common.Throttler(50);
this._pendingBatchResize = false;
this._onMessageResizedBound = this._onMessageResized.bind(this);
this._topGroup = Console.ConsoleGroup.createTopGroup();
this._currentGroup = this._topGroup;
......@@ -215,8 +217,13 @@ Console.ConsoleView = class extends UI.VBox {
this._prompt.addEventListener(Console.ConsolePrompt.Events.TextChanged, this._promptTextChanged, this);
this._keyboardNavigationEnabled = Runtime.experiments.isEnabled('consoleKeyboardNavigation');
if (this._keyboardNavigationEnabled)
if (this._keyboardNavigationEnabled) {
this._messagesElement.addEventListener('keydown', this._messagesKeyDown.bind(this), false);
this._prompt.element.addEventListener('focusin', () => {
if (this._isScrolledToBottom())
this._viewport.setStickToBottom(true);
});
}
this._consoleHistoryAutocompleteSetting.addChangeListener(this._consoleHistoryAutocompleteChanged, this);
......@@ -625,18 +632,43 @@ Console.ConsoleView = class extends UI.VBox {
const nestingLevel = this._currentGroup.nestingLevel();
switch (message.type) {
case SDK.ConsoleMessage.MessageType.Command:
return new Console.ConsoleCommand(message, this._linkifier, this._badgePool, nestingLevel);
return new Console.ConsoleCommand(
message, this._linkifier, this._badgePool, nestingLevel, this._onMessageResizedBound);
case SDK.ConsoleMessage.MessageType.Result:
return new Console.ConsoleCommandResult(message, this._linkifier, this._badgePool, nestingLevel);
return new Console.ConsoleCommandResult(
message, this._linkifier, this._badgePool, nestingLevel, this._onMessageResizedBound);
case SDK.ConsoleMessage.MessageType.StartGroupCollapsed:
case SDK.ConsoleMessage.MessageType.StartGroup:
return new Console.ConsoleGroupViewMessage(
message, this._linkifier, this._badgePool, nestingLevel, this._updateMessageList.bind(this));
message, this._linkifier, this._badgePool, nestingLevel, this._updateMessageList.bind(this),
this._onMessageResizedBound);
default:
return new Console.ConsoleViewMessage(message, this._linkifier, this._badgePool, nestingLevel);
return new Console.ConsoleViewMessage(
message, this._linkifier, this._badgePool, nestingLevel, this._onMessageResizedBound);
}
}
/**
* @param {!Common.Event} event
* @return {!Promise}
*/
async _onMessageResized(event) {
if (!this._keyboardNavigationEnabled)
return;
const treeElement = /** @type {!UI.TreeElement} */ (event.data);
if (this._pendingBatchResize || !treeElement.treeOutline)
return;
this._pendingBatchResize = true;
await Promise.resolve();
const treeOutlineElement = treeElement.treeOutline.element;
this._viewport.setStickToBottom(this._isScrolledToBottom());
// Scroll, in case mutations moved the element below the visible area.
if (treeOutlineElement.offsetHeight <= this._messagesElement.offsetHeight)
treeOutlineElement.scrollIntoViewIfNeeded();
this._pendingBatchResize = false;
}
_consoleCleared() {
const hadFocus = this._viewport.element.hasFocus();
this._cancelBuildHiddenCache();
......@@ -1394,16 +1426,6 @@ Console.ConsoleViewFilter = class {
* @unrestricted
*/
Console.ConsoleCommand = class extends Console.ConsoleViewMessage {
/**
* @param {!SDK.ConsoleMessage} message
* @param {!Components.Linkifier} linkifier
* @param {!ProductRegistry.BadgePool} badgePool
* @param {number} nestingLevel
*/
constructor(message, linkifier, badgePool, nestingLevel) {
super(message, linkifier, badgePool, nestingLevel);
}
/**
* @override
* @return {!Element}
......@@ -1445,16 +1467,6 @@ Console.ConsoleCommand = class extends Console.ConsoleViewMessage {
Console.ConsoleCommand.MaxLengthToIgnoreHighlighter = 10000;
Console.ConsoleCommandResult = class extends Console.ConsoleViewMessage {
/**
* @param {!SDK.ConsoleMessage} message
* @param {!Components.Linkifier} linkifier
* @param {!ProductRegistry.BadgePool} badgePool
* @param {number} nestingLevel
*/
constructor(message, linkifier, badgePool, nestingLevel) {
super(message, linkifier, badgePool, nestingLevel);
}
/**
* @override
* @return {!Element}
......
......@@ -37,8 +37,9 @@ Console.ConsoleViewMessage = class {
* @param {!Components.Linkifier} linkifier
* @param {!ProductRegistry.BadgePool} badgePool
* @param {number} nestingLevel
* @param {function(!Common.Event)} onResize
*/
constructor(consoleMessage, linkifier, badgePool, nestingLevel) {
constructor(consoleMessage, linkifier, badgePool, nestingLevel, onResize) {
this._message = consoleMessage;
this._linkifier = linkifier;
this._badgePool = badgePool;
......@@ -47,6 +48,7 @@ Console.ConsoleViewMessage = class {
this._nestingLevel = nestingLevel;
/** @type {!Array<{element: !Element, selectFirst: function()}>} */
this._selectableChildren = [];
this._messageResized = onResize;
/** @type {?DataGrid.DataGrid} */
this._dataGrid = null;
......@@ -626,6 +628,9 @@ Console.ConsoleViewMessage = class {
section.enableContextMenu();
section.setShowSelectionOnKeyboardFocus(true, true);
this._selectableChildren.push(section);
section.addEventListener(UI.TreeOutline.Events.ElementAttached, this._messageResized);
section.addEventListener(UI.TreeOutline.Events.ElementExpanded, this._messageResized);
section.addEventListener(UI.TreeOutline.Events.ElementCollapsed, this._messageResized);
return section.element;
}
......@@ -695,8 +700,12 @@ Console.ConsoleViewMessage = class {
}
const renderResult = await UI.Renderer.render(/** @type {!Object} */ (node));
if (renderResult) {
if (renderResult.tree)
if (renderResult.tree) {
this._selectableChildren.push(renderResult.tree);
renderResult.tree.addEventListener(UI.TreeOutline.Events.ElementAttached, this._messageResized);
renderResult.tree.addEventListener(UI.TreeOutline.Events.ElementExpanded, this._messageResized);
renderResult.tree.addEventListener(UI.TreeOutline.Events.ElementCollapsed, this._messageResized);
}
result.appendChild(renderResult.node);
} else {
result.appendChild(this._formatParameterAsObject(remoteObject, false));
......@@ -1627,10 +1636,11 @@ Console.ConsoleGroupViewMessage = class extends Console.ConsoleViewMessage {
* @param {!ProductRegistry.BadgePool} badgePool
* @param {number} nestingLevel
* @param {function()} onToggle
* @param {function(!Common.Event)} onResize
*/
constructor(consoleMessage, linkifier, badgePool, nestingLevel, onToggle) {
constructor(consoleMessage, linkifier, badgePool, nestingLevel, onToggle, onResize) {
console.assert(consoleMessage.isGroupStartMessage());
super(consoleMessage, linkifier, badgePool, nestingLevel);
super(consoleMessage, linkifier, badgePool, nestingLevel, onResize);
this._collapsed = consoleMessage.type === SDK.ConsoleMessage.MessageType.StartGroupCollapsed;
/** @type {?UI.Icon} */
this._expandGroupIcon = null;
......
......@@ -133,6 +133,10 @@ Console.ConsoleViewport = class {
event.target === this._contentElement) {
focusLastChild = true;
this._virtualSelectedIndex = this._itemCount - 1;
// Update stick to bottom before scrolling into view.
this.refresh();
this.scrollItemIntoView(this._virtualSelectedIndex);
}
this._updateFocusedItem(focusLastChild);
}
......@@ -213,14 +217,14 @@ Console.ConsoleViewport = class {
const containerHasFocus = this._contentElement === this.element.ownerDocument.deepActiveElement();
if (this._lastSelectedElement && changed)
this._lastSelectedElement.classList.remove('console-selected');
if (selectedElement && (changed || containerHasFocus) && this.element.hasFocus()) {
if (selectedElement && (focusLastChild || changed || containerHasFocus) && this.element.hasFocus()) {
selectedElement.classList.add('console-selected');
// Do not focus the message if something within holds focus (e.g. object).
if (!selectedElement.hasFocus()) {
if (focusLastChild)
this._renderedItems[this._virtualSelectedIndex - this._firstActiveIndex].focusLastChildOrSelf();
else
focusWithoutScroll(selectedElement);
if (focusLastChild) {
this.setStickToBottom(false);
this._renderedItems[this._virtualSelectedIndex - this._firstActiveIndex].focusLastChildOrSelf();
} else if (!selectedElement.hasFocus()) {
focusWithoutScroll(selectedElement);
}
}
if (this._itemCount && !this._contentElement.hasFocus())
......
......@@ -82,6 +82,7 @@
word-wrap: break-word;
-webkit-user-select: text;
transform: translateZ(0);
overflow-anchor: none; /* Chrome-specific scroll-anchoring opt-out */
}
#console-prompt {
......
......@@ -29,8 +29,10 @@ ConsoleTestRunner.dumpConsoleMessages = function(printOriginatingCommand, dumpCl
ConsoleTestRunner.dumpConsoleMessagesIntoArray = function(printOriginatingCommand, dumpClassNames, formatter) {
formatter = formatter || ConsoleTestRunner.prepareConsoleMessageText;
const result = [];
ConsoleTestRunner.disableConsoleViewport();
const consoleView = Console.ConsoleView.instance();
const originalViewportStyle = consoleView._viewport.element.style;
const originalSize = {width: originalViewportStyle.width, height: originalViewportStyle.height};
ConsoleTestRunner.disableConsoleViewport();
if (consoleView._needsFullUpdate)
consoleView._updateMessageList();
const viewMessages = consoleView._visibleViewMessages;
......@@ -72,6 +74,8 @@ ConsoleTestRunner.dumpConsoleMessagesIntoArray = function(printOriginatingComman
if (printOriginatingCommand && uiMessage.consoleMessage().originatingMessage())
result.push('Originating from: ' + uiMessage.consoleMessage().originatingMessage().messageText);
}
consoleView._viewport.element.style.width = originalSize.width;
consoleView._viewport.element.style.height = originalSize.height;
return result;
};
......
......@@ -258,3 +258,41 @@ Has object: expanded
activeElement: LI.parent.object-properties-section-root-element.selected.expanded
active text: {x: 1}
Running: testArrowUpToFirstVisibleMessageShouldSelectLastObject
Evaluating: console.log(obj1);console.log("after");
Message count: 2
Setting focus in prompt:
Shift+Tab:
Viewport virtual selection: 1
Has object: collapsed
activeElement: DIV.console-message-wrapper.console-from-api.console-info-level.console-selected
active text: console-key-expand.js:191 after
ArrowUp:
Viewport virtual selection: 0
Has object: collapsed
activeElement: LI.parent.object-properties-section-root-element.selected
active text: {x: 1}
Running: testFocusLastChildInBigObjectShouldScrollIntoView
Evaluating: console.log(bigObj);
Message count: 1
Setting focus in prompt:
Shift+Tab:
ArrowRight:
Tab:
Viewport virtual selection: -1
Has object: expanded
activeElement: TEXTAREA
Shift+Tab:
Viewport virtual selection: 0
Has object: expanded
activeElement: LI.parent.object-properties-section-root-element.selected.expanded
active text: {a0: 0, a1: 1, a2: 2, a3: 3, a4: 4, …}
Is at bottom: false, should stick: false
......@@ -190,6 +190,46 @@
next();
},
async function testArrowUpToFirstVisibleMessageShouldSelectLastObject(next) {
await clearAndLog(`console.log(obj1);console.log("after");`, 2);
await ConsoleTestRunner.waitForRemoteObjectsConsoleMessagesPromise();
TestRunner.addResult(`Setting focus in prompt:`);
prompt.focus();
shiftPress('Tab');
dumpFocus(true);
press('ArrowUp');
dumpFocus(true);
next();
},
async function testFocusLastChildInBigObjectShouldScrollIntoView(next) {
await TestRunner.evaluateInPagePromise(`
var bigObj = Object.create(null);
for (var i = 0; i < 100; i++)
bigObj['a' + i] = i;
`);
await clearAndLog(`console.log(bigObj);`, 1);
await ConsoleTestRunner.waitForRemoteObjectsConsoleMessagesPromise();
TestRunner.addResult(`Setting focus in prompt:`);
prompt.focus();
shiftPress('Tab');
press('ArrowRight');
await ConsoleTestRunner.waitForRemoteObjectsConsoleMessagesPromise();
press('Tab');
dumpFocus(true);
shiftPress('Tab');
dumpFocus(true);
dumpScrollInfo();
next();
},
]);
......@@ -219,6 +259,13 @@
eventSender.keyDown(key, ['shiftKey']);
}
function dumpScrollInfo() {
viewport.refresh();
let infoText =
'Is at bottom: ' + viewport.element.isScrolledToBottom() + ', should stick: ' + viewport.stickToBottom();
TestRunner.addResult(infoText);
}
function dumpFocus(activeElement, messageIndex = 0, skipObjectCheck) {
const firstMessage = consoleView._visibleViewMessages[messageIndex];
const hasTrace = !!firstMessage.element().querySelector('.console-message-stack-trace-toggle');
......
......@@ -32,13 +32,13 @@ Shift+Tab:
DIV#console-messages.monospace
Tab:
DIV.console-group.console-group-messages
DIV:console-key-navigation.js:20 Message #99
Setting focus in prompt:
TEXTAREA:Code editor
Shift+Tab:
DIV.console-group.console-group-messages
DIV:console-key-navigation.js:20 Message #99
Scrolling to top of viewport
DIV.console-group.console-group-messages
......
Verifies viewport stick-to-bottom behavior when prompt has space below editable area.
Message count: 150
Running: testExpandLastVisibleObjectRemainsInView
Force selecting index 149
Is at bottom: true, should stick: true, selected element is fully visible? true
Expanding object
Is at bottom: false, should stick: false, selected element is fully visible? true
Collapsing object
Is at bottom: true, should stick: false, selected element is fully visible? true
Running: testExpandFirstVisibleObjectRemainsInView
Force selecting index 146
Is at bottom: false, should stick: false, selected element is fully visible? true
Expanding object
Is at bottom: false, should stick: false, selected element is fully visible? true
Collapsing object
Is at bottom: false, should stick: false, selected element is fully visible? true
// 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(`Verifies viewport stick-to-bottom behavior when prompt has space below editable area.\n`);
await TestRunner.loadModule('console_test_runner');
await TestRunner.showPanel('console');
await ConsoleTestRunner.waitUntilConsoleEditorLoaded();
ConsoleTestRunner.fixConsoleViewportDimensions(600, 200);
await TestRunner.evaluateInPagePromise(`
for (var i = 0; i < 150; ++i)
console.log({id: "#" + i, anotherKey: true, toGrowExpandedHeight: true});
//# sourceURL=console-viewport-stick-to-bottom-expand-object.js
`);
await ConsoleTestRunner.waitForConsoleMessagesPromise(150);
await ConsoleTestRunner.waitForPendingViewportUpdates();
const consoleView = Console.ConsoleView.instance();
const viewport = consoleView._viewport;
TestRunner.runTestSuite([
async function testExpandLastVisibleObjectRemainsInView(next) {
const index = consoleView._visibleViewMessages.length - 1;
forceSelect(index);
dumpInfo();
TestRunner.addResult('Expanding object');
const objectSection = consoleView._visibleViewMessages[index]._selectableChildren[0];
objectSection.objectTreeElement().expand();
await ConsoleTestRunner.waitForRemoteObjectsConsoleMessagesPromise();
dumpInfo();
TestRunner.addResult('Collapsing object');
objectSection.objectTreeElement().collapse();
dumpInfo();
next();
},
async function testExpandFirstVisibleObjectRemainsInView(next) {
const index = viewport.firstVisibleIndex() + 1; // add 1 for first "fully visible" item
forceSelect(index);
dumpInfo();
TestRunner.addResult('Expanding object');
const objectSection = consoleView._visibleViewMessages[index]._selectableChildren[0];
objectSection.objectTreeElement().expand();
dumpInfo();
TestRunner.addResult('Collapsing object');
objectSection.objectTreeElement().collapse();
dumpInfo();
next();
},
]);
function dumpInfo() {
viewport.refresh();
let infoText =
'Is at bottom: ' + viewport.element.isScrolledToBottom() + ', should stick: ' + viewport.stickToBottom();
const selectedElement = viewport.renderedElementAt(viewport._virtualSelectedIndex);
if (selectedElement) {
const selectedRect = selectedElement.getBoundingClientRect();
const viewportRect = viewport.element.getBoundingClientRect();
const fullyVisible = (selectedRect.top + 2 >= viewportRect.top && selectedRect.bottom - 2 <= viewportRect.bottom);
infoText += ', selected element is fully visible? ' + fullyVisible;
}
TestRunner.addResult(infoText);
}
function forceSelect(index) {
TestRunner.addResult(`\nForce selecting index ${index}`);
viewport._virtualSelectedIndex = index;
viewport._contentElement.focus();
viewport._updateFocusedItem();
}
})();
......@@ -11,9 +11,10 @@
const node = await ElementsTestRunner.nodeWithIdPromise('node');
ElementsTestRunner.firstElementsTreeOutline()._saveNodeToTempVariable(node);
const promise = TestRunner.addSnifferPromise(Console.ConsoleViewMessage.prototype, '_formattedParameterAsNodeForTest');
await ConsoleTestRunner.waitForConsoleMessagesPromise(2);
const secondMessage = Console.ConsoleView.instance()._visibleViewMessages[1];
await TestRunner.addSnifferPromise(secondMessage, '_formattedParameterAsNodeForTest');
await promise;
ConsoleTestRunner.dumpConsoleMessages();
TestRunner.completeTest();
......
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