Bug 1504254 - Edit prefs on double click r=paolo
authorMark Striemer <mstriemer@mozilla.com>
Mon, 30 Sep 2019 16:06:39 +0000
changeset 495693 b9549e85c43e932a1953eee333c6d0a631eb0d2a
parent 495692 5f77e91dd95e12a0118390fe8b6f5d48f4b505c5
child 495694 862aa467880275ea736416134c5252d1e45e0145
push id114140
push userdvarga@mozilla.com
push dateWed, 02 Oct 2019 18:04:51 +0000
treeherdermozilla-inbound@32eb0ea893f3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspaolo
bugs1504254
milestone71.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1504254 - Edit prefs on double click r=paolo Differential Revision: https://phabricator.services.mozilla.com/D44916
browser/components/aboutconfig/content/aboutconfig.js
browser/components/aboutconfig/test/browser/browser_clipboard.js
browser/components/aboutconfig/test/browser/browser_edit.js
--- a/browser/components/aboutconfig/content/aboutconfig.js
+++ b/browser/components/aboutconfig/content/aboutconfig.js
@@ -351,16 +351,28 @@ class PrefRow {
     gPrefInEdit = this;
     this.editing = true;
     this.refreshElement();
     // The type=number input isn't selected unless it's focused first.
     this.inputField.focus();
     this.inputField.select();
   }
 
