Bug 1261317 - part2: prevent autocomplete on arrow keys in multiline editor;r=pbro
authorJulian Descottes <jdescottes@mozilla.com>
Fri, 01 Apr 2016 17:26:58 +0200
changeset 291868 d37f13e88aae4ba2c69951189f3d88c723bfc9a7
parent 291867 33d4c04cf572fecce5536c27f164753ba2a7e30f
child 291869 64693d39a134df2274838df42029c58e9451ad94
push idunknown
push userunknown
push dateunknown
reviewerspbro
bugs1261317
milestone48.0a1
Bug 1261317 - part2: prevent autocomplete on arrow keys in multiline editor;r=pbro Check if the CSS inplace editor is currently using several lines to display its value. When using several lines, prevent increment, decrement and autocomplete features. The autocomplete can still be triggered by typing a value at the end of the input (or before a space). When the autocomplete popup is opened, UP/DOWN still allow to cycle through the suggestions. MozReview-Commit-ID: DapCdhjx444
devtools/client/inspector/rules/test/browser.ini
devtools/client/inspector/rules/test/browser_rules_completion-new-property_multiline.js
devtools/client/shared/inplace-editor.js
--- a/devtools/client/inspector/rules/test/browser.ini
+++ b/devtools/client/inspector/rules/test/browser.ini
@@ -67,16 +67,17 @@ support-files =
 [browser_rules_colorpicker-swatch-displayed.js]
 [browser_rules_colorUnit.js]
 [browser_rules_completion-existing-property_01.js]
 [browser_rules_completion-existing-property_02.js]
 [browser_rules_completion-new-property_01.js]
 [browser_rules_completion-new-property_02.js]
 [browser_rules_completion-new-property_03.js]
 [browser_rules_completion-new-property_04.js]
