Shortcut for moving cursor to visible code
Page-up/page-down keys don't move the cursor. The subsequent up/down
keys scroll window back to the cursor position.
This behavior makes it harder to navigate through a diff view via
keyboard. To make behavior more consistent, the shortcut is added to
move cursor to currently visible code.
Bug: Issue 12221
Change-Id: I4f01ed3d3a5db983a8f85ec7a64f80280048c984
diff --git a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html
index 8a1ed87..495f8ab 100644
--- a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html
+++ b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html
@@ -148,6 +148,7 @@
NEXT_LINE: 'NEXT_LINE',
PREV_LINE: 'PREV_LINE',
+ VISIBLE_LINE: 'VISIBLE_LINE',
NEXT_CHUNK: 'NEXT_CHUNK',
PREV_CHUNK: 'PREV_CHUNK',
EXPAND_ALL_DIFF_CONTEXT: 'EXPAND_ALL_DIFF_CONTEXT',
@@ -237,6 +238,8 @@
_describe(Shortcut.NEXT_LINE, ShortcutSection.DIFFS, 'Go to next line');
_describe(Shortcut.PREV_LINE, ShortcutSection.DIFFS, 'Go to previous line');
+ _describe(Shortcut.VISIBLE_LINE, ShortcutSection.DIFFS,
+ 'Move cursor to currently visible code');
_describe(Shortcut.NEXT_CHUNK, ShortcutSection.DIFFS,
'Go to next diff chunk');
_describe(Shortcut.PREV_CHUNK, ShortcutSection.DIFFS,
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
index e8d629c..87152d8 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
@@ -180,6 +180,15 @@
}
}
+ moveToVisibleArea() {
+ if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE) {
+ this.$.cursorManager.moveToVisibleArea(
+ this._rowHasSide.bind(this));
+ } else {
+ this.$.cursorManager.moveToVisibleArea();
+ }
+ }
+
moveToNextChunk(opt_clipToTop) {
this.$.cursorManager.next(this._isFirstRowOfChunk.bind(this),
target => target.parentNode.scrollHeight, opt_clipToTop);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
index d49b562..d7eaabf 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -229,6 +229,7 @@
[this.Shortcut.RIGHT_PANE]: '_handleRightPane',
[this.Shortcut.NEXT_LINE]: '_handleNextLineOrFileWithComments',
[this.Shortcut.PREV_LINE]: '_handlePrevLineOrFileWithComments',
+ [this.Shortcut.VISIBLE_LINE]: '_handleVisibleLine',
[this.Shortcut.NEXT_FILE_WITH_COMMENTS]:
'_handleNextLineOrFileWithComments',
[this.Shortcut.PREV_FILE_WITH_COMMENTS]:
@@ -382,6 +383,13 @@
this.$.cursor.moveUp();
}
+ _handleVisibleLine(e) {
+ if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+
+ e.preventDefault();
+ this.$.cursor.moveToVisibleArea();
+ }
+
_onOpenFixPreview(e) {
this.$.applyFixDialog.open(e);
}
diff --git a/polygerrit-ui/app/elements/gr-app-element.js b/polygerrit-ui/app/elements/gr-app-element.js
index d39ba58..c1e317a 100644
--- a/polygerrit-ui/app/elements/gr-app-element.js
+++ b/polygerrit-ui/app/elements/gr-app-element.js
@@ -240,6 +240,10 @@
this.Shortcut.NEXT_LINE, 'j', 'down');
this.bindShortcut(
this.Shortcut.PREV_LINE, 'k', 'up');
+ if (this._isCursorManagerSupportMoveToVisibleLine()) {
+ this.bindShortcut(
+ this.Shortcut.VISIBLE_LINE, '.');
+ }
this.bindShortcut(
this.Shortcut.NEXT_CHUNK, 'n');
this.bindShortcut(
@@ -303,6 +307,14 @@
this.Shortcut.SEARCH, '/');
}
+ _isCursorManagerSupportMoveToVisibleLine() {
+ // This method is a copy-paste from the
+ // method _isIntersectionObserverSupported of gr-cursor-manager.js
+ // It is better share this method with gr-cursor-manager,
+ // but doing it require a lot if changes instead of 1-line copied code
+ return 'IntersectionObserver' in window;
+ }
+
_accountChanged(account) {
if (!account) { return; }
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
index 37c7147..4f98e88 100644
--- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
@@ -131,6 +131,75 @@
}
/**
+ * Move the cursor to the row which is the closest to the viewport center
+ * in vertical direction.
+ * The method uses IntersectionObservers API. If browser
+ * doesn't support this API the method does nothing
+ *
+ * @param {!Function=} opt_condition Optional condition. If a condition
+ * is passed only stops which meet conditions are taken into account.
+ */
+ moveToVisibleArea(opt_condition) {
+ if (!this.stops || !this._isIntersectionObserverSupported()) {
+ return;
+ }
+ const filteredStops = opt_condition ? this.stops.filter(opt_condition)
+ : this.stops;
+ const dims = this._getWindowDims();
+ const windowCenter =
+ Math.round((dims.innerHeight + this.scrollTopMargin) / 2);
+
+ let closestToTheCenter = null;
+ let minDistanceToCenter = null;
+ let unobservedCount = filteredStops.length;
+
+ const observer = new IntersectionObserver(entries => {
+ // This callback is called for the first time immediately.
+ // Typically it gets all observed stops at once, but
+ // sometimes can get them in several chunks.
+ entries.forEach(entry => {
+ observer.unobserve(entry.target);
+
+ // In Edge it is recommended to use intersectionRatio instead of
+ // isIntersecting.
+ const isInsideViewport =
+ entry.isIntersecting || entry.intersectionRatio > 0;
+ if (!isInsideViewport) {
+ return;
+ }
+ const center = entry.boundingClientRect.top + Math.round(
+ entry.boundingClientRect.height / 2);
+ const distanceToWindowCenter = Math.abs(center - windowCenter);
+ if (minDistanceToCenter === null ||
+ distanceToWindowCenter < minDistanceToCenter) {
+ closestToTheCenter = entry.target;
+ minDistanceToCenter = distanceToWindowCenter;
+ }
+ });
+ unobservedCount -= entries.length;
+ if (unobservedCount == 0 && closestToTheCenter) {
+ // set cursor when all stops were observed.
+ // In most cases the target is visible, so scroll is not
+ // needed. But in rare cases the target can become invisible
+ // at this point (due to some scrolling in window).
+ // To avoid jumps set noScroll options.
+ this.setCursor(closestToTheCenter, true);
+ }
+ });
+ filteredStops.forEach(stop => {
+ observer.observe(stop);
+ });
+ }
+
+ _isIntersectionObserverSupported() {
+ // The copy of this method exists in gr-app-element.js under the
+ // name _isCursorManagerSupportMoveToVisibleLine
+ // If you update this method, you must update gr-app-element.js
+ // as well.
+ return 'IntersectionObserver' in window;
+ }
+
+ /**
* Set the cursor to an arbitrary element.
*
* @param {!HTMLElement} element