Merge "Show hovercard with a delay of 500 ms"
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.js b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.js
index a77f5f7..1ba3c23 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.js
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.js
@@ -30,6 +30,12 @@
 const DIAGONAL_OVERFLOW = 15;
 
 /**
+ * How long should be wait before showing the hovercard when the user hovers
+ * over the element?
+ */
+const SHOW_DELAY_MS = 500;
+
+/**
  * The mixin for gr-hovercard-behavior.
  *
  * @example
@@ -118,8 +124,8 @@
   attached() {
     super.attached();
     if (!this._target) { this._target = this.target; }
-    this.listen(this._target, 'mouseenter', 'show');
-    this.listen(this._target, 'focus', 'show');
+    this.listen(this._target, 'mouseenter', 'showDelayed');
+    this.listen(this._target, 'focus', 'showDelayed');
     this.listen(this._target, 'mouseleave', 'hide');
     this.listen(this._target, 'blur', 'hide');
     this.listen(this._target, 'click', 'hide');
@@ -184,6 +190,10 @@
    * @param {Event} e DOM Event (e.g. `mouseleave` event)
    */
   hide(e) {
+    this._isScheduledToShow = false;
+    if (!this._isShowing) {
+      return;
+    }
     const targetRect = this._target.getBoundingClientRect();
     const x = e.clientX;
     const y = e.clientY;
@@ -195,10 +205,10 @@
       return;
     }
 
-    // If the hovercard is already hidden or the user is now hovering over the
-    //  hovercard or the user is returning from the hovercard but now hovering
-    //  over the target (to stop an annoying flicker effect), just return.
-    if (!this._isShowing || e.toElement === this ||
+    // If the user is now hovering over the hovercard or the user is returning
+    // from the hovercard but now hovering over the target (to stop an annoying
+    // flicker effect), just return.
+    if (e.toElement === this ||
         (e.fromElement === this && e.toElement === this._target)) {
       return;
     }
@@ -221,12 +231,31 @@
   }
 
   /**
+   * Shows/opens the hovercard with a fixed delay.
+   */
+  showDelayed() {
+    this.showDelayedBy(SHOW_DELAY_MS);
+  }
+
+  /**
+   * Shows/opens the hovercard with the given delay.
+   */
+  showDelayedBy(delayMs) {
+    if (this._isShowing || this._isScheduledToShow) return;
+    this._isScheduledToShow = true;
+    setTimeout(() => {
+      // This happens when the mouse leaves the target before the delay is over.
+      if (!this._isScheduledToShow) return;
+      this._isScheduledToShow = false;
+      this.show();
+    }, delayMs);
+  }
+
+  /**
    * Shows/opens the hovercard. This occurs when the user triggers the
    * `mousenter` event on the hovercard's `target` element.
-   *
-   * @param {Event} e DOM Event (e.g., `mouseenter` event)
    */
-  show(e) {
+  show() {
     if (this._isShowing) {
       return;
     }
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.html b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.html
index 1ffed0a..99791e7 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.html
@@ -109,14 +109,31 @@
     }, TRANSITION_TIME);
   });
 
-  test('card shows on enter and hides on leave', done => {
+  test('showDelayed does not show immediately', done => {
+    element.showDelayedBy(100);
+    setTimeout(() => {
+      assert.isFalse(element._isShowing);
+      done();
+    }, 0);
+  });
+
+  test('showDelayed shows after delay', done => {
+    element.showDelayedBy(1);
+    setTimeout(() => {
+      assert.isTrue(element._isShowing);
+      done();
+    }, 10);
+  });
+
+  test('card is scheduled to show on enter and hides on leave', done => {
     const button = dom(document).querySelector('button');
     assert.isFalse(element._isShowing);
     button.addEventListener('mouseenter', event => {
-      assert.isTrue(element._isShowing);
+      assert.isTrue(element._isScheduledToShow);
       button.dispatchEvent(new CustomEvent('mouseleave'));
     });
     button.addEventListener('mouseleave', event => {
+      assert.isFalse(element._isScheduledToShow);
       assert.isFalse(element._isShowing);
       done();
     });