Expose whether moving the cursor was successful

This is useful to handle things such as the next-file toast which is
currently handled by gr-cursor-manager directly, but should probably be
moved out to keep gr-cursor-manager independent from concepts like
files.

Change-Id: I74a531ddcd74274cf18aed888f4fc31400257e37
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts
index ffdd662..47a1cbb 100644
--- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts
@@ -34,6 +34,18 @@
 // Time in which pressing n key again after the toast navigates to next file
 const NAVIGATE_TO_NEXT_FILE_TIMEOUT_MS = 5000;
 
+/**
+ * Return type for cursor moves, that indicate whether a move was possible.
+ */
+export enum CursorMoveResult {
+  /** The cursor was successfully moved. */
+  MOVED,
+  /** There were no stops - the cursor was reset. */
+  NO_STOPS,
+  /** There was no more stop to move to - the cursor was clipped to the end. */
+  CLIPPED,
+}
+
 @customElement('gr-cursor-manager')
 export class GrCursorManager extends GestureEventListeners(
   LegacyElementMixin(PolymerElement)
@@ -104,16 +116,16 @@
    *     back to first instead of to last.
    * @param navigateToNextFile Navigate to next unreviewed file
    *     if user presses next on the last diff chunk
+   * @return If a move was performed or why not.
    * @private
    */
-
   next(
     condition?: Function,
     getTargetHeight?: (target: HTMLElement) => number,
     clipToTop?: boolean,
     navigateToNextFile?: boolean
-  ) {
-    this._moveCursor(
+  ): CursorMoveResult {
+    return this._moveCursor(
       1,
       condition,
       getTargetHeight,
@@ -122,8 +134,8 @@
     );
   }
 
-  previous(condition?: Function) {
-    this._moveCursor(-1, condition);
+  previous(condition?: Function): CursorMoveResult {
+    return this._moveCursor(-1, condition);
   }
 
   /**
@@ -269,6 +281,7 @@
    * back to first instead of to last.
    * @param navigateToNextFile Navigate to next unreviewed file
    * if user presses next on the last diff chunk
+   * @return  If a move was performed or why not.
    * @private
    */
   _moveCursor(
@@ -277,27 +290,25 @@
     getTargetHeight?: (target: HTMLElement) => number,
     clipToTop?: boolean,
     navigateToNextFile?: boolean
-  ) {
+  ): CursorMoveResult {
     if (!this.stops.length) {
       this.unsetCursor();
-      return;
+      return CursorMoveResult.NO_STOPS;
     }
 
     this._unDecorateTarget();
 
     const newIndex = this._getNextindex(delta, condition, clipToTop);
+    const newTarget = newIndex !== -1 ? this.stops[newIndex] : null;
 
-    let newTarget = null;
-    if (newIndex !== -1) {
-      newTarget = this.stops[newIndex];
-    }
+    const clipped = this.index === newIndex;
 
     /*
      * If user presses n on the last diff chunk, show a toast informing user
      * that pressing n again will navigate them to next unreviewed file.
      * If click happens within the time limit, then navigate to next file
      */
-    if (navigateToNextFile && this.index === newIndex && this.isAtEnd()) {
+    if (navigateToNextFile && clipped && this.isAtEnd()) {
       if (
         this._lastDisplayedNavigateToNextFileToast &&
         Date.now() - this._lastDisplayedNavigateToNextFileToast <=
@@ -311,7 +322,7 @@
             bubbles: true,
           })
         );
-        return;
+        return CursorMoveResult.CLIPPED;
       }
       this._lastDisplayedNavigateToNextFileToast = Date.now();
       this.dispatchEvent(
@@ -323,14 +334,14 @@
           bubbles: true,
         })
       );
-      return;
+      return CursorMoveResult.CLIPPED;
     }
 
     this.index = newIndex;
     this.target = newTarget as HTMLElement;
 
     if (!newTarget) {
-      return;
+      return CursorMoveResult.NO_STOPS;
     }
 
     if (getTargetHeight) {
@@ -344,6 +355,8 @@
     }
 
     this._decorateTarget();
