Bug 1388104 - Open the textbox context menu on any html input in the toolbox;r=gl draft
authorBrian Grinstead <bgrinstead@mozilla.com>
Tue, 15 Aug 2017 15:39:32 -0700
changeset 646985 d1febb54888a8a82f93d54c78ddbdd40281f91ee
parent 646957 d25db0546c92afa72d61685c9310104ef28235bd
child 646986 3be3578f73005c51a876e6c8c580184245d3e8bb
push id74274
push userbgrinstead@mozilla.com
push dateTue, 15 Aug 2017 22:40:49 +0000
reviewersgl
bugs1388104
milestone57.0a1
Bug 1388104 - Open the textbox context menu on any html input in the toolbox;r=gl Right now HTML inputs do not get a context menu when being right clicked. The inspector works around this by calling toolbox.openTextBoxContextMenu on each individual input, but the rest of the toolbox can be served by handling this at the toolbox level. MozReview-Commit-ID: KuRQmuf01xh
devtools/client/framework/test/browser_toolbox_textbox_context_menu.js
devtools/client/framework/test/head.js
devtools/client/framework/toolbox.js
--- a/devtools/client/framework/test/browser_toolbox_textbox_context_menu.js
+++ b/devtools/client/framework/test/browser_toolbox_textbox_context_menu.js
@@ -1,51 +1,91 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
+// HTML inputs don't automatically get the 'edit' context menu, so we have
+// a helper on the toolbox to do so. Make sure that shows menu items in the
+// right state, and that it works for an input inside of a panel.
+
 const URL = "data:text/html;charset=utf8,test for textbox context menu";
+const textboxToolId = "test-tool-1";
 