+  toggle() {
+    Services.prefs.setBoolPref(this.name, !this.value);
+  }
+
+  editOrToggle() {
+    if (this.type == "Boolean") {
+      this.toggle();
+    } else {
+      this.edit();
+    }
+  }
+
   save() {
     if (this.type == "Number") {
       if (!this.inputField.reportValidity()) {
         return;
       }
       Services.prefs.setIntPref(this.name, parseInt(this.inputField.value));
     } else {
       Services.prefs.setStringPref(this.name, this.inputField.value);
@@ -467,31 +479,57 @@ function loadPrefs() {
 
   showAll.addEventListener("click", event => {
     search.focus();
     search.value = "";
     gFilterPrefsTask.disarm();
     filterPrefs({ showAll: true });
   });
 
+  function shouldBeginEdit(event) {
+    if (
+      event.target.localName != "button" &&
+      event.target.localName != "input"
+    ) {
+      let row = event.target.closest("tr");
+      return row && row._pref.exists;
+    }
+    return false;
+  }
+
+  // Disable double/triple-click text selection since that triggers edit/toggle.
+  prefs.addEventListener("mousedown", event => {
+    if (event.detail > 1 && shouldBeginEdit(event)) {
+      event.preventDefault();
+    }
+  });
+
   prefs.addEventListener("click", event => {
+    if (event.detail == 2 && shouldBeginEdit(event)) {
+      event.target.closest("tr")._pref.editOrToggle();
+      return;
+    }
+
     if (event.target.localName != "button") {
       return;
     }
+
     let pref = event.target.closest("tr")._pref;
     let button = event.target.closest("button");
+
     if (button.classList.contains("button-add")) {
       Preferences.set(pref.name, pref.value);
       if (pref.type != "Boolean") {
         pref.edit();
       }
-    } else if (button.classList.contains("button-toggle")) {
-      Services.prefs.setBoolPref(pref.name, !pref.value);
-    } else if (button.classList.contains("button-edit")) {
-      pref.edit();
+    } else if (
+      button.classList.contains("button-toggle") ||
+      button.classList.contains("button-edit")
+    ) {
+      pref.editOrToggle();
     } else if (button.classList.contains("button-save")) {
       pref.save();
     } else {
       // This is "button-reset" or "button-delete".
       pref.editing = false;
       Services.prefs.clearUserPref(pref.name);
       pref.editButton.focus();
     }
--- a/browser/components/aboutconfig/test/browser/browser_clipboard.js
+++ b/browser/components/aboutconfig/test/browser/browser_clipboard.js
@@ -26,50 +26,71 @@ add_task(async function test_copy() {
       ["test.aboutconfig.copy.spaces.2", "  "],
       ["test.aboutconfig.copy.spaces.3", "   "],
       ["test.aboutconfig.copy.string", "010.5"],
     ]) {
       // Limit the number of preferences shown so all the rows are visible.
       this.search(name);
       let row = this.getRow(name);
 
-      // Triple click at any location in the name cell should select the name.
-      await BrowserTestUtils.synthesizeMouseAtCenter(
-        row.nameCell,
-        { clickCount: 3 },
-        this.browser
-      );
+      let selectText = async target => {
+        let { width, height } = target.getBoundingClientRect();
+        await BrowserTestUtils.synthesizeMouse(
+          target,
+          1,
+          1,
+          { type: "mousedown" },
+          this.browser
+        );
+        await BrowserTestUtils.synthesizeMouse(
+          target,
+          width - 1,
+          height - 1,
+          { type: "mousemove" },
+          this.browser
+        );
+        await BrowserTestUtils.synthesizeMouse(
+          target,
+          width - 1,
+          height - 1,
+          { type: "mouseup" },
+          this.browser
+        );
+      };
+
+      // Drag across the name cell.
+      await selectText(row.nameCell);
       Assert.ok(row.nameCell.contains(this.window.getSelection().anchorNode));
       await SimpleTest.promiseClipboardChange(name, async () => {
         await BrowserTestUtils.synthesizeKey(
           "c",
           { accelKey: true },
           this.browser
         );
       });
 
-      // Triple click at any location in the value cell should select the value.
-      await BrowserTestUtils.synthesizeMouseAtCenter(
-        row.valueCell,
-        { clickCount: 3 },
-        this.browser
-      );
+      // Drag across the value cell.
+      await selectText(row.valueCell);
       let selection = this.window.getSelection();
       Assert.ok(row.valueCell.contains(selection.anchorNode));
 
-      // The selection is never collapsed because of the <span> element, and
-      // this makes sure that an empty string can be copied.
-      Assert.ok(!selection.isCollapsed);
-      await SimpleTest.promiseClipboardChange(expectedString, async () => {
-        await BrowserTestUtils.synthesizeKey(
-          "c",
-          { accelKey: true },
-          this.browser
-        );
-      });
+      if (expectedString !== "") {
+        // Non-empty values should have a selection.
+        Assert.ok(!selection.isCollapsed);
+        await SimpleTest.promiseClipboardChange(expectedString, async () => {
+          await BrowserTestUtils.synthesizeKey(
+            "c",
+            { accelKey: true },
+            this.browser
+          );
+        });
+      } else {
+        // Nothing is selected for an empty value.
+        Assert.equal(selection.toString(), "");
+      }
     }
   });
 });
 
 add_task(async function test_copy_multiple() {
   await AboutConfigTest.withNewTab(async function() {
     // Lines are separated by a single LF character on all platforms.
     let expectedString =
--- a/browser/components/aboutconfig/test/browser/browser_edit.js
+++ b/browser/components/aboutconfig/test/browser/browser_edit.js
@@ -1,11 +1,12 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
+const PREF_MODIFY_PREFIX = "test.aboutconfig.modify";
 const PREF_MODIFY_BOOLEAN = "test.aboutconfig.modify.boolean";
 const PREF_MODIFY_NUMBER = "test.aboutconfig.modify.number";
 const PREF_MODIFY_STRING = "test.aboutconfig.modify.string";
 
 add_task(async function setup() {
   await SpecialPowers.pushPrefEnv({
     set: [
       [PREF_MODIFY_BOOLEAN, true],
@@ -296,8 +297,114 @@ add_task(async function test_escape_canc
       EventUtils.synthesizeKey("KEY_Escape", {}, this.window);
 
       Assert.ok(!row.valueInput);
       Assert.equal(row.value, "Edit me, maybe");
       Assert.equal(row.value, Preferences.get(PREF_MODIFY_STRING));
     }
   });
 });
+
+add_task(async function test_double_click_modify() {
+  Preferences.set(PREF_MODIFY_BOOLEAN, true);
+  Preferences.set(PREF_MODIFY_NUMBER, 10);
+  Preferences.set(PREF_MODIFY_STRING, "Hello!");
+
+  await AboutConfigTest.withNewTab(async function() {
+    this.search(PREF_MODIFY_PREFIX);
+
+    let click = (target, opts) =>
+      EventUtils.synthesizeMouseAtCenter(target, opts, this.window);
+    let doubleClick = target => {
+      // Trigger two mouse events to simulate the first then second click.
+      click(target, { clickCount: 1 });
+      click(target, { clickCount: 2 });
+    };
+    let tripleClick = target => {
+      // Trigger all 3 mouse events to simulate the three mouse events we'd see.
+      click(target, { clickCount: 1 });
+      click(target, { clickCount: 2 });
+      click(target, { clickCount: 3 });
+    };
+
+    // Check double-click to edit a boolean.
+    let boolRow = this.getRow(PREF_MODIFY_BOOLEAN);
+    Assert.equal(boolRow.value, "true");
+    doubleClick(boolRow.valueCell);
+    Assert.equal(boolRow.value, "false");
+    doubleClick(boolRow.nameCell);
+    Assert.equal(boolRow.value, "true");
+
+    // Check double-click to edit a number.
+    let intRow = this.getRow(PREF_MODIFY_NUMBER);
+    Assert.equal(intRow.value, 10);
+    doubleClick(intRow.valueCell);
+    Assert.equal(this.document.activeElement, intRow.valueInput);
+    EventUtils.sendString("75");
+    EventUtils.synthesizeKey("KEY_Enter");
+    Assert.equal(intRow.value, 75);
+
+    // Check double-click focuses input when already editing.
+    Assert.equal(intRow.value, 75);
+    doubleClick(intRow.nameCell);
+    Assert.equal(this.document.activeElement, intRow.valueInput);
+    intRow.valueInput.blur();
+    Assert.notEqual(this.document.activeElement, intRow.valueInput);
+    doubleClick(intRow.nameCell);
+    Assert.equal(this.document.activeElement, intRow.valueInput);
+    EventUtils.sendString("20");
+    EventUtils.synthesizeKey("KEY_Enter");
+    Assert.equal(intRow.value, 20);
+
+    // Check double-click to edit a string.
+    let stringRow = this.getRow(PREF_MODIFY_STRING);
+    Assert.equal(stringRow.value, "Hello!");
+    doubleClick(stringRow.valueCell);
+    Assert.equal(
+      this.document.activeElement,
+      stringRow.valueInput,
+      "The input is focused"
+    );
+    EventUtils.sendString("New String!");
+    EventUtils.synthesizeKey("KEY_Enter");
+    Assert.equal(stringRow.value, "New String!");
+
+    // Check triple-click also edits the pref and selects the text inside.
+    tripleClick(stringRow.nameCell);
+    Assert.equal(
+      this.document.activeElement,
+      stringRow.valueInput,
+      "The input is focused"
+    );
+
+    // Check double-click inside input selects a word.
+    let newString = "Another string...";
+    EventUtils.sendString(newString);
+    Assert.equal(this.window.getSelection().toString(), "");
+    let stringInput = stringRow.valueInput;
+    doubleClick(stringInput);
+    let selectionLength = stringInput.selectionEnd - stringInput.selectionStart;
+    Assert.greater(selectionLength, 0);
+    Assert.less(selectionLength, newString.length);
+    EventUtils.synthesizeKey("KEY_Enter");
+    Assert.equal(stringRow.value, newString);
+
+    // Check that double/triple-click on the add row selects text as usual.
+    let addRow = this.getRow(PREF_MODIFY_PREFIX);
+    Assert.ok(addRow.hasClass("deleted"));
+    doubleClick(addRow.nameCell);
+    Assert.ok(PREF_MODIFY_PREFIX.includes(this.window.getSelection()));
+    tripleClick(addRow.nameCell);
+    Assert.equal(this.window.getSelection().toString(), PREF_MODIFY_PREFIX);
+    // Make sure the localized text is set in the value cell.
+    let labels = Array.from(addRow.valueCell.querySelectorAll("label > span"));
+    await this.document.l10n.translateElements(labels);
+    Assert.ok(labels.every(label => !!label.textContent));
+    // Double-click the first input label text.
+    doubleClick(labels[0]);
+    Assert.equal(this.window.getSelection().toString(), labels[0].textContent);
+    tripleClick(addRow.valueCell.querySelector("label > span"));
+    Assert.equal(
+      this.window.getSelection().toString(),
+      labels.map(l => l.textContent).join("")
+    );
+  });
+});