+
+    return clipped ? CursorMoveResult.CLIPPED : CursorMoveResult.MOVED;
   }
 
   _decorateTarget() {
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.js b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.js
index bc07d84..5c0bb42 100644
--- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.js
@@ -18,6 +18,7 @@
 import '../../../test/common-test-setup-karma.js';
 import './gr-cursor-manager.js';
 import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+import {CursorMoveResult} from './gr-cursor-manager.js';
 
 const basicTestFixutre = fixtureFromTemplate(html`
     <gr-cursor-manager cursor-target-class="targeted"></gr-cursor-manager>
@@ -66,10 +67,11 @@
     assert.isFalse(element.isAtEnd());
 
     // Progress the cursor.
-    element.next();
+    let result = element.next();
 
     // Confirm that the next stop is selected and that the previous stop is
     // unselected.
+    assert.equal(result, CursorMoveResult.MOVED);
     assert.equal(element.index, 3);
     assert.equal(element.target, list.children[3]);
     assert.isTrue(element.isAtEnd());
@@ -77,19 +79,23 @@
     assert.isTrue(list.children[3].classList.contains('targeted'));
 
     // Progress the cursor.
-    element.next();
+    result = element.next();
 
     // We should still be at the end.
+    assert.equal(result, CursorMoveResult.CLIPPED);
     assert.equal(element.index, 3);
     assert.equal(element.target, list.children[3]);
     assert.isTrue(element.isAtEnd());
 
     // Wind the cursor all the way back to the first stop.
-    element.previous();
-    element.previous();
-    element.previous();
+    result = element.previous();
+    assert.equal(result, CursorMoveResult.MOVED);
+    result = element.previous();
+    assert.equal(result, CursorMoveResult.MOVED);
+    result = element.previous();
+    assert.equal(result, CursorMoveResult.MOVED);
 
-    // The element state should reflect the end of the list.
+    // The element state should reflect the start of the list.
     assert.equal(element.index, 0);
     assert.equal(element.target, list.children[0]);
     assert.isTrue(element.isAtStart());
@@ -113,8 +119,9 @@
 
   test('next() goes to first element when no cursor is set', () => {
     element.stops = list.querySelectorAll('li');
-    element.next();
+    const result = element.next();
 
+    assert.equal(result, CursorMoveResult.MOVED);
     assert.equal(element.index, 0);
     assert.equal(element.target, list.children[0]);
     assert.isTrue(list.children[0].classList.contains('targeted'));
@@ -122,10 +129,23 @@
     assert.isFalse(element.isAtEnd());
   });
 
-  test('next() goes to first element when no cursor is set', () => {
-    element.stops = list.querySelectorAll('li');
-    element.previous();
+  test('next() resets the cursor when there are no stops', () => {
+    element.stops = [];
+    const result = element.next();
 
+    assert.equal(result, CursorMoveResult.NO_STOPS);
+    assert.equal(element.index, -1);
+    assert.isNotOk(element.target);
+    assert.isFalse(list.children[1].classList.contains('targeted'));
+    assert.isFalse(element.isAtStart());
+    assert.isFalse(element.isAtEnd());
+  });
+
+  test('previous() goes to last element when no cursor is set', () => {
+    element.stops = list.querySelectorAll('li');
+    const result = element.previous();
+
+    assert.equal(result, CursorMoveResult.MOVED);
     const lastIndex = list.children.length - 1;
     assert.equal(element.index, lastIndex);
     assert.equal(element.target, list.children[lastIndex]);
@@ -134,6 +154,18 @@
     assert.isTrue(element.isAtEnd());
   });
 
+  test('previous() resets the cursor when there are no stops', () => {
+    element.stops = [];
+    const result = element.previous();
+
+    assert.equal(result, CursorMoveResult.NO_STOPS);
+    assert.equal(element.index, -1);
+    assert.isNotOk(element.target);
+    assert.isFalse(list.children[1].classList.contains('targeted'));
+    assert.isFalse(element.isAtStart());
+    assert.isFalse(element.isAtEnd());
+  });
+
   test('_moveCursor', () => {
     // Initialize the cursor with its stops.
     element.stops = list.querySelectorAll('li');