+[browser_rules_completion-new-property_multiline.js]
 [browser_rules_computed-lists_01.js]
 [browser_rules_computed-lists_02.js]
 [browser_rules_completion-popup-hidden-after-navigation.js]
 [browser_rules_content_01.js]
 [browser_rules_content_02.js]
 skip-if = e10s && debug # Bug 1250058 - Docshell leak on debug e10s
 [browser_rules_context-menu-show-mdn-docs-01.js]
 [browser_rules_context-menu-show-mdn-docs-02.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_multiline.js
@@ -0,0 +1,119 @@
+/* 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/ */
+
+"use strict";
+
+// Test the behaviour of the CSS autocomplete for CSS value displayed on
+// multiple lines. Expected behavior is:
+// - UP/DOWN should navigate in the input and not increment/decrement numbers
+// - typing a new value should still trigger the autocomplete
+// - UP/DOWN when the autocomplete popup is displayed should cycle through
+//   suggestions
+
+const LONG_CSS_VALUE =
+  "transparent linear-gradient(0deg, blue 0%, white 5%, red 10%, blue 15%, " +
+  "white 20%, red 25%, blue 30%, white 35%, red 40%, blue 45%, white 50%, " +
+  "red 55%, blue 60%, white 65%, red 70%, blue 75%, white 80%, red 85%, " +
+  "blue 90%, white 95% ) repeat scroll 0% 0%";
+
+const EXPECTED_CSS_VALUE = LONG_CSS_VALUE.replace("95%", "95%, red");
+
+const TEST_URI =
+  `<style>
+    .title {
+      background: ${LONG_CSS_VALUE};
+    }
+  </style>
+  <h1 class=title>Header</h1>`;
+
+add_task(function*() {
+  yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  let { inspector, view} = yield openRuleView();
+
+  info("Selecting the test node");
+  yield selectNode("h1", inspector);
+
+  info("Focusing the property editable field");
+  let rule = getRuleViewRuleEditor(view, 1).rule;
+  let prop = rule.textProps[0];
+
+  info("Focusing the css property editable value");
+  let rect = prop.editor.valueSpan.getBoundingClientRect();
+  let editor = yield focusEditableField(view, prop.editor.valueSpan,
+    rect.width / 2, rect.height / 2);
+
+  info("Moving the caret next to a number");
+  let pos = editor.input.value.indexOf("0deg") + 1;
+  editor.input.setSelectionRange(pos, pos);
+  is(editor.input.value[editor.input.selectionStart - 1], "0",
+    "Input caret is after a 0");
+
+  info("Check that UP/DOWN navigates in the input, even when next to a number");
+  EventUtils.synthesizeKey("VK_DOWN", {}, view.styleWindow);
+  ok(editor.input.selectionStart != pos, "Input caret moved");
+  is(editor.input.value, LONG_CSS_VALUE, "Input value was not decremented.");
+
+  info("Move the caret to the end of the gradient definition.");
+  pos = editor.input.value.indexOf("95%") + 3;
+  editor.input.setSelectionRange(pos, pos);
+
+  info("Sending \", re\" to the editable field.");
+  for (let key of ", re") {
+    yield synthesizeKeyForAutocomplete(key, editor, view.styleWindow);
+  }
+
+  info("Check the autocomplete can still be displayed.");
+  ok(editor.popup && editor.popup.isOpen, "Autocomplete popup is displayed.");
+  is(editor.popup.selectedIndex, 0,
+    "Autocomplete has an item selected by default");
+
+  let item = editor.popup.getItemAtIndex(editor.popup.selectedIndex);
+  is(item.label, "rebeccapurple",
+    "Check autocomplete displays expected value.");
+
+  info("Check autocomplete suggestions can be cycled using UP/DOWN arrows.");
+
+  yield synthesizeKeyForAutocomplete("VK_DOWN", editor, view.styleWindow);
+  ok(editor.popup.selectedIndex, 1, "Using DOWN cycles autocomplete values.");
+  yield synthesizeKeyForAutocomplete("VK_DOWN", editor, view.styleWindow);
+  ok(editor.popup.selectedIndex, 2, "Using DOWN cycles autocomplete values.");
+  yield synthesizeKeyForAutocomplete("VK_UP", editor, view.styleWindow);
+  is(editor.popup.selectedIndex, 1, "Using UP cycles autocomplete values.");
+  item = editor.popup.getItemAtIndex(editor.popup.selectedIndex);
+  is(item.label, "red", "Check autocomplete displays expected value.");
+
+  info("Select the background-color suggestion with a mouse click.");
+  let onRuleviewChanged = view.once("ruleview-changed");
+  let onInputFocus = once(editor.input, "focus", true);
+  let node = editor.popup._list.childNodes[editor.popup.selectedIndex];
+  EventUtils.synthesizeMouseAtCenter(node, {}, view.styleWindow);
+  yield onInputFocus;
+  yield onRuleviewChanged;
+
+  is(editor.input.value, EXPECTED_CSS_VALUE,
+    "Input value correctly autocompleted");
+
+  info("Press ESCAPE to leave the input.");
+  onRuleviewChanged = view.once("ruleview-changed");
+  EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow);
+  yield onRuleviewChanged;
+});
+
+/**
+ * Send the provided key to the currently focused input of the provided window.
+ * Wait for the editor to emit "after-suggest" to make sure the autocompletion
+ * process is finished.
+ *
+ * @param {String} key
+ *        The key to send to the input.
+ * @param {InplaceEditor} editor
+ *        The inplace editor which owns the focused input.
+ * @param {Window} win
+ *        Window in which the key event will be dispatched.
+ */
+function* synthesizeKeyForAutocomplete(key, editor, win) {
+  let onSuggest = editor.once("after-suggest");
+  EventUtils.synthesizeKey(key, {}, win);
+  yield onSuggest;
+}
--- a/devtools/client/shared/inplace-editor.js
+++ b/devtools/client/shared/inplace-editor.js
@@ -981,44 +981,50 @@ InplaceEditor.prototype = {
    * Handle the input field's keypress event.
    */
   _onKeyPress: function(event) {
     let prevent = false;
 
     let key = event.keyCode;
     let input = this.input;
 
+    let multilineNavigation = !this._isSingleLine() &&
+      isKeyIn(key, "UP", "DOWN", "LEFT", "RIGHT");
     let isPlainText = this.contentType == CONTENT_TYPES.PLAIN_TEXT;
+    let isPopupOpen = this.popup && this.popup.isOpen;
 
     let increment = 0;
-    if (!isPlainText) {
+    if (!isPlainText && !multilineNavigation) {
       increment = this._getIncrement(event);
     }
 
     if (isKeyIn(key, "HOME", "END", "PAGE_UP", "PAGE_DOWN")) {
       this._preventSuggestions = true;
     }
 
     let cycling = false;
     if (increment && this._incrementValue(increment)) {
       this._updateSize();
       prevent = true;
       cycling = true;
-    } else if (increment && this.popup && this.popup.isOpen) {
+    }
+
+    if (isPopupOpen && isKeyIn(key, "UP", "DOWN", "PAGE_UP", "PAGE_DOWN")) {
       prevent = true;
       cycling = true;
-      this._cycleCSSSuggestion(increment > 0);
+      this._cycleCSSSuggestion(isKeyIn(key, "UP", "PAGE_UP"));
       this._doValidation();
     }
 
     if (isKeyIn(key, "BACK_SPACE", "DELETE", "LEFT", "RIGHT")) {
-      if (this.popup && this.popup.isOpen) {
+      if (isPopupOpen) {
         this.popup.hidePopup();
       }
-    } else if (!cycling && !event.metaKey && !event.altKey && !event.ctrlKey) {
+    } else if (!cycling && !multilineNavigation &&
+      !event.metaKey && !event.altKey && !event.ctrlKey) {
       this._maybeSuggestCompletion(true);
     }
 
     if (this.multiline && event.shiftKey && isKeyIn(key, "RETURN")) {
       prevent = false;
     } else if (
       this._advanceChars(event.charCode, input.value, input.selectionStart) ||
       isKeyIn(key, "RETURN", "TAB")) {