Bug 1246807 - Implement Editable width / height inputs r=jryans
authorGabriel Luong <gabriel.luong@gmail.com>
Mon, 22 Feb 2016 13:15:26 -0500
changeset 321451 c9d23c6bf98bca091cc3dc02b97dc335a4ad346b
parent 321450 897d193994904c5fc5fb3d16f3b3a18d2f2d420d
child 321452 66be41482fd44fbcae112ea3a09ec4395769c06c
push id5913
push userjlund@mozilla.com
push dateMon, 25 Apr 2016 16:57:49 +0000
treeherdermozilla-beta@dcaf0a6fa115 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjryans
bugs1246807
milestone47.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 1246807 - Implement Editable width / height inputs r=jryans
devtools/client/responsive.html/components/moz.build
devtools/client/responsive.html/components/resizable-viewport.js
devtools/client/responsive.html/components/viewport-dimension.js
devtools/client/responsive.html/components/viewport.js
devtools/client/responsive.html/constants.js
devtools/client/responsive.html/index.css
devtools/client/responsive.html/moz.build
--- a/devtools/client/responsive.html/components/moz.build
+++ b/devtools/client/responsive.html/components/moz.build
@@ -2,12 +2,13 @@
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
     'browser.js',
     'resizable-viewport.js',
+    'viewport-dimension.js',
     'viewport-toolbar.js',
     'viewport.js',
     'viewports.js',
 )
--- a/devtools/client/responsive.html/components/resizable-viewport.js
+++ b/devtools/client/responsive.html/components/resizable-viewport.js
@@ -4,22 +4,23 @@
 
 /* global window */
 
 "use strict";
 
 const { DOM: dom, createClass, createFactory, PropTypes } =
   require("devtools/client/shared/vendor/react");
 
+const Constants = require("../constants");
 const Types = require("../types");
 const Browser = createFactory(require("./browser"));
 const ViewportToolbar = createFactory(require("./viewport-toolbar"));
 
