Abort cursor moves via a sentinel, not a callback

This is less flexible, but makes it possible to enforce that aborting
stops are never targeted.

Change-Id: I32fa19f0c64c29597cae96a61a607bdfcd066b78
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 d11bc10..9fdbb34 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
@@ -48,6 +48,19 @@
   ABORTED,
 }
 
+/** A sentinel that can be inserted to disallow moving across. */
+export class AbortStop {}
+
+export type Stop = HTMLElement | AbortStop;
+
+/**
+ * Type guard and checker to check if a stop can be targetted.
+ * Abort stops cannot be targetted.
+ */
+export function isTargetable(stop: Stop): stop is HTMLElement {
+  return !(stop instanceof AbortStop);
+}
+
 @customElement('gr-cursor-manager')
 export class GrCursorManager extends GestureEventListeners(
   LegacyElementMixin(PolymerElement)
@@ -95,7 +108,12 @@
   focusOnMove = false;
 
   @property({type: Array})
-  stops: HTMLElement[] = [];
+  stops: Stop[] = [];
+
+  /** Only non-AbortStop stops. */
+  get targetableStops(): HTMLElement[] {
+    return this.stops.filter(isTargetable);
+  }
 
   /** @override */
   detached() {
@@ -106,9 +124,6 @@
   /**
    * Move the cursor forward. Clipped to the ends of the stop list.
    *
-   * @param options.abort Will abort moving the cursor when encountering a
-   *    stop for which this condition is met. Will abort even if the stop
-   *    would have been filtered
    * @param options.filter Will keep going and skip any stops for which this
    *    condition is not met.
    * @param options.getTargetHeight Optional function to calculate the
@@ -122,7 +137,6 @@
   next(
     options: {
       filter?: (stop: HTMLElement) => boolean;
-      abort?: (stop: HTMLElement) => boolean;
       getTargetHeight?: (target: HTMLElement) => number;
       clipToTop?: boolean;
     } = {}
@@ -133,7 +147,6 @@
   previous(
     options: {
       filter?: (stop: HTMLElement) => boolean;
-      abort?: (stop: HTMLElement) => boolean;
     } = {}
   ): CursorMoveResult {
     return this._moveCursor(-1, options);
@@ -152,7 +165,9 @@
     if (!this.stops || !this._isIntersectionObserverSupported()) {
       return;
     }
-    const filteredStops = condition ? this.stops.filter(condition) : this.stops;
+    const filteredStops = condition
+      ? this.targetableStops.filter(condition)
+      : this.targetableStops;
     const dims = this._getWindowDims();
     const windowCenter = Math.round(dims.innerHeight / 2);
 
@@ -219,7 +234,7 @@
    * setting the cursor.
    */
   setCursor(element: HTMLElement, noScroll?: boolean) {
-    if (!this.stops.includes(element)) {
+    if (!this.targetableStops.includes(element)) {
       this.unsetCursor();
       return;
     }
@@ -258,18 +273,21 @@
 
   moveToStart() {
     if (this.stops.length) {
-      this.setCursor(this.stops[0]);
+      this.setCursorAtIndex(0);
     }
   }
 
   moveToEnd() {
     if (this.stops.length) {
-      this.setCursor(this.stops[this.stops.length - 1]);
+      this.setCursorAtIndex(this.stops.length - 1);
     }
   }
 
   setCursorAtIndex(index: number, noScroll?: boolean) {
-    this.setCursor(this.stops[index], noScroll);
+    const stop = this.stops[index];
+    if (isTargetable(stop)) {
+      this.setCursor(stop, noScroll);
+    }
   }
 
   /**
@@ -294,12 +312,10 @@
     delta: number,
     {
       filter,
-      abort,
       getTargetHeight,
       clipToTop,
     }: {
       filter?: (stop: HTMLElement) => boolean;
-      abort?: (stop: HTMLElement) => boolean;
       getTargetHeight?: (target: HTMLElement) => number;
       clipToTop?: boolean;
     } = {}
@@ -317,7 +333,7 @@
     }
 
     let clipped = false;
-
+    let newStop: Stop;
     do {
       newIndex += delta;
       if (
@@ -325,19 +341,23 @@
         (delta < 0 && newIndex < 0)
       ) {
         newIndex = delta < 0 || clipToTop ? 0 : this.stops.length - 1;
+        newStop = this.stops[newIndex];
         clipped = true;
         break;
       }
-      if (abort && abort(this.stops[newIndex])) {
-        newIndex = this.index;
-        return CursorMoveResult.ABORTED;
-      }
-    } while (filter && !filter(this.stops[newIndex]));
+      // Sadly needed so that type narrowing understands that this.stops[newIndex] is
+      // targetable after I have checked that.
+      newStop = this.stops[newIndex];
+    } while (isTargetable(newStop) && filter && !filter(newStop));
+
+    if (!isTargetable(newStop)) {
+      return CursorMoveResult.ABORTED;
+    }
 
     this._unDecorateTarget();
 
     this.index = newIndex;
-    this.target = this.stops[newIndex];
+    this.target = newStop;
 
     if (getTargetHeight) {
       this._targetHeight = getTargetHeight(this.target);