Convert files to typescript

* gr-annotation
* gr-cursor-manager
* scripts/util

Change-Id: I95a1a3f5b7feb6204f25e8ebeba4dcea574a0fa9
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation.ts b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation.ts
index 3d837bf..e50bcd7 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation.ts
@@ -1,4 +1,3 @@
-
 /**
  * @license
  * Copyright (C) 2016 The Android Open Source Project
@@ -15,8 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
-import {sanitizeDOMValue} from '@polymer/polymer/lib/utils/settings.js';
+import {getSanitizeDOMValue} from '@polymer/polymer/lib/utils/settings';
 
 // TODO(wyatta): refactor this to be <MARK> rather than <HL>.
 const ANNOTATION_TAG = 'HL';
@@ -25,19 +23,16 @@
 const REGEX_ASTRAL_SYMBOL = /[\uD800-\uDBFF][\uDC00-\uDFFF]/;
 
 export const GrAnnotation = {
-
   /**
    * The DOM API textContent.length calculation is broken when the text
    * contains Unicode. See https://mathiasbynens.be/notes/javascript-unicode .
    *
-   * @param  {!Text} node text node.
-   * @return {number} The length of the text.
    */
-  getLength(node) {
-    return this.getStringLength(node.textContent);
+  getLength(node: Node) {
+    return this.getStringLength(node.textContent || '');
   },
 
-  getStringLength(str) {
+  getStringLength(str: string) {
     return str.replace(REGEX_ASTRAL_SYMBOL, '_').length;
   },
 
@@ -45,26 +40,34 @@
    * Annotates the [offset, offset+length) text segment in the parent with the
    * element definition provided as arguments.
    *
-   * @param {!Element} parent the node whose contents will be annotated.
-   * @param {number} offset the 0-based offset from which the annotation will
-   *   start.
-   * @param {number} length of the annotated text.
-   * @param {GrAnnotation.ElementSpec} elementSpec the spec to create the
-   *   annotating element.
+   * @param parent the node whose contents will be annotated.
+   * If parent is Text then parent.parentNode must not be null
+   * @param offset the 0-based offset from which the annotation will
+   * start.
+   * @param length of the annotated text.
+   * @param elementSpec the spec to create the
+   * annotating element.
    */
-  annotateWithElement(parent, offset, length, {tagName, attributes = {}}) {
-    let childNodes;
+  annotateWithElement(
+    parent: Node,
+    offset: number,
+    length: number,
+    elSpec: ElementSpec
+  ) {
+    const tagName = elSpec.tagName;
+    const attributes = elSpec.attributes || {};
+    let childNodes: Node[];
 
     if (parent instanceof Element) {
       childNodes = Array.from(parent.childNodes);
     } else if (parent instanceof Text) {
       childNodes = [parent];
-      parent = parent.parentNode;
+      parent = parent.parentNode!;
     } else {
       return;
     }
 
-    const nestedNodes = [];
+    const nestedNodes: Node[] = [];
     for (let node of childNodes) {
       const initialNodeLength = this.getLength(node);
       // If the current node is completely before the offset.
@@ -87,12 +90,12 @@
     }
 
     const wrapper = document.createElement(tagName);
-    const sanitizer = sanitizeDOMValue;
+    const sanitizer = getSanitizeDOMValue();
     for (const [name, value] of Object.entries(attributes)) {
       wrapper.setAttribute(
-          name, sanitizer ?
-            sanitizer(value, name, 'attribute', wrapper) :
-            value);
+        name,
+        sanitizer ? sanitizer(value, name, 'attribute', wrapper) : value
+      );
     }
     for (const inner of nestedNodes) {
       parent.replaceChild(wrapper, inner);
@@ -105,8 +108,13 @@
    * element. If the element has child elements, the range is split and
    * applied as deeply as possible.
    */
-  annotateElement(parent, offset, length, cssClass) {
-    const nodes = [].slice.apply(parent.childNodes);
+  annotateElement(
+    parent: HTMLElement,
+    offset: number,
+    length: number,
+    cssClass: string
+  ) {
+    const nodes: Array<HTMLElement | Text> = [].slice.apply(parent.childNodes);
     let nodeLength;
     let subLength;
 
@@ -141,41 +149,40 @@
 
   /**
    * Wraps node in annotation tag with cssClass, replacing the node in DOM.
-   *
-   * @return {!Element} Wrapped node.
    */
-  wrapInHighlight(node, cssClass) {
+  wrapInHighlight(node: Element | Text, cssClass: string) {
     let hl;
-    if (node.tagName === ANNOTATION_TAG) {
+    if (!(node instanceof Text) && node.tagName === ANNOTATION_TAG) {
       hl = node;
       hl.classList.add(cssClass);
     } else {
       hl = document.createElement(ANNOTATION_TAG);
       hl.className = cssClass;
-      dom(node.parentElement).replaceChild(hl, node);
-      dom(hl).appendChild(node);
+      if (node.parentElement) node.parentElement.replaceChild(hl, node);
+      hl.appendChild(node);
     }
     return hl;
   },
 
   /**
    * Splits Text Node and wraps it in hl with cssClass.
-   * Wraps trailing part after split, tailing one if opt_firstPart is true.
-   *
-   * @param {!Node} node
-   * @param {number} offset
-   * @param {string} cssClass
-   * @param {boolean=} opt_firstPart
+   * Wraps trailing part after split, tailing one if firstPart is true.
    */
-  splitAndWrapInHighlight(node, offset, cssClass, opt_firstPart) {
+  splitAndWrapInHighlight(
+    node: Text,
+    offset: number,
+    cssClass: string,
+    firstPart?: boolean
+  ) {
     if (this.getLength(node) === offset || offset === 0) {
       return this.wrapInHighlight(node, cssClass);
     } else {
-      if (opt_firstPart) {
+      if (firstPart) {
         this.splitNode(node, offset);
         // Node points to first part of the Text, second one is sibling.
       } else {
-        node = this.splitNode(node, offset);
+        // if node is Text then splitNode will return a Text
+        node = this.splitNode(node, offset) as Text;
       }
       return this.wrapInHighlight(node, cssClass);
     }
@@ -184,29 +191,28 @@
   /**
    * Splits Node at offset.
    * If Node is Element, it's cloned and the node at offset is split too.
-   *
-   * @param {!Node} node
-   * @param {number} offset
-   * @return {!Node} Trailing Node.
    */
-  splitNode(element, offset) {
+  splitNode(element: Node, offset: number) {
     if (element instanceof Text) {
       return this.splitTextNode(element, offset);
     }
     const tail = element.cloneNode(false);
-    element.parentElement.insertBefore(tail, element.nextSibling);
+
+    if (element.parentElement)
+      element.parentElement.insertBefore(tail, element.nextSibling);
     // Skip nodes before offset.
     let node = element.firstChild;
-    while (node &&
-        this.getLength(node) <= offset ||
-        this.getLength(node) === 0) {
+    while (
+      node &&
+      (this.getLength(node) <= offset || this.getLength(node) === 0)
+    ) {
       offset -= this.getLength(node);
       node = node.nextSibling;
     }
-    if (this.getLength(node) > offset) {
+    if (node && this.getLength(node) > offset) {
       tail.appendChild(this.splitNode(node, offset));
     }
-    while (node.nextSibling) {
+    while (node && node.nextSibling) {
       tail.appendChild(node.nextSibling);
     }
     return tail;
@@ -218,12 +224,10 @@
    * DOM Api for splitText() is broken for Unicode:
    * https://mathiasbynens.be/notes/javascript-unicode
    *
-   * @param {!Text} node
-   * @param {number} offset
-   * @return {!Text} Trailing Text Node.
+   * @return Trailing Text Node.
    */
-  splitTextNode(node, offset) {
-    if (node.textContent.match(REGEX_ASTRAL_SYMBOL)) {
+  splitTextNode(node: Text, offset: number) {
+    if (node.textContent && node.textContent.match(REGEX_ASTRAL_SYMBOL)) {
       // TODO (viktard): Polyfill Array.from for IE10.
       const head = Array.from(node.textContent);
       const tail = head.splice(offset);
@@ -242,7 +246,7 @@
     }
   },
 
-  _annotateText(node, offset, length, cssClass) {
+  _annotateText(node: Text, offset: number, length: number, cssClass: string) {
     const nodeLength = this.getLength(node);
 
     // There are four cases:
@@ -262,8 +266,12 @@
       this.splitAndWrapInHighlight(node, offset, cssClass, false);
     } else {
       // Case 4
-      this.splitAndWrapInHighlight(this.splitTextNode(node, offset), length,
-          cssClass, true);
+      this.splitAndWrapInHighlight(
+        this.splitTextNode(node, offset),
+        length,
+        cssClass,
+        true
+      );
     }
   },
 };
@@ -271,9 +279,8 @@
 /**
  * Data used to construct an element.
  *
- * @typedef {{
- *   tagName: string,
- *   attributes: (!Object<string, *>|undefined)
- * }}
  */
-GrAnnotation.ElementSpec;
+export interface ElementSpec {
+  tagName: string;
+  attributes?: {[attributeName: string]: string | undefined};
+}
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 de9dcc2..ffcd10c 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
@@ -14,97 +14,85 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
-import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
-import {PolymerElement} from '@polymer/polymer/polymer-element.js';
-import {htmlTemplate} from './gr-cursor-manager_html.js';
-import {ScrollMode} from '../../../constants/constants.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin';
+import {PolymerElement} from '@polymer/polymer/polymer-element';
+import {htmlTemplate} from './gr-cursor-manager_html';
+import {ScrollMode} from '../../../constants/constants';
+import {customElement, property, observe} from '@polymer/decorators';
+
+export interface GrCursorManager {
+  $: {};
+}
+
+declare global {
+  interface HTMLElementTagNameMap {
+    'gr-cursor-manager': GrCursorManager;
+  }
+}
 
 // Time in which pressing n key again after the toast navigates to next file
 const NAVIGATE_TO_NEXT_FILE_TIMEOUT_MS = 5000;
 
-/** @extends PolymerElement */
-class GrCursorManager extends GestureEventListeners(
-    LegacyElementMixin(
-        PolymerElement)) {
-  static get template() { return htmlTemplate; }
-
-  static get is() { return 'gr-cursor-manager'; }
-
-  static get properties() {
-    return {
-      stops: {
-        type: Array,
-        value() {
-          return [];
-        },
-        observer: '_updateIndex',
-      },
-      /**
-       * @type {?Object}
-       */
-      target: {
-        type: Object,
-        notify: true,
-        observer: '_scrollToTarget',
-      },
-      /**
-       * The height of content intended to be included with the target.
-       *
-       * @type {?number}
-       */
-      _targetHeight: Number,
-
-      /**
-       * The index of the current target (if any). -1 otherwise.
-       */
-      index: {
-        type: Number,
-        value: -1,
-        notify: true,
-      },
-
-      /**
-       * The class to apply to the current target. Use null for no class.
-       */
-      cursorTargetClass: {
-        type: String,
-        value: null,
-      },
-
-      /**
-       * The scroll behavior for the cursor. Values are 'never' and
-       * 'keep-visible'. 'keep-visible' will only scroll if the cursor is beyond
-       * the viewport.
-       * TODO (beckysiegel) figure out why it can be undefined
-       *
-       * @type {string|undefined}
-       */
-      scrollMode: {
-        type: String,
-        value: ScrollMode.NEVER,
-      },
-
-      /**
-       * When true, will call element.focus() during scrolling.
-       */
-      focusOnMove: {
-        type: Boolean,
-        value: false,
-      },
-
-      /**
-       * The scrollTopMargin defines height of invisible area at the top
-       * of the page. If cursor locates inside this margin - it is
-       * not visible, because it is covered by some other element.
-       */
-      scrollTopMargin: {
-        type: Number,
-        value: 0,
-      },
-    };
+@customElement('gr-cursor-manager')
+export class GrCursorManager extends GestureEventListeners(
+  LegacyElementMixin(PolymerElement)
+) {
+  static get template() {
+    return htmlTemplate;
   }
 
+  @property({type: Object, notify: true})
+  target: HTMLElement | null = null;
+
+  /**
+   * The height of content intended to be included with the target.
+   */
+  @property({type: Number})
+  _targetHeight: number | null = null;
+
+  /**
+   * The index of the current target (if any). -1 otherwise.
+   */
+  @property({type: Number, notify: true})
+  index = -1;
+
+  /**
+   * The class to apply to the current target. Use null for no class.
+   */
+  @property({type: String})
+  cursorTargetClass: string | null = null;
+
+  /**
+   * The scroll behavior for the cursor. Values are 'never' and
+   * 'keep-visible'. 'keep-visible' will only scroll if the cursor is beyond
+   * the viewport.
+   * TODO (beckysiegel) figure out why it can be undefined
+   *
+   * @type {string|undefined}
+   */
+  @property({type: String})
+  scrollMode: string = ScrollMode.NEVER;
+
+  /**
+   * When true, will call element.focus() during scrolling.
+   */
+  @property({type: Boolean})
+  focusOnMove = false;
+
+  /**
+   * The scrollTopMargin defines height of invisible area at the top
+   * of the page. If cursor locates inside this margin - it is
+   * not visible, because it is covered by some other element.
+   */
+  @property({type: Number})
+  scrollTopMargin = 0;
+
+  private _lastDisplayedNavigateToNextFileToast: number | null = null;
+
+  @property({type: Array})
+  stops: HTMLElement[] = [];
+
   /** @override */
   detached() {
     super.detached();
@@ -114,27 +102,36 @@
   /**
    * Move the cursor forward. Clipped to the ends of the stop list.
    *
-   * @param {!Function=} opt_condition Optional stop condition. If a condition
+   * @param condition Optional stop condition. If a condition
    *    is passed the cursor will continue to move in the specified direction
    *    until the condition is met.
-   * @param {!Function=} opt_getTargetHeight Optional function to calculate the
+   * @param getTargetHeight Optional function to calculate the
    *    height of the target's 'section'. The height of the target itself is
    *    sometimes different, used by the diff cursor.
-   * @param {boolean=} opt_clipToTop When none of the next indices match, move
+   * @param clipToTop When none of the next indices match, move
    *     back to first instead of to last.
-   * @param {boolean=} opt_navigateToNextFile Navigate to next unreviewed file
+   * @param navigateToNextFile Navigate to next unreviewed file
    *     if user presses next on the last diff chunk
    * @private
    */
 
-  next(opt_condition, opt_getTargetHeight, opt_clipToTop,
-      opt_navigateToNextFile) {
-    this._moveCursor(1, opt_condition, opt_getTargetHeight, opt_clipToTop,
-        opt_navigateToNextFile);
+  next(
+    condition?: Function,
+    getTargetHeight?: Function,
+    clipToTop?: boolean,
+    navigateToNextFile?: boolean
+  ) {
+    this._moveCursor(
+      1,
+      condition,
+      getTargetHeight,
+      clipToTop,
+      navigateToNextFile
+    );
   }
 
-  previous(opt_condition) {
-    this._moveCursor(-1, opt_condition);
+  previous(condition?: Function) {
+    this._moveCursor(-1, condition);
   }
 
   /**
@@ -143,21 +140,21 @@
    * 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.
+   * @param condition Optional condition. If a condition
+   * is passed only stops which meet conditions are taken into account.
    */
-  moveToVisibleArea(opt_condition) {
+  moveToVisibleArea(condition?: (el: Element) => boolean) {
     if (!this.stops || !this._isIntersectionObserverSupported()) {
       return;
     }
-    const filteredStops = opt_condition ? this.stops.filter(opt_condition)
-      : this.stops;
+    const filteredStops = condition ? this.stops.filter(condition) : this.stops;
     const dims = this._getWindowDims();
-    const windowCenter =
-        Math.round((dims.innerHeight + this.scrollTopMargin) / 2);
+    const windowCenter = Math.round(
+      (dims.innerHeight + this.scrollTopMargin) / 2
+    );
 
-    let closestToTheCenter = null;
-    let minDistanceToCenter = null;
+    let closestToTheCenter: HTMLElement | null = null;
+    let minDistanceToCenter: number | null = null;
     let unobservedCount = filteredStops.length;
 
     const observer = new IntersectionObserver(entries => {
@@ -170,21 +167,26 @@
         // In Edge it is recommended to use intersectionRatio instead of
         // isIntersecting.
         const isInsideViewport =
-            entry.isIntersecting || entry.intersectionRatio > 0;
+          entry.isIntersecting || entry.intersectionRatio > 0;
         if (!isInsideViewport) {
           return;
         }
-        const center = entry.boundingClientRect.top + Math.round(
-            entry.boundingClientRect.height / 2);
+        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;
+        if (
+          minDistanceToCenter === null ||
+          distanceToWindowCenter < minDistanceToCenter
+        ) {
+          // entry.target comes from the filteredStops array,
+          // hence it is an HTMLElement
+          closestToTheCenter = entry.target as HTMLElement;
           minDistanceToCenter = distanceToWindowCenter;
         }
       });
       unobservedCount -= entries.length;
-      if (unobservedCount == 0 && closestToTheCenter) {
+      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
@@ -209,13 +211,12 @@
   /**
    * Set the cursor to an arbitrary element.
    *
-   * @param {!HTMLElement} element
-   * @param {boolean=} opt_noScroll prevent any potential scrolling in response
-   *   setting the cursor.
+   * @param noScroll prevent any potential scrolling in response
+   * setting the cursor.
    */
-  setCursor(element, opt_noScroll) {
+  setCursor(element: HTMLElement, noScroll?: boolean) {
     let behavior;
-    if (opt_noScroll) {
+    if (noScroll) {
       behavior = this.scrollMode;
       this.scrollMode = ScrollMode.NEVER;
     }
@@ -225,7 +226,9 @@
     this._updateIndex();
     this._decorateTarget();
 
-    if (opt_noScroll) { this.scrollMode = behavior; }
+    if (noScroll && behavior) {
+      this.scrollMode = behavior;
+    }
   }
 
   unsetCursor() {
@@ -255,29 +258,34 @@
     }
   }
 
-  setCursorAtIndex(index, opt_noScroll) {
-    this.setCursor(this.stops[index], opt_noScroll);
+  setCursorAtIndex(index: number, noScroll?: boolean) {
+    this.setCursor(this.stops[index], noScroll);
   }
 
   /**
    * Move the cursor forward or backward by delta. Clipped to the beginning or
    * end of stop list.
    *
-   * @param {number} delta either -1 or 1.
-   * @param {!Function=} opt_condition Optional stop condition. If a condition
-   *    is passed the cursor will continue to move in the specified direction
-   *    until the condition is met.
-   * @param {!Function=} opt_getTargetHeight Optional function to calculate the
-   *    height of the target's 'section'. The height of the target itself is
-   *    sometimes different, used by the diff cursor.
-   * @param {boolean=} opt_clipToTop When none of the next indices match, move
-   *     back to first instead of to last.
-   * @param {boolean=} opt_navigateToNextFile Navigate to next unreviewed file
-   *     if user presses next on the last diff chunk
+   * @param delta either -1 or 1.
+   * @param condition Optional stop condition. If a condition
+   * is passed the cursor will continue to move in the specified direction
+   * until the condition is met.
+   * @param getTargetHeight Optional function to calculate the
+   * height of the target's 'section'. The height of the target itself is
+   * sometimes different, used by the diff cursor.
+   * @param clipToTop When none of the next indices match, move
+   * back to first instead of to last.
+   * @param navigateToNextFile Navigate to next unreviewed file
+   * if user presses next on the last diff chunk
    * @private
    */
-  _moveCursor(delta, opt_condition, opt_getTargetHeight, opt_clipToTop,
-      opt_navigateToNextFile) {
+  _moveCursor(
+    delta: number,
+    condition?: Function,
+    getTargetHeight?: Function,
+    clipToTop?: boolean,
+    navigateToNextFile?: boolean
+  ) {
     if (!this.stops.length) {
       this.unsetCursor();
       return;
@@ -285,7 +293,7 @@
 
     this._unDecorateTarget();
 
-    const newIndex = this._getNextindex(delta, opt_condition, opt_clipToTop);
+    const newIndex = this._getNextindex(delta, condition, clipToTop);
 
     let newTarget = null;
     if (newIndex !== -1) {
@@ -297,42 +305,53 @@
      * that pressing n again will navigate them to next unreviewed file.
      * If click happens within the time limit, then navigate to next file
      */
-    if (opt_navigateToNextFile && this.index === newIndex) {
+    if (navigateToNextFile && this.index === newIndex) {
       if (newIndex === this.stops.length - 1) {
-        if (this._lastDisplayedNavigateToNextFileToast && (Date.now() -
-          this._lastDisplayedNavigateToNextFileToast <=
-            NAVIGATE_TO_NEXT_FILE_TIMEOUT_MS)) {
+        if (
+          this._lastDisplayedNavigateToNextFileToast &&
+          Date.now() - this._lastDisplayedNavigateToNextFileToast <=
+            NAVIGATE_TO_NEXT_FILE_TIMEOUT_MS
+        ) {
           // reset for next file
           this._lastDisplayedNavigateToNextFileToast = null;
-          this.dispatchEvent(new CustomEvent(
-              'navigate-to-next-unreviewed-file', {
-                composed: true, bubbles: true,
-              }));
+          this.dispatchEvent(
+            new CustomEvent('navigate-to-next-unreviewed-file', {
+              composed: true,
+              bubbles: true,
+            })
+          );
           return;
         }
         this._lastDisplayedNavigateToNextFileToast = Date.now();
-        this.dispatchEvent(new CustomEvent('show-alert', {
-          detail: {
-            message: 'Press n again to navigate to next unreviewed file',
-          },
-          composed: true, bubbles: true,
-        }));
+        this.dispatchEvent(
+          new CustomEvent('show-alert', {
+            detail: {
+              message: 'Press n again to navigate to next unreviewed file',
+            },
+            composed: true,
+            bubbles: true,
+          })
+        );
         return;
       }
     }
 
     this.index = newIndex;
-    this.target = newTarget;
+    this.target = newTarget as HTMLElement;
 
-    if (!this.target) { return; }
+    if (!newTarget) {
+      return;
+    }
 
-    if (opt_getTargetHeight) {
-      this._targetHeight = opt_getTargetHeight(newTarget);
+    if (getTargetHeight) {
+      this._targetHeight = getTargetHeight(newTarget);
     } else {
       this._targetHeight = newTarget.scrollHeight;
     }
 
-    if (this.focusOnMove) { this.target.focus(); }
+    if (this.focusOnMove) {
+      newTarget.focus();
+    }
 
     this._decorateTarget();
   }
@@ -352,14 +371,14 @@
   /**
    * Get the next stop index indicated by the delta direction.
    *
-   * @param {number} delta either -1 or 1.
-   * @param {!Function=} opt_condition Optional stop condition.
-   * @param {boolean=} opt_clipToTop When none of the next indices match, move
-   *     back to first instead of to last.
-   * @return {number} the new index.
+   * @param delta either -1 or 1.
+   * @param condition Optional stop condition.
+   * @param clipToTop When none of the next indices match, move
+   * back to first instead of to last.
+   * @return the new index.
    * @private
    */
-  _getNextindex(delta, opt_condition, opt_clipToTop) {
+  _getNextindex(delta: number, condition?: Function, clipToTop?: boolean) {
     if (!this.stops.length) {
       return -1;
     }
@@ -371,15 +390,18 @@
     }
     do {
       newIndex = newIndex + delta;
-    } while ((delta > 0 || newIndex > 0) &&
-             (delta < 0 || newIndex < this.stops.length - 1) &&
-             opt_condition && !opt_condition(this.stops[newIndex]));
+    } while (
+      (delta > 0 || newIndex > 0) &&
+      (delta < 0 || newIndex < this.stops.length - 1) &&
+      condition &&
+      !condition(this.stops[newIndex])
+    );
 
     newIndex = Math.max(0, Math.min(this.stops.length - 1, newIndex));
 
     // If we failed to satisfy the condition:
-    if (opt_condition && !opt_condition(this.stops[newIndex])) {
-      if (delta < 0 || opt_clipToTop) {
+    if (condition && !condition(this.stops[newIndex])) {
+      if (delta < 0 || clipToTop) {
         return 0;
       } else if (delta > 0) {
         return this.stops.length - 1;
@@ -390,6 +412,7 @@
     return newIndex;
   }
 
+  @observe('stops')
   _updateIndex() {
     if (!this.target) {
       this.index = -1;
@@ -407,35 +430,44 @@
   /**
    * Calculate where the element is relative to the window.
    *
-   * @param {!Object} target Target to scroll to.
-   * @return {number} Distance to top of the target.
+   * @param target Target to scroll to.
+   * @return Distance to top of the target.
    */
-  _getTop(target) {
-    let top = target.offsetTop;
-    for (let offsetParent = target.offsetParent;
+  _getTop(target: HTMLElement) {
+    let top: number = target.offsetTop;
+    for (
+      let offsetParent = target.offsetParent;
       offsetParent;
-      offsetParent = offsetParent.offsetParent) {
-      top += offsetParent.offsetTop;
+      offsetParent = (offsetParent as HTMLElement).offsetParent
+    ) {
+      top += (offsetParent as HTMLElement).offsetTop;
     }
     return top;
   }
 
   /**
-   * @return {boolean}
+   * @return
    */
-  _targetIsVisible(top) {
+  _targetIsVisible(top: number) {
     const dims = this._getWindowDims();
-    return this.scrollMode === ScrollMode.KEEP_VISIBLE &&
-        top > (dims.pageYOffset + this.scrollTopMargin) &&
-        top < dims.pageYOffset + dims.innerHeight;
+    return (
+      this.scrollMode === ScrollMode.KEEP_VISIBLE &&
+      top > dims.pageYOffset + this.scrollTopMargin &&
+      top < dims.pageYOffset + dims.innerHeight
+    );
   }
 
-  _calculateScrollToValue(top, target) {
+  _calculateScrollToValue(top: number, target: HTMLElement) {
     const dims = this._getWindowDims();
-    return top + this.scrollTopMargin - (dims.innerHeight / 3) +
-        (target.offsetHeight / 2);
+    return (
+      top +
+      this.scrollTopMargin -
+      dims.innerHeight / 3 +
+      target.offsetHeight / 2
+    );
   }
 
+  @observe('target')
   _scrollToTarget() {
     if (!this.target || this.scrollMode === ScrollMode.NEVER) {
       return;
@@ -443,8 +475,9 @@
 
     const dims = this._getWindowDims();
     const top = this._getTop(this.target);
-    const bottomIsVisible = this._targetHeight ?
-      this._targetIsVisible(top + this._targetHeight) : true;
+    const bottomIsVisible = this._targetHeight
+      ? this._targetIsVisible(top + this._targetHeight)
+      : true;
     const scrollToValue = this._calculateScrollToValue(top, this.target);
 
     if (this._targetIsVisible(top)) {
@@ -473,5 +506,3 @@
     };
   }
 }
-
-customElements.define(GrCursorManager.is, GrCursorManager);
diff --git a/polygerrit-ui/app/scripts/util.ts b/polygerrit-ui/app/scripts/util.ts
index e4be858..6e4464f 100644
--- a/polygerrit-ui/app/scripts/util.ts
+++ b/polygerrit-ui/app/scripts/util.ts
@@ -15,10 +15,14 @@
  * limitations under the License.
  */
 
+interface CancelablePromise<T> extends Promise<T> {
+  cancel(): void;
+}
+
 // TODO (dmfilippov): Each function must be exported separately. According to
 // the code style guide, a namespacing is not allowed.
 export const util = {
-  getCookie(name) {
+  getCookie(name: string) {
     const key = name + '=';
     const cookies = document.cookie.split(';');
     for (let i = 0; i < cookies.length; i++) {
@@ -41,22 +45,28 @@
    * {isCancelled: true} synchronously. If the inner promise for a cancelled
    * promise resolves or rejects this is ignored.
    */
-  makeCancelable: promise => {
+  makeCancelable<T>(promise: Promise<T>) {
     // True if the promise is either resolved or reject (possibly cancelled)
     let isDone = false;
 
-    let rejectPromise;
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    let rejectPromise: (reason?: any) => void;
 
-    const wrappedPromise = new Promise((resolve, reject) => {
-      rejectPromise = reject;
-      promise.then(val => {
-        if (!isDone) resolve(val);
-        isDone = true;
-      }, error => {
-        if (!isDone) reject(error);
-        isDone = true;
-      });
-    });
+    const wrappedPromise: CancelablePromise<T> = new Promise(
+      (resolve, reject) => {
+        rejectPromise = reject;
+        promise.then(
+          val => {
+            if (!isDone) resolve(val);
+            isDone = true;
+          },
+          error => {
+            if (!isDone) reject(error);
+            isDone = true;
+          }
+        );
+      }
+    ) as CancelablePromise<T>;
 
     wrappedPromise.cancel = () => {
       if (isDone) return;