-add_task(function* () {
-  let toolbox = yield openNewTabAndToolbox(URL, "inspector");
+registerCleanupFunction(() => {
+  gDevTools.unregisterTool(textboxToolId);
+});
+
+add_task(async function checkMenuEntryStates() {
+  info("Checking the state of edit menuitems with an empty clipboard");
+  let toolbox = await openNewTabAndToolbox(URL, "inspector");
   let textboxContextMenu = toolbox.textBoxContextMenuPopup;
 
   emptyClipboard();
 
   // Make sure the focus is predictable.
   let inspector = toolbox.getPanel("inspector");
   let onFocus = once(inspector.searchBox, "focus");
   inspector.searchBox.focus();
-  yield onFocus;
+  await onFocus;
 
   ok(textboxContextMenu, "The textbox context menu is loaded in the toolbox");
 
   let cmdUndo = textboxContextMenu.querySelector("[command=cmd_undo]");
   let cmdDelete = textboxContextMenu.querySelector("[command=cmd_delete]");
   let cmdSelectAll = textboxContextMenu.querySelector("[command=cmd_selectAll]");
   let cmdCut = textboxContextMenu.querySelector("[command=cmd_cut]");
   let cmdCopy = textboxContextMenu.querySelector("[command=cmd_copy]");
   let cmdPaste = textboxContextMenu.querySelector("[command=cmd_paste]");
 
   info("Opening context menu");
 
   let onContextMenuPopup = once(textboxContextMenu, "popupshowing");
   textboxContextMenu.openPopupAtScreen(0, 0, true);
-  yield onContextMenuPopup;
+  await onContextMenuPopup;
 
   is(cmdUndo.getAttribute("disabled"), "true", "cmdUndo is disabled");
   is(cmdDelete.getAttribute("disabled"), "true", "cmdDelete is disabled");
   is(cmdSelectAll.getAttribute("disabled"), "true", "cmdSelectAll is disabled");
 
   // Cut/Copy/Paste items are enabled in context menu even if there
   // is no selection. See also Bug 1303033, and 1317322
   is(cmdCut.getAttribute("disabled"), "", "cmdCut is enabled");
   is(cmdCopy.getAttribute("disabled"), "", "cmdCopy is enabled");
   is(cmdPaste.getAttribute("disabled"), "", "cmdPaste is enabled");
-
-  yield cleanup(toolbox);
 });
 
-function* cleanup(toolbox) {
-  yield toolbox.destroy();
-  gBrowser.removeCurrentTab();
+add_task(async function automaticallyBindTexbox() {
+  info("Registering a tool with an input field and making sure the context menu works");
+  gDevTools.registerTool({
+    id: textboxToolId,
+    isTargetSupported: () => true,
+    url: "data:text/html;charset=utf8,<input />",
+    label: "Context menu works without tool intervention",
+    build: function (iframeWindow, toolbox) {
+      this.panel = createTestPanel(iframeWindow, toolbox);
+      return this.panel.open();
+    },
+  });
+
+  let toolbox = await openNewTabAndToolbox(URL, textboxToolId);
+  is(toolbox.currentToolId, textboxToolId, "The custom tool has been opened");
+  await checkTextBox(toolbox.getCurrentPanel().document.querySelector("input"), toolbox);
+});
+
+async function checkTextBox(textBox, {textBoxContextMenuPopup}) {
+  is(textBoxContextMenuPopup.state, "closed", "The menu is closed");
+
+  info("Simulating context click on the textbox and expecting the menu to open");
+  let onContextMenu = once(textBoxContextMenuPopup, "popupshown");
+  EventUtils.synthesizeMouse(textBox, 2, 2, {type: "contextmenu", button: 2},
+                             textBox.ownerDocument.defaultView);
+  await onContextMenu;
+
+  is(textBoxContextMenuPopup.state, "open", "The menu is now visible");
+
+  info("Closing the menu");
+  let onContextMenuHidden = once(textBoxContextMenuPopup, "popuphidden");
+  textBoxContextMenuPopup.hidePopup();
+  await onContextMenuHidden;
+
+  is(textBoxContextMenuPopup.state, "closed", "The menu is closed again");
 }
--- a/devtools/client/framework/test/head.js
+++ b/devtools/client/framework/test/head.js
@@ -198,31 +198,36 @@ function waitForSourceLoad(toolbox, url)
 *
 * There may be no benefit in doing this as an abstract type, but if nothing
 * else gives us a place to write documentation.
 */
 function DevToolPanel(iframeWindow, toolbox) {
   EventEmitter.decorate(this);
 
   this._toolbox = toolbox;
+  this._window = iframeWindow;
 }
 
 DevToolPanel.prototype = {
   open: function () {
     let deferred = defer();
 
     executeSoon(() => {
       this._isReady = true;
       this.emit("ready");
       deferred.resolve(this);
     });
 
     return deferred.promise;
   },
 
+  get document() {
+    return this._window.document;
+  },
+
   get target() {
     return this._toolbox.target;
   },
 
   get toolbox() {
     return this._toolbox;
   },
 
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -437,16 +437,23 @@ Toolbox.prototype = {
       Services.prefs.addObserver("devtools.cache.disabled", this._applyCacheSettings);
       Services.prefs.addObserver("devtools.serviceWorkers.testing.enabled",
                                  this._applyServiceWorkersTestingSettings);
 
       this.textBoxContextMenuPopup =
         this.doc.getElementById("toolbox-textbox-context-popup");
       this.textBoxContextMenuPopup.addEventListener("popupshowing",
         this._updateTextBoxMenuItems, true);
+      this.doc.addEventListener("contextmenu", (e) => {
+        if (e.originalTarget.closest("input") || e.originalTarget.closest("textarea")) {
+          e.stopPropagation();
+          e.preventDefault();
+          this.openTextBoxContextMenu(e.screenX, e.screenY);
+        }
+      }, true);
 
       this.shortcuts = new KeyShortcuts({
         window: this.doc.defaultView
       });
       // Get the DOM element to mount the ToolboxController to.
       this._componentMount = this.doc.getElementById("toolbox-toolbar-mount");
 
       this._mountReactComponent();