-const VIEWPORT_MIN_WIDTH = 280;
-const VIEWPORT_MIN_HEIGHT = 280;
+const VIEWPORT_MIN_WIDTH = Constants.MIN_VIEWPORT_DIMENSION;
+const VIEWPORT_MIN_HEIGHT = Constants.MIN_VIEWPORT_DIMENSION;
 
 module.exports = createClass({
 
   displayName: "ResizableViewport",
 
   propTypes: {
     location: Types.location.isRequired,
     viewport: PropTypes.shape(Types.viewport).isRequired,
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/components/viewport-dimension.js
@@ -0,0 +1,164 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { DOM: dom, createClass, PropTypes } =
+  require("devtools/client/shared/vendor/react");
+
+const Constants = require("../constants");
+const Types = require("../types");
+
+module.exports = createClass({
+
+  displayName: "ViewportDimension",
+
+  propTypes: {
+    viewport: PropTypes.shape(Types.viewport).isRequired,
+    onResizeViewport: PropTypes.func.isRequired,
+  },
+
+  getInitialState() {
+    let { width, height } = this.props.viewport;
+
+    return {
+      width,
+      height,
+      isEditing: false,
+      isInvalid: false,
+    };
+  },
+
+  componentWillReceiveProps(nextProps) {
+    let { width, height } = nextProps.viewport;
+
+    this.setState({
+      width,
+      height,
+    });
+  },
+
+  validateInput(value) {
+    let isInvalid = true;
+
+    // Check the value is a number and greater than MIN_VIEWPORT_DIMENSION
+    if (/^\d{3,4}$/.test(value) &&
+        parseInt(value, 10) >= Constants.MIN_VIEWPORT_DIMENSION) {
+      isInvalid = false;
+    }
+
+    this.setState({
+      isInvalid,
+    });
+  },
+
+  onInputBlur() {
+    this.onInputSubmit();
+
+    this.setState({
+      isEditing: false,
+      inInvalid: false,
+    });
+  },
+
+  onInputChange({ target }) {
+    if (target.value.length > 4) {
+      return;
+    }
+
+    if (this.refs.widthInput == target) {
+      this.setState({ width: target.value });
+      this.validateInput(target.value);
+    }
+
+    if (this.refs.heightInput == target) {
+      this.setState({ height: target.value });
+      this.validateInput(target.value);
+    }
+  },
+
+  onInputFocus() {
+    this.setState({
+      isEditing: true,
+    });
+  },
+
+  onInputKeyUp({ target, keyCode }) {
+    // On Enter, submit the input
+    if (keyCode == 13) {
+      this.onInputSubmit();
+    }
+
+    // On Esc, blur the target
+    if (keyCode == 27) {
+      target.blur();
+    }
+  },
+
+  onInputSubmit() {
+    if (this.state.isInvalid) {
+      let { width, height } = this.props.viewport;
+
+      this.setState({
+        width,
+        height,
+        isInvalid: false,
+      });
+
+      return;
+    }
+
+    this.props.onResizeViewport(parseInt(this.state.width, 10),
+                                parseInt(this.state.height, 10));
+  },
+
+  render() {
+    let editableClass = "viewport-dimension-editable";
+    let inputClass = "viewport-dimension-input";
+
+    if (this.state.isEditing) {
+      editableClass += " editing";
+      inputClass += " editing";
+    }
+
+    if (this.state.isInvalid) {
+      editableClass += " invalid";
+    }
+
+    return dom.div(
+      {
+        className: "viewport-dimension",
+      },
+      dom.div(
+        {
+          className: editableClass,
+        },
+        dom.input({
+          ref: "widthInput",
+          className: inputClass,
+          size: 4,
+          value: this.state.width,
+          onBlur: this.onInputBlur,
+          onChange: this.onInputChange,
+          onFocus: this.onInputFocus,
+          onKeyUp: this.onInputKeyUp,
+        }),
+        dom.span({
+          className: "viewport-dimension-separator",
+        }, "×"),
+        dom.input({
+          ref: "heightInput",
+          className: inputClass,
+          size: 4,
+          value: this.state.height,
+          onBlur: this.onInputBlur,
+          onChange: this.onInputChange,
+          onFocus: this.onInputFocus,
+          onKeyUp: this.onInputKeyUp,
+        })
+      )
+    );
+  },
+
+});
--- a/devtools/client/responsive.html/components/viewport.js
+++ b/devtools/client/responsive.html/components/viewport.js
@@ -4,16 +4,17 @@
 
 "use strict";
 
 const { DOM: dom, createClass, createFactory, PropTypes } =
   require("devtools/client/shared/vendor/react");
 
 const Types = require("../types");
 const ResizableViewport = createFactory(require("./resizable-viewport"));
+const ViewportDimension = createFactory(require("./viewport-dimension"));
 
 module.exports = createClass({
 
   displayName: "Viewport",
 
   propTypes: {
     location: Types.location.isRequired,
     viewport: PropTypes.shape(Types.viewport).isRequired,
@@ -33,13 +34,17 @@ module.exports = createClass({
       {
         className: "viewport",
       },
       ResizableViewport({
         location,
         viewport,
         onResizeViewport,
         onRotateViewport,
+      }),
+      ViewportDimension({
+        viewport,
+        onResizeViewport,
       })
     );
   },
 
 });
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/constants.js
@@ -0,0 +1,8 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// The minimum viewport width and height
+exports.MIN_VIEWPORT_DIMENSION = 280;
--- a/devtools/client/responsive.html/index.css
+++ b/devtools/client/responsive.html/index.css
@@ -2,20 +2,24 @@
  * React component group on how to best handle CSS. */
 
 /**
  * CSS Variables specific to the responsive design mode
  */
 
 .theme-light {
   --viewport-box-shadow: 0 4px 4px 0 rgba(155, 155, 155, 0.26);
+  --viewport-dimension-color: var(--theme-splitter-color);
+  --viewport-dimension-editing-color: var(--theme-body-color);
 }
 
 .theme-dark {
   --viewport-box-shadow: 0 4px 4px 0 rgba(105, 105, 105, 0.26);
+  --viewport-dimension-color: var(--theme-body-color);
+  --viewport-dimension-editing-color: var(--theme-selection-color);
 }
 
 * {
   box-sizing: border-box;
 }
 
 html, body {
   margin: 0;
@@ -52,21 +56,21 @@ body {
 /**
  * Viewport Container
  */
 
 .viewport {
   display: inline-block;
   /* Align all viewports to the top */
   vertical-align: top;
-  border: 1px solid var(--theme-splitter-color);
-  box-shadow: var(--viewport-box-shadow);
 }
 
 .resizable-viewport {
+  border: 1px solid var(--theme-splitter-color);
+  box-shadow: var(--viewport-box-shadow);
   position: relative;
 }
 
 /**
  * Viewport Toolbar
  */
 
 .viewport-toolbar {
@@ -147,8 +151,52 @@ body {
 .viewport-vertical-resize-handle {
   position: absolute;
   width: calc(100% - 16px);
   height: 5px;
   left: 0;
   bottom: -4px;
   cursor: s-resize;
 }
+
+/**
+ * Viewport Dimension Label
+ */
+
+.viewport-dimension {
+  display: flex;
+  justify-content: center;
+  font: 10px sans-serif;
+  margin-top: 10px;
+}
+
+.viewport-dimension-editable {
+  border-bottom: 1px solid transparent;
+}
+
+.viewport-dimension-editable,
+.viewport-dimension-input {
+  color: var(--viewport-dimension-color);
+  transition: all 0.25s ease;
+}
+
+.viewport-dimension-editable.editing,
+.viewport-dimension-input.editing {
+  color: var(--viewport-dimension-editing-color);
+}
+
+.viewport-dimension-editable.editing {
+  border-bottom: 1px solid var(--theme-selection-background);
+}
+
+.viewport-dimension-editable.editing.invalid {
+  border-bottom: 1px solid #d92215;
+}
+
+.viewport-dimension-input {
+  background: transparent;
+  border: none;
+  text-align: center;
+}
+
+.viewport-dimension-span {
+  -moz-user-select: none;
+}
--- a/devtools/client/responsive.html/moz.build
+++ b/devtools/client/responsive.html/moz.build
@@ -8,16 +8,17 @@ DIRS += [
     'actions',
     'components',
     'images',
     'reducers',
 ]
 
 DevToolsModules(
     'app.js',
+    'constants.js',
     'index.css',
     'manager.js',
     'reducers.js',
     'store.js',
     'types.js',
 )
 
 XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']