Ignore mouseup shortly after showing cr.ui.Menu

This works simlarly to views::MenuController[1] by ignoring mouseups:
- less than 200ms after showing some UI
- with minimal mouse movement (4px length difference)

R=mustaq@chromium.org
BUG=531939

[1] https://goo.gl/iZht5k

Review URL: https://codereview.chromium.org/1358893003

Cr-Commit-Position: refs/heads/master@{#350323}
diff --git a/chrome/test/data/webui/menu_test.html b/chrome/test/data/webui/menu_test.html
index 7e0e920..6d26cc3 100644
--- a/chrome/test/data/webui/menu_test.html
+++ b/chrome/test/data/webui/menu_test.html
@@ -3,9 +3,30 @@
 <body>
 <script>
 
-function testHandleMouseOver() {
-  var menu = new cr.ui.Menu;
+/** @type {cr.ui.Menu} */
+var menu;
 
+/**
+ * @param {number} x The screenX coord of the mouseup event.
+ * @param {number} y The screenY coord of the mouseup event.
+ */
+function mouseUpAt(x, y) {
+  var mouseUpEvent = new MouseEvent('mouseup', {
+    bubbles: true,
+    cancelable: true,
+    target: menu,
+    screenX: x,
+    screenY: y,
+  });
+  mouseUpEvent.isTrustedForTesting = true;
+  return menu.dispatchEvent(mouseUpEvent);
+}
+
+function setUp() {
+  menu = new cr.ui.Menu;
+}
+
+function testHandleMouseOver() {
   var called = false;
   menu.findMenuItem_ = function() {
     called = true;
@@ -18,6 +39,30 @@
   assertTrue(called);
 }
 
+function testHandleMouseUp() {
+  var realNow = Date.now;
+  Date.now = function() { return 10; };
+
+  menu.show({x: 5, y: 5});
+
+  // Stop mouseups at the same time and position.
+  assertFalse(mouseUpAt(5, 5));
+
+  // Allow mouseups with different positions but the same time.
+  assertTrue(mouseUpAt(50, 50));
+
+  // Alow mouseups with the same position but different times.
+  Date.now = function() { return 1000; };
+  assertTrue(mouseUpAt(5, 5));
+
+  Date.now = realNow;
+}
+
+function testShowViaKeyboardIgnoresMouseUps() {
+  menu.show();
+  assertTrue(mouseUpAt(0, 0));
+}
+
 </script>
 </body>
 </html>
diff --git a/ui/webui/resources/js/cr/ui/context_menu_handler.js b/ui/webui/resources/js/cr/ui/context_menu_handler.js
index 1ff7b6e..10cb77c 100644
--- a/ui/webui/resources/js/cr/ui/context_menu_handler.js
+++ b/ui/webui/resources/js/cr/ui/context_menu_handler.js
@@ -43,7 +43,7 @@
 
       this.menu_ = menu;
       menu.classList.remove('hide-delayed');
-      menu.hidden = false;
+      menu.show({x: e.screenX, y: e.screenY});
       menu.contextElement = e.currentTarget;
 
       // When the menu is shown we steal a lot of events.
@@ -80,7 +80,7 @@
         menu.classList.add('hide-delayed');
       else
         menu.classList.remove('hide-delayed');
-      menu.hidden = true;
+      menu.hide();
       var originalContextElement = menu.contextElement;
       menu.contextElement = null;
       this.showingEvents_.removeAll();
diff --git a/ui/webui/resources/js/cr/ui/menu.js b/ui/webui/resources/js/cr/ui/menu.js
index d54f5430..abcb79c 100644
--- a/ui/webui/resources/js/cr/ui/menu.js
+++ b/ui/webui/resources/js/cr/ui/menu.js
@@ -32,6 +32,7 @@
     decorate: function() {
       this.addEventListener('mouseover', this.handleMouseOver_);
       this.addEventListener('mouseout', this.handleMouseOut_);
+      this.addEventListener('mouseup', this.handleMouseUp_, true);
 
       this.classList.add('decorated');
       this.setAttribute('role', 'menu');
@@ -113,6 +114,37 @@
       this.selectedItem = null;
     },
 
+    /**
+     * If there's a mouseup that happens quickly in about the same position,
+     * stop it from propagating to items. This is to prevent accidentally
+     * selecting a menu item that's created under the mouse cursor.
+     * @param {Event} e A mouseup event on the menu (in capturing phase).
+     * @private
+     */
+    handleMouseUp_: function(e) {
+      assert(this.contains(/** @type {Element} */(e.target)));
+
+      if (!this.trustEvent_(e) || Date.now() - this.shown_.time > 200)
+        return;
+
+      var pos = this.shown_.mouseDownPos;
+      if (!pos || Math.abs(pos.x - e.screenX) + Math.abs(pos.y - e.screenY) > 4)
+        return;
+
+      e.preventDefault();
+      e.stopPropagation();
+    },
+
+    /**
+     * @param {!Event} e
+     * @return {boolean} Whether |e| can be trusted.
+     * @private
+     * @suppress {checkTypes}
+     */
+    trustEvent_: function(e) {
+      return e.isTrusted || e.isTrustedForTesting;
+    },
+
     get menuItems() {
       return this.querySelectorAll(this.menuItemSelector || '*');
     },
@@ -238,6 +270,17 @@
       return false;
     },
 
+    hide: function() {
+      this.hidden = true;
+      delete this.shown_;
+    },
+
+    /** @param {{x: number, y: number}=} opt_mouseDownPos */
+    show: function(opt_mouseDownPos) {
+      this.shown_ = {mouseDownPos: opt_mouseDownPos, time: Date.now()};
+      this.hidden = false;
+    },
+
     /**
      * Updates menu items command according to context.
      * @param {Node=} node Node for which to actuate commands state.
diff --git a/ui/webui/resources/js/cr/ui/menu_button.js b/ui/webui/resources/js/cr/ui/menu_button.js
index f499f5c..3a7bae5 100644
--- a/ui/webui/resources/js/cr/ui/menu_button.js
+++ b/ui/webui/resources/js/cr/ui/menu_button.js
@@ -110,7 +110,7 @@
               this.hideMenu();
             } else if (e.button == 0) {  // Only show the menu when using left
                                          // mouse button.
-              this.showMenu(false);
+              this.showMenu(false, {x: e.screenX, y: e.screenY});
 
               // Prevent the button from stealing focus on mousedown.
               e.preventDefault();
@@ -158,7 +158,7 @@
         case 'contextmenu':
           if ((!this.menu || !this.menu.contains(e.target)) &&
               (!this.hideTimestamp_ || Date.now() - this.hideTimestamp_ > 50))
-            this.showMenu(true);
+            this.showMenu(true, {x: e.screenX, y: e.screenY});
           e.preventDefault();
           // Don't allow elements further up in the DOM to show their menus.
           e.stopPropagation();
@@ -175,8 +175,10 @@
      * Shows the menu.
      * @param {boolean} shouldSetFocus Whether to set focus on the
      *     selected menu item.
+     * @param {{x: number, y: number}=} opt_mousePos The position of the mouse
+     *     when shown (in screen coordinates).
      */
-    showMenu: function(shouldSetFocus) {
+    showMenu: function(shouldSetFocus, opt_mousePos) {
       this.hideMenu();
 
       this.menu.updateCommands(this);
@@ -189,7 +191,7 @@
       if (!this.dispatchEvent(event))
         return;
 
-      this.menu.hidden = false;
+      this.menu.show(opt_mousePos);
 
       this.setAttribute('menu-shown', '');
 
@@ -225,7 +227,7 @@
         this.menu.classList.add('hide-delayed');
       else
         this.menu.classList.remove('hide-delayed');
-      this.menu.hidden = true;
+      this.menu.hide();
 
       this.showingEvents_.removeAll();
       this.focus();