Properly bookend file-list hotkey scrolling
The addition of incremental file loading in the file-list introduced an edge case in which the user could scroll off the file-list with hotkeys. This change implements the gr-cursor-manager to control file-list scrolling behavior. In addition, some interesting edge cases with the cursor-manager were uncovered during use -- these were also fixed. Change-Id: I936d4368058212c98684bc2617be2d98bdf6e41d
This commit is contained in:
@@ -21,6 +21,7 @@ limitations under the License.
|
||||
<link rel="import" href="../../diff/gr-diff/gr-diff.html">
|
||||
<link rel="import" href="../../diff/gr-diff-cursor/gr-diff-cursor.html">
|
||||
<link rel="import" href="../../shared/gr-button/gr-button.html">
|
||||
<link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html">
|
||||
<link rel="import" href="../../shared/gr-linked-text/gr-linked-text.html">
|
||||
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
|
||||
<link rel="import" href="../../shared/gr-select/gr-select.html">
|
||||
@@ -64,7 +65,7 @@ limitations under the License.
|
||||
.row:not(.header):hover {
|
||||
background-color: #f5fafd;
|
||||
}
|
||||
.row[selected] {
|
||||
.row.selected {
|
||||
background-color: #ebf5fb;
|
||||
}
|
||||
.path {
|
||||
@@ -145,7 +146,7 @@ limitations under the License.
|
||||
max-width: 25em;
|
||||
}
|
||||
@media screen and (max-width: 50em) {
|
||||
.row[selected] {
|
||||
.row.selected {
|
||||
background-color: transparent;
|
||||
}
|
||||
.stats {
|
||||
@@ -207,7 +208,7 @@ limitations under the License.
|
||||
items="[[_shownFiles]]"
|
||||
as="file"
|
||||
initial-count="[[_fileListIncrement]]">
|
||||
<div class="row" selected$="[[_computeFileSelected(index, selectedIndex)]]">
|
||||
<div class="file-row row" selected$="[[_computeFileSelected(index, selectedIndex)]]">
|
||||
<div class="reviewed" hidden$="[[!_loggedIn]]" hidden>
|
||||
<input type="checkbox" checked$="[[_computeReviewed(file, _reviewed)]]"
|
||||
data-path$="[[file.__path]]" on-change="_handleReviewedChange"
|
||||
@@ -302,7 +303,11 @@ limitations under the License.
|
||||
</gr-button>
|
||||
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
|
||||
<gr-storage id="storage"></gr-storage>
|
||||
<gr-diff-cursor id="cursor"></gr-diff-cursor>
|
||||
<gr-diff-cursor id="diffCursor"></gr-diff-cursor>
|
||||
<gr-cursor-manager
|
||||
id="fileCursor"
|
||||
scroll-behavior="keep-visible"
|
||||
cursor-target-class="selected"></gr-cursor-manager>
|
||||
</template>
|
||||
<script src="gr-file-list.js"></script>
|
||||
</dom-module>
|
||||
|
||||
@@ -35,10 +35,6 @@
|
||||
drafts: Object,
|
||||
revisions: Object,
|
||||
projectConfig: Object,
|
||||
selectedIndex: {
|
||||
type: Number,
|
||||
notify: true,
|
||||
},
|
||||
keyEventTarget: {
|
||||
type: Object,
|
||||
value: function() { return document.body; },
|
||||
@@ -253,7 +249,7 @@
|
||||
this.set(['_shownFiles', i, '__expanded'], false);
|
||||
this.set(['_files', i, '__expanded'], false);
|
||||
}
|
||||
this.$.cursor.handleDiffUpdate();
|
||||
this.$.diffCursor.handleDiffUpdate();
|
||||
},
|
||||
|
||||
_computeCommentsString: function(comments, patchNum, path) {
|
||||
@@ -326,7 +322,7 @@
|
||||
if (!this._showInlineDiffs) { return; }
|
||||
|
||||
e.preventDefault();
|
||||
this.$.cursor.moveLeft();
|
||||
this.$.diffCursor.moveLeft();
|
||||
},
|
||||
|
||||
_handleShiftRightKey: function(e) {
|
||||
@@ -334,20 +330,20 @@
|
||||
if (!this._showInlineDiffs) { return; }
|
||||
|
||||
e.preventDefault();
|
||||
this.$.cursor.moveRight();
|
||||
this.$.diffCursor.moveRight();
|
||||
},
|
||||
|
||||
_handleIKey: function(e) {
|
||||
if (this.shouldSuppressKeyboardShortcut(e)) { return; }
|
||||
if (this.selectedIndex === undefined) { return; }
|
||||
if (this.$.fileCursor.index === -1) { return; }
|
||||
|
||||
e.preventDefault();
|
||||
var expanded = this._files[this.selectedIndex].__expanded;
|
||||
var expanded = this._files[this.$.fileCursor.index].__expanded;
|
||||
// Until Polymer 2.0, manual management of reflection between _files
|
||||
// and _shownFiles is necessary.
|
||||
this.set(['_shownFiles', this.selectedIndex, '__expanded'],
|
||||
this.set(['_shownFiles', this.$.fileCursor.index, '__expanded'],
|
||||
!expanded);
|
||||
this.set(['_files', this.selectedIndex, '__expanded'], !expanded);
|
||||
this.set(['_files', this.$.fileCursor.index, '__expanded'], !expanded);
|
||||
},
|
||||
|
||||
_handleCapitalIKey: function(e) {
|
||||
@@ -359,14 +355,11 @@
|
||||
|
||||
_handleDownKey: function(e) {
|
||||
if (this.shouldSuppressKeyboardShortcut(e)) { return; }
|
||||
|
||||
e.preventDefault();
|
||||
if (this._showInlineDiffs) {
|
||||
this.$.cursor.moveDown();
|
||||
this.$.diffCursor.moveDown();
|
||||
} else {
|
||||
this.selectedIndex =
|
||||
Math.min(this._numFilesShown, this.selectedIndex + 1);
|
||||
this._scrollToSelectedFile();
|
||||
this.$.fileCursor.next();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -375,10 +368,9 @@
|
||||
|
||||
e.preventDefault();
|
||||
if (this._showInlineDiffs) {
|
||||
this.$.cursor.moveUp();
|
||||
this.$.diffCursor.moveUp();
|
||||
} else {
|
||||
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
||||
this._scrollToSelectedFile();
|
||||
this.$.fileCursor.previous();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -425,9 +417,9 @@
|
||||
|
||||
e.preventDefault();
|
||||
if (e.shiftKey) {
|
||||
this.$.cursor.moveToNextCommentThread();
|
||||
this.$.diffCursor.moveToNextCommentThread();
|
||||
} else {
|
||||
this.$.cursor.moveToNextChunk();
|
||||
this.$.diffCursor.moveToNextChunk();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -437,9 +429,9 @@
|
||||
|
||||
e.preventDefault();
|
||||
if (e.shiftKey) {
|
||||
this.$.cursor.moveToPreviousCommentThread();
|
||||
this.$.diffCursor.moveToPreviousCommentThread();
|
||||
} else {
|
||||
this.$.cursor.moveToPreviousChunk();
|
||||
this.$.diffCursor.moveToPreviousChunk();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -461,43 +453,27 @@
|
||||
},
|
||||
|
||||
_openCursorFile: function() {
|
||||
var diff = this.$.cursor.getTargetDiffElement();
|
||||
var diff = this.$.diffCursor.getTargetDiffElement();
|
||||
page.show(this._computeDiffURL(diff.changeNum, diff.patchRange,
|
||||
diff.path));
|
||||
},
|
||||
|
||||
_openSelectedFile: function(opt_index) {
|
||||
if (opt_index != null) {
|
||||
this.selectedIndex = opt_index;
|
||||
this.$.fileCursor.setCursorAtIndex(opt_index);
|
||||
}
|
||||
page.show(this._computeDiffURL(this.changeNum, this.patchRange,
|
||||
this._files[this.selectedIndex].__path));
|
||||
this._files[this.$.fileCursor.index].__path));
|
||||
},
|
||||
|
||||
_addDraftAtTarget: function() {
|
||||
var diff = this.$.cursor.getTargetDiffElement();
|
||||
var target = this.$.cursor.getTargetLineElement();
|
||||
var diff = this.$.diffCursor.getTargetDiffElement();
|
||||
var target = this.$.diffCursor.getTargetLineElement();
|
||||
if (diff && target) {
|
||||
diff.addDraftAtLine(target);
|
||||
}
|
||||
},
|
||||
|
||||
_scrollToSelectedFile: function() {
|
||||
var el = this.$$('.row[selected]');
|
||||
var top = 0;
|
||||
for (var node = el; node; node = node.offsetParent) {
|
||||
top += node.offsetTop;
|
||||
}
|
||||
|
||||
// Don't scroll if it's already in view.
|
||||
if (top > window.pageYOffset &&
|
||||
top < window.pageYOffset + window.innerHeight - el.clientHeight) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.scrollTo(0, top - document.body.clientHeight / 2);
|
||||
},
|
||||
|
||||
_shouldHideChangeTotals: function(_patchChange) {
|
||||
return _patchChange.inserted === 0 && _patchChange.deleted === 0;
|
||||
},
|
||||
@@ -576,8 +552,12 @@
|
||||
var diffElements = Polymer.dom(this.root).querySelectorAll('gr-diff');
|
||||
|
||||
// Overwrite the cursor's list of diffs:
|
||||
this.$.cursor.splice.apply(this.$.cursor,
|
||||
['diffs', 0, this.$.cursor.diffs.length].concat(diffElements));
|
||||
this.$.diffCursor.splice.apply(this.$.diffCursor,
|
||||
['diffs', 0, this.$.diffCursor.diffs.length].concat(diffElements));
|
||||
|
||||
var files = Polymer.dom(this.root).querySelectorAll('.file-row');
|
||||
this.$.fileCursor.stops = files;
|
||||
if (this.$.fileCursor.index === -1) { this.$.fileCursor.moveToStart(); }
|
||||
}.bind(this), 1);
|
||||
},
|
||||
|
||||
|
||||
@@ -279,7 +279,7 @@ limitations under the License.
|
||||
basePatchNum: 'PARENT',
|
||||
patchNum: '2',
|
||||
};
|
||||
element.selectedIndex = 0;
|
||||
element.$.fileCursor.setCursorAtIndex(0);
|
||||
});
|
||||
|
||||
test('toggle left diff via shortcut', function() {
|
||||
@@ -298,24 +298,26 @@ limitations under the License.
|
||||
|
||||
test('keyboard shortcuts', function() {
|
||||
flushAsynchronousOperations();
|
||||
var elementItems = Polymer.dom(element.root).querySelectorAll(
|
||||
'.row:not(.header)');
|
||||
assert.equal(elementItems.length, 5);
|
||||
assert.isTrue(elementItems[0].hasAttribute('selected'));
|
||||
assert.isFalse(elementItems[1].hasAttribute('selected'));
|
||||
assert.isFalse(elementItems[2].hasAttribute('selected'));
|
||||
|
||||
var items = Polymer.dom(element.root).querySelectorAll('.file-row');
|
||||
element.$.fileCursor.stops = items;
|
||||
element.$.fileCursor.setCursorAtIndex(0);
|
||||
assert.equal(items.length, 3);
|
||||
assert.isTrue(items[0].classList.contains('selected'));
|
||||
assert.isFalse(items[1].classList.contains('selected'));
|
||||
assert.isFalse(items[2].classList.contains('selected'));
|
||||
MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
|
||||
assert.equal(element.selectedIndex, 1);
|
||||
assert.equal(element.$.fileCursor.index, 1);
|
||||
MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
|
||||
|
||||
var showStub = sandbox.stub(page, 'show');
|
||||
assert.equal(element.selectedIndex, 2);
|
||||
assert.equal(element.$.fileCursor.index, 2);
|
||||
MockInteractions.pressAndReleaseKeyOn(element, 13, null, 'enter');
|
||||
assert(showStub.lastCall.calledWith('/c/42/2/myfile.txt'),
|
||||
'Should navigate to /c/42/2/myfile.txt');
|
||||
|
||||
MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
|
||||
assert.equal(element.selectedIndex, 1);
|
||||
assert.equal(element.$.fileCursor.index, 1);
|
||||
MockInteractions.pressAndReleaseKeyOn(element, 79, null, 'o');
|
||||
assert(showStub.lastCall.calledWith('/c/42/2/file_added_in_rev2.txt'),
|
||||
'Should navigate to /c/42/2/file_added_in_rev2.txt');
|
||||
@@ -323,20 +325,20 @@ limitations under the License.
|
||||
MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
|
||||
MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
|
||||
MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
|
||||
assert.equal(element.selectedIndex, 0);
|
||||
|
||||
showStub.restore();
|
||||
assert.equal(element.$.fileCursor.index, 0);
|
||||
});
|
||||
|
||||
test('i key shows/hides selected inline diff', function() {
|
||||
element.selectedIndex = 0;
|
||||
flushAsynchronousOperations();
|
||||
element.$.fileCursor.stops = element.diffs;
|
||||
element.$.fileCursor.setCursorAtIndex(0);
|
||||
MockInteractions.pressAndReleaseKeyOn(element, 73, null, 'i');
|
||||
flushAsynchronousOperations();
|
||||
assert.isFalse(element.diffs[0].hasAttribute('hidden'));
|
||||
MockInteractions.pressAndReleaseKeyOn(element, 73, null, 'i');
|
||||
flushAsynchronousOperations();
|
||||
assert.isTrue(element.diffs[0].hasAttribute('hidden'));
|
||||
element.selectedIndex = 1;
|
||||
element.$.fileCursor.setCursorAtIndex(1);
|
||||
MockInteractions.pressAndReleaseKeyOn(element, 73, null, 'i');
|
||||
flushAsynchronousOperations();
|
||||
assert.isFalse(element.diffs[1].hasAttribute('hidden'));
|
||||
@@ -416,7 +418,7 @@ limitations under the License.
|
||||
basePatchNum: 'PARENT',
|
||||
patchNum: '2',
|
||||
};
|
||||
element.selectedIndex = 0;
|
||||
element.$.fileCursor.setCursorAtIndex(0);
|
||||
|
||||
flushAsynchronousOperations();
|
||||
var fileRows =
|
||||
@@ -499,7 +501,7 @@ limitations under the License.
|
||||
basePatchNum: 'PARENT',
|
||||
patchNum: '2',
|
||||
};
|
||||
element.selectedIndex = 0;
|
||||
element.$.fileCursor.setCursorAtIndex(0);
|
||||
flushAsynchronousOperations();
|
||||
var fileRows =
|
||||
Polymer.dom(element.root).querySelectorAll('.row:not(.header)');
|
||||
@@ -537,7 +539,7 @@ limitations under the License.
|
||||
basePatchNum: 'PARENT',
|
||||
patchNum: '2',
|
||||
};
|
||||
element.selectedIndex = 0;
|
||||
element.$.fileCursor.setCursorAtIndex(0);
|
||||
flushAsynchronousOperations();
|
||||
var diffDisplay = element.diffs[0];
|
||||
element._userPrefs = {diff_view: 'SIDE_BY_SIDE'};
|
||||
@@ -580,7 +582,7 @@ limitations under the License.
|
||||
patchNum: '2',
|
||||
};
|
||||
var computeSpy = sandbox.spy(element, '_fileListActionsVisible');
|
||||
element.selectedIndex = 0;
|
||||
element.$.fileCursor.setCursorAtIndex(0);
|
||||
element._numFilesShown = 1;
|
||||
flush(function() {
|
||||
assert.isTrue(computeSpy.lastCall.returnValue);
|
||||
|
||||
@@ -21,7 +21,7 @@ limitations under the License.
|
||||
<template>
|
||||
<gr-cursor-manager
|
||||
id="cursorManager"
|
||||
scroll="[[_scrollBehavior]]"
|
||||
scroll-behavior="[[_scrollBehavior]]"
|
||||
cursor-target-class="target-row"
|
||||
target="{{diffRow}}"></gr-cursor-manager>
|
||||
</template>
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
* 'keep-visible'. 'keep-visible' will only scroll if the cursor is beyond
|
||||
* the viewport.
|
||||
*/
|
||||
scroll: {
|
||||
scrollBehavior: {
|
||||
type: String,
|
||||
value: ScrollBehavior.NEVER,
|
||||
},
|
||||
@@ -108,6 +108,10 @@
|
||||
}
|
||||
},
|
||||
|
||||
setCursorAtIndex: function(index) {
|
||||
this.setCursor(this.stops[index]);
|
||||
},
|
||||
|
||||
/**
|
||||
* Move the cursor forward or backward by delta. Noop if moving past either
|
||||
* end of the stop list.
|
||||
@@ -194,7 +198,9 @@
|
||||
},
|
||||
|
||||
_scrollToTarget: function() {
|
||||
if (!this.target || this.scroll === ScrollBehavior.NEVER) { return; }
|
||||
if (!this.target || this.scrollBehavior === ScrollBehavior.NEVER) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate where the element is relative to the window.
|
||||
var top = this.target.offsetTop;
|
||||
@@ -204,7 +210,7 @@
|
||||
top += offsetParent.offsetTop;
|
||||
}
|
||||
|
||||
if (this.scroll === ScrollBehavior.KEEP_VISIBLE &&
|
||||
if (this.scrollBehavior === ScrollBehavior.KEEP_VISIBLE &&
|
||||
top > window.pageYOffset &&
|
||||
top < window.pageYOffset + window.innerHeight) { return; }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user