Bug 1467572 - Part 6: Add the viewport dimension to the global toolbar. r=jryans
authorGabriel Luong <gabriel.luong@gmail.com>
Wed, 15 Aug 2018 17:27:36 -0400
changeset 486951 9f1db12b0ab98e66e49a0816a353ce99e7acdaa9
parent 486950 dae96e14900fcca69d0215602b1b30b29ace0233
child 486952 c208d5b5b7462b19efc3556d112c346c93842307
push id9719
push userffxbld-merge
push dateFri, 24 Aug 2018 17:49:46 +0000
treeherdermozilla-beta@719ec98fba77 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjryans
bugs1467572
milestone63.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 1467572 - Part 6: Add the viewport dimension to the global toolbar. r=jryans
devtools/client/responsive.html/components/App.js
devtools/client/responsive.html/components/Browser.js
devtools/client/responsive.html/components/DeviceAdder.js
devtools/client/responsive.html/components/DeviceModal.js
devtools/client/responsive.html/components/DevicePixelRatioSelector.js
devtools/client/responsive.html/components/DeviceSelector.js
devtools/client/responsive.html/components/ReloadConditions.js
devtools/client/responsive.html/components/ResizableViewport.js
devtools/client/responsive.html/components/ToggleMenu.js
devtools/client/responsive.html/components/Toolbar.js
devtools/client/responsive.html/components/ViewportDimension.js
devtools/client/responsive.html/components/Viewports.js
devtools/client/responsive.html/index.css
--- a/devtools/client/responsive.html/components/App.js
+++ b/devtools/client/responsive.html/components/App.js
@@ -6,16 +6,20 @@
 
 "use strict";
 
 const { Component, createFactory } = require("devtools/client/shared/vendor/react");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const { connect } = require("devtools/client/shared/vendor/react-redux");
 
+const DeviceModal = createFactory(require("./DeviceModal"));
+const Toolbar = createFactory(require("./Toolbar"));
+const Viewports = createFactory(require("./Viewports"));
+
 const {
   addCustomDevice,
   removeCustomDevice,
   updateDeviceDisplayed,
   updateDeviceModal,
   updatePreferredDevices,
 } = require("../actions/devices");
 const { changeNetworkThrottling } = require("devtools/client/shared/components/throttling/actions");
@@ -25,20 +29,16 @@ const { changeTouchSimulation } = requir
 const {
   changeDevice,
   changePixelRatio,
   removeDeviceAssociation,
   resizeViewport,
   rotateViewport,
 } = require("../actions/viewports");
 
-const DeviceModal = createFactory(require("./DeviceModal"));
-const Toolbar = createFactory(require("./Toolbar"));
-const Viewports = createFactory(require("./Viewports"));
-
 const Types = require("../types");
 
 class App extends Component {
   static get propTypes() {
     return {
       devices: PropTypes.shape(Types.devices).isRequired,
       dispatch: PropTypes.func.isRequired,
       displayPixelRatio: Types.pixelRatio.value.isRequired,
@@ -47,16 +47,17 @@ class App extends Component {
       screenshot: PropTypes.shape(Types.screenshot).isRequired,
       touchSimulation: PropTypes.shape(Types.touchSimulation).isRequired,
       viewports: PropTypes.arrayOf(PropTypes.shape(Types.viewport)).isRequired,
     };
   }
 
   constructor(props) {
     super(props);
+
     this.onAddCustomDevice = this.onAddCustomDevice.bind(this);
     this.onBrowserMounted = this.onBrowserMounted.bind(this);
     this.onChangeDevice = this.onChangeDevice.bind(this);
     this.onChangeNetworkThrottling = this.onChangeNetworkThrottling.bind(this);
     this.onChangePixelRatio = this.onChangePixelRatio.bind(this);
     this.onChangeReloadCondition = this.onChangeReloadCondition.bind(this);
     this.onChangeTouchSimulation = this.onChangeTouchSimulation.bind(this);
     this.onContentResize = this.onContentResize.bind(this);
@@ -194,23 +195,22 @@ class App extends Component {
       onRemoveCustomDevice,
       onRemoveDeviceAssociation,
       onResizeViewport,
       onScreenshot,
       onUpdateDeviceDisplayed,
       onUpdateDeviceModal,
     } = this;
 
-    let selectedDevice = "";
-    let selectedPixelRatio = { value: 0 };
+    if (!viewports.length) {
+      return null;
+    }
 
-    if (viewports.length) {
-      selectedDevice = viewports[0].device;
-      selectedPixelRatio = viewports[0].pixelRatio;
-    }
+    const selectedDevice = viewports[0].device;
+    const selectedPixelRatio = viewports[0].pixelRatio;
 
     let deviceAdderViewportTemplate = {};
     if (devices.modalOpenedFromViewport !== null) {
       deviceAdderViewportTemplate = viewports[devices.modalOpenedFromViewport];
     }
 
     return dom.div(
       {
@@ -220,22 +220,24 @@ class App extends Component {
         devices,
         displayPixelRatio,
         networkThrottling,
         reloadConditions,
         screenshot,
         selectedDevice,
         selectedPixelRatio,
         touchSimulation,
+        viewport: viewports[0],
         onChangeDevice,
         onChangeNetworkThrottling,
         onChangePixelRatio,
         onChangeReloadCondition,
         onChangeTouchSimulation,
         onExit,
+        onRemoveDeviceAssociation,
         onResizeViewport,
         onScreenshot,
         onUpdateDeviceModal,
       }),
       Viewports({
         screenshot,
         viewports,
         onBrowserMounted,
--- a/devtools/client/responsive.html/components/Browser.js
+++ b/devtools/client/responsive.html/components/Browser.js
@@ -4,18 +4,18 @@
 
 /* eslint-env browser */
 
 "use strict";
 
 const Services = require("Services");
 const flags = require("devtools/shared/flags");
 const { PureComponent } = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
-const dom = require("devtools/client/shared/vendor/react-dom-factories");
 
 const e10s = require("../utils/e10s");
 const message = require("../utils/message");
 const { getToplevelWindow } = require("../utils/window");
 
 const FRAME_SCRIPT = "resource://devtools/client/responsive.html/browser/content.js";
 
 class Browser extends PureComponent {
--- a/devtools/client/responsive.html/components/DeviceAdder.js
+++ b/devtools/client/responsive.html/components/DeviceAdder.js
@@ -1,36 +1,39 @@
 /* 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/. */
 
 /* eslint-env browser */
 
 "use strict";
 
-const { PureComponent, createFactory } = require("devtools/client/shared/vendor/react");
+const { createFactory, PureComponent } = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
-const dom = require("devtools/client/shared/vendor/react-dom-factories");
+
+const ViewportDimension = createFactory(require("./ViewportDimension.js"));
 
 const { getFormatStr, getStr } = require("../utils/l10n");
 const Types = require("../types");
-const ViewportDimension = createFactory(require("./ViewportDimension.js"));
 
 class DeviceAdder extends PureComponent {
   static get propTypes() {
     return {
       devices: PropTypes.shape(Types.devices).isRequired,
       viewportTemplate: PropTypes.shape(Types.viewport).isRequired,
       onAddCustomDevice: PropTypes.func.isRequired,
     };
   }
 
   constructor(props) {
     super(props);
+
     this.state = {};
+
     this.onChangeSize = this.onChangeSize.bind(this);
     this.onDeviceAdderShow = this.onDeviceAdderShow.bind(this);
     this.onDeviceAdderSave = this.onDeviceAdderSave.bind(this);
   }
 
   componentWillReceiveProps(nextProps) {
     const {
       width,
@@ -38,17 +41,17 @@ class DeviceAdder extends PureComponent 
     } = nextProps.viewportTemplate;
 
     this.setState({
       width,
       height,
     });
   }
 
-  onChangeSize(width, height) {
+  onChangeSize(_, width, height) {
     this.setState({
       width,
       height,
     });
   }
 
   onDeviceAdderShow() {
     this.setState({
@@ -173,17 +176,17 @@ class DeviceAdder extends PureComponent 
               },
               getStr("responsive.deviceAdderSize")
             ),
             ViewportDimension({
               viewport: {
                 width,
                 height,
               },
-              onChangeSize: this.onChangeSize,
+              onResizeViewport: this.onChangeSize,
               onRemoveDeviceAssociation: () => {},
             })
           ),
           dom.label(
             {
               id: "device-adder-pixel-ratio",
             },
             dom.span(
--- a/devtools/client/responsive.html/components/DeviceModal.js
+++ b/devtools/client/responsive.html/components/DeviceModal.js
@@ -1,40 +1,43 @@
 /* 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/. */
 
 /* eslint-env browser */
 
 "use strict";
 
-const { PureComponent, createFactory } = require("devtools/client/shared/vendor/react");
+const { createFactory, PureComponent } = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
-const dom = require("devtools/client/shared/vendor/react-dom-factories");
+
+const DeviceAdder = createFactory(require("./DeviceAdder"));
 
 const { getStr, getFormatStr } = require("../utils/l10n");
 const Types = require("../types");
-const DeviceAdder = createFactory(require("./DeviceAdder"));
 
 class DeviceModal extends PureComponent {
   static get propTypes() {
     return {
       deviceAdderViewportTemplate: PropTypes.shape(Types.viewport).isRequired,
       devices: PropTypes.shape(Types.devices).isRequired,
       onAddCustomDevice: PropTypes.func.isRequired,
       onDeviceListUpdate: PropTypes.func.isRequired,
       onRemoveCustomDevice: PropTypes.func.isRequired,
       onUpdateDeviceDisplayed: PropTypes.func.isRequired,
       onUpdateDeviceModal: PropTypes.func.isRequired,
     };
   }
 
   constructor(props) {
     super(props);
+
     this.state = {};
+
     this.onAddCustomDevice = this.onAddCustomDevice.bind(this);
     this.onDeviceCheckboxChange = this.onDeviceCheckboxChange.bind(this);
     this.onDeviceModalSubmit = this.onDeviceModalSubmit.bind(this);
     this.onKeyDown = this.onKeyDown.bind(this);
   }
 
   componentDidMount() {
     window.addEventListener("keydown", this.onKeyDown, true);
--- a/devtools/client/responsive.html/components/DevicePixelRatioSelector.js
+++ b/devtools/client/responsive.html/components/DevicePixelRatioSelector.js
@@ -1,24 +1,24 @@
 /* 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/. */
 
 /* eslint-env browser */
 
 "use strict";
 
-const { PureComponent} = require("devtools/client/shared/vendor/react");
+const { PureComponent } = require("devtools/client/shared/vendor/react");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 
+const { getStr, getFormatStr } = require("../utils/l10n");
 const Types = require("../types");
-const { getStr, getFormatStr } = require("../utils/l10n");
+
 const labelForOption = value => getFormatStr("responsive.devicePixelRatioOption", value);
-
 const PIXEL_RATIO_PRESET = [1, 2, 3];
 
 const createVisibleOption = value => {
   const label = labelForOption(value);
   return dom.option({
     value,
     title: label,
     key: value,
--- a/devtools/client/responsive.html/components/DeviceSelector.js
+++ b/devtools/client/responsive.html/components/DeviceSelector.js
@@ -1,20 +1,21 @@
 /* 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 { getStr } = require("../utils/l10n");
 const { PureComponent } = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
-const dom = require("devtools/client/shared/vendor/react-dom-factories");
 
+const { getStr } = require("../utils/l10n");
 const Types = require("../types");
+
 const OPEN_DEVICE_MODAL_VALUE = "OPEN_DEVICE_MODAL";
 
 class DeviceSelector extends PureComponent {
   static get propTypes() {
     return {
       devices: PropTypes.shape(Types.devices).isRequired,
       selectedDevice: PropTypes.string.isRequired,
       viewportId: PropTypes.number.isRequired,
--- a/devtools/client/responsive.html/components/ReloadConditions.js
+++ b/devtools/client/responsive.html/components/ReloadConditions.js
@@ -1,20 +1,21 @@
 /* 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 { PureComponent, createFactory } = require("devtools/client/shared/vendor/react");
+const { createFactory, PureComponent } = require("devtools/client/shared/vendor/react");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 
-const Types = require("../types");
+const ToggleMenu = createFactory(require("./ToggleMenu"));
+
 const { getStr } = require("../utils/l10n");
-const ToggleMenu = createFactory(require("./ToggleMenu"));
+const Types = require("../types");
 
 class ReloadConditions extends PureComponent {
   static get propTypes() {
     return {
       reloadConditions: PropTypes.shape(Types.reloadConditions).isRequired,
       onChangeReloadCondition: PropTypes.func.isRequired,
     };
   }
--- a/devtools/client/responsive.html/components/ResizableViewport.js
+++ b/devtools/client/responsive.html/components/ResizableViewport.js
@@ -2,22 +2,23 @@
  * 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/. */
 
 /* global window */
 
 "use strict";
 
 const { Component, createFactory } = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
-const dom = require("devtools/client/shared/vendor/react-dom-factories");
+
+const Browser = createFactory(require("./Browser"));
 
 const Constants = require("../constants");
 const Types = require("../types");
-const Browser = createFactory(require("./Browser"));
 
 const VIEWPORT_MIN_WIDTH = Constants.MIN_VIEWPORT_DIMENSION;
 const VIEWPORT_MIN_HEIGHT = Constants.MIN_VIEWPORT_DIMENSION;
 
 class ResizableViewport extends Component {
   static get propTypes() {
     return {
       screenshot: PropTypes.shape(Types.screenshot).isRequired,
--- a/devtools/client/responsive.html/components/ToggleMenu.js
+++ b/devtools/client/responsive.html/components/ToggleMenu.js
@@ -1,17 +1,17 @@
 /* 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 { PureComponent } = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
-const dom = require("devtools/client/shared/vendor/react-dom-factories");
 
 const MenuItem = {
   id: PropTypes.string.isRequired,
   label: PropTypes.string.isRequired,
   checked: PropTypes.bool,
 };
 
 class ToggleMenu extends PureComponent {
--- a/devtools/client/responsive.html/components/Toolbar.js
+++ b/devtools/client/responsive.html/components/Toolbar.js
@@ -1,86 +1,97 @@
 /* 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 { PureComponent, createFactory } = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
-const dom = require("devtools/client/shared/vendor/react-dom-factories");
 
-const { getStr } = require("../utils/l10n");
-const Types = require("../types");
 const DevicePixelRatioSelector = createFactory(require("./DevicePixelRatioSelector"));
 const DeviceSelector = createFactory(require("./DeviceSelector"));
 const NetworkThrottlingSelector = createFactory(require("devtools/client/shared/components/throttling/NetworkThrottlingSelector"));
 const ReloadConditions = createFactory(require("./ReloadConditions"));
+const ViewportDimension = createFactory(require("./ViewportDimension"));
+
+const { getStr } = require("../utils/l10n");
+const Types = require("../types");
 
 class Toolbar extends PureComponent {
   static get propTypes() {
     return {
       devices: PropTypes.shape(Types.devices).isRequired,
       displayPixelRatio: Types.pixelRatio.value.isRequired,
       networkThrottling: PropTypes.shape(Types.networkThrottling).isRequired,
       reloadConditions: PropTypes.shape(Types.reloadConditions).isRequired,
       screenshot: PropTypes.shape(Types.screenshot).isRequired,
       selectedDevice: PropTypes.string.isRequired,
       selectedPixelRatio: PropTypes.shape(Types.pixelRatio).isRequired,
       touchSimulation: PropTypes.shape(Types.touchSimulation).isRequired,
+      viewport: PropTypes.shape(Types.viewport).isRequired,
       onChangeDevice: PropTypes.func.isRequired,
       onChangeNetworkThrottling: PropTypes.func.isRequired,
       onChangePixelRatio: PropTypes.func.isRequired,
       onChangeReloadCondition: PropTypes.func.isRequired,
       onChangeTouchSimulation: PropTypes.func.isRequired,
       onExit: PropTypes.func.isRequired,
+      onRemoveDeviceAssociation: PropTypes.func.isRequired,
       onResizeViewport: PropTypes.func.isRequired,
       onScreenshot: PropTypes.func.isRequired,
       onUpdateDeviceModal: PropTypes.func.isRequired,
     };
   }
 
   render() {
     const {
       devices,
       displayPixelRatio,
       networkThrottling,
       reloadConditions,
       screenshot,
       selectedDevice,
       selectedPixelRatio,
       touchSimulation,
+      viewport,
       onChangeDevice,
       onChangeNetworkThrottling,
       onChangePixelRatio,
       onChangeReloadCondition,
       onChangeTouchSimulation,
       onExit,
+      onRemoveDeviceAssociation,
       onResizeViewport,
       onScreenshot,
       onUpdateDeviceModal,
     } = this.props;
 
     let touchButtonClass = "toolbar-button devtools-button";
     if (touchSimulation.enabled) {
       touchButtonClass += " checked";
     }
 
     return dom.header(
       { id: "toolbar" },
       DeviceSelector({
         devices,
         selectedDevice,
-        viewportId: 0,
+        viewportId: viewport.id,
         onChangeDevice,
         onResizeViewport,
         onUpdateDeviceModal,
       }),
       dom.div(
         { id: "toolbar-center-controls" },
+        ViewportDimension({
+          viewport,
+          onRemoveDeviceAssociation,
+          onResizeViewport,
+        }),
         DevicePixelRatioSelector({
           devices,
           displayPixelRatio,
           selectedDevice,
           selectedPixelRatio,
           onChangePixelRatio,
         }),
         NetworkThrottlingSelector({
--- a/devtools/client/responsive.html/components/ViewportDimension.js
+++ b/devtools/client/responsive.html/components/ViewportDimension.js
@@ -1,21 +1,199 @@
 /* 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 { Component } = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
-const dom = require("devtools/client/shared/vendor/react-dom-factories");
+
 const { isKeyIn } = require("../utils/key");
+const { MIN_VIEWPORT_DIMENSION } = require("../constants");
+const Types = require("../types");
 
-const Constants = require("../constants");
-const Types = require("../types");
+class ViewportDimension extends Component {
+  static get propTypes() {
+    return {
+      viewport: PropTypes.shape(Types.viewport).isRequired,
+      onResizeViewport: PropTypes.func.isRequired,
+      onRemoveDeviceAssociation: PropTypes.func.isRequired,
+    };
+  }
+
+  constructor(props) {
+    super(props);
+
+    const { width, height } = props.viewport;
+
+    this.state = {
+      width,
+      height,
+      isEditing: false,
+      isWidthValid: true,
+      isHeightValid: true,
+    };
+
+    this.isInputValid = this.isInputValid.bind(this);
+    this.onInputBlur = this.onInputBlur.bind(this);
+    this.onInputChange = this.onInputChange.bind(this);
+    this.onInputFocus = this.onInputFocus.bind(this);
+    this.onInputKeyDown = this.onInputKeyDown.bind(this);
+    this.onInputKeyUp = this.onInputKeyUp.bind(this);
+    this.onInputSubmit = this.onInputSubmit.bind(this);
+  }
+
+  componentWillReceiveProps(nextProps) {
+    const { width, height } = nextProps.viewport;
+
+    this.setState({
+      width,
+      height,
+    });
+  }
+
+  /**
+   * Return true if the given value is a number and greater than MIN_VIEWPORT_DIMENSION
+   * and false otherwise.
+   */
+  isInputValid(value) {
+    return /^\d{3,4}$/.test(value) && parseInt(value, 10) >= MIN_VIEWPORT_DIMENSION;
+  }
+
+  onInputBlur() {
+    const { width, height } = this.props.viewport;
+
+    if (this.state.width != width || this.state.height != height) {
+      this.onInputSubmit();
+    }
+
+    this.setState({ isEditing: false });
+  }
+
+  onInputChange({ target }, callback) {
+    if (target.value.length > 4) {
+      return;
+    }
+
+    if (this.widthInput == target) {
+      this.setState({
+        width: target.value,
+        isWidthValid: this.isInputValid(target.value),
+      }, callback);
+    }
+
+    if (this.heightInput == target) {
+      this.setState({
+        height: target.value,
+        isHeightValid: this.isInputValid(target.value),
+      }, callback);
+    }
+  }
+
+  onInputFocus() {
+    this.setState({ isEditing: true });
+  }
+
+  onInputKeyDown(event) {
+    const increment = getIncrement(event);
+    if (!increment) {
+      return;
+    }
+
+    const { target } = event;
+    target.value = parseInt(target.value, 10) + increment;
+    this.onInputChange(event, this.onInputSubmit);
+  }
+
+  onInputKeyUp({ target, keyCode }) {
+    // On Enter, submit the input
+    if (keyCode == 13) {
+      this.onInputSubmit();
+    }
+
+    // On Esc, blur the target
+    if (keyCode == 27) {
+      target.blur();
+    }
+  }
+
+  onInputSubmit() {
+    const {
+      viewport,
+      onRemoveDeviceAssociation,
+      onResizeViewport,
+    } = this.props;
+
+    if (!this.state.isWidthValid || !this.state.isHeightValid) {
+      const { width, height } = viewport;
+
+      this.setState({
+        width,
+        height,
+        isWidthValid: true,
+        isHeightValid: true,
+      });
+
+      return;
+    }
+
+    // Change the device selector back to an unselected device
+    // TODO: Bug 1332754: Logic like this probably belongs in the action creator.
+    if (viewport.device) {
+      onRemoveDeviceAssociation(viewport.id);
+    }
+
+    onResizeViewport(viewport.id,
+      parseInt(this.state.width, 10), parseInt(this.state.height, 10));
+  }
+
+  render() {
+    return dom.div(
+      {
+        className:
+          "viewport-dimension" +
+          (this.state.isEditing ? " editing" : "") +
+          (!this.state.isWidthValid || !this.state.isHeightValid ? " invalid" : ""),
+      },
+      dom.input({
+        ref: input => {
+          this.widthInput = input;
+        },
+        className: "viewport-dimension-input" +
+                   (this.state.isWidthValid ? "" : " invalid"),
+        size: 4,
+        value: this.state.width,
+        onBlur: this.onInputBlur,
+        onChange: this.onInputChange,
+        onFocus: this.onInputFocus,
+        onKeyDown: this.onInputKeyDown,
+        onKeyUp: this.onInputKeyUp,
+      }),
+      dom.span({
+        className: "viewport-dimension-separator",
+      }, "×"),
+      dom.input({
+        ref: input => {
+          this.heightInput = input;
+        },
+        className: "viewport-dimension-input" +
+                   (this.state.isHeightValid ? "" : " invalid"),
+        size: 4,
+        value: this.state.height,
+        onBlur: this.onInputBlur,
+        onChange: this.onInputChange,
+        onFocus: this.onInputFocus,
+        onKeyDown: this.onInputKeyDown,
+        onKeyUp: this.onInputKeyUp,
+      })
+    );
+  }
+}
 
 /**
  * Get the increment/decrement step to use for the provided key event.
  */
 function getIncrement(event) {
   const defaultIncrement = 1;
   const largeIncrement = 100;
   const mediumIncrement = 10;
@@ -35,191 +213,9 @@ function getIncrement(event) {
     } else {
       increment *= mediumIncrement;
     }
   }
 
   return increment;
 }
 
-class ViewportDimension extends Component {
-  static get propTypes() {
-    return {
-      viewport: PropTypes.shape(Types.viewport).isRequired,
-      onChangeSize: PropTypes.func.isRequired,
-      onRemoveDeviceAssociation: PropTypes.func.isRequired,
-    };
-  }
-
-  constructor(props) {
-    super(props);
-    const { width, height } = props.viewport;
-
-    this.state = {
-      width,
-      height,
-      isEditing: false,
-      isInvalid: false,
-    };
-
-    this.validateInput = this.validateInput.bind(this);
-    this.onInputBlur = this.onInputBlur.bind(this);
-    this.onInputChange = this.onInputChange.bind(this);
-    this.onInputFocus = this.onInputFocus.bind(this);
-    this.onInputKeyDown = this.onInputKeyDown.bind(this);
-    this.onInputKeyUp = this.onInputKeyUp.bind(this);
-    this.onInputSubmit = this.onInputSubmit.bind(this);
-  }
-
-  componentWillReceiveProps(nextProps) {
-    const { 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() {
-    const { width, height } = this.props.viewport;
-
-    if (this.state.width != width || this.state.height != height) {
-      this.onInputSubmit();
-    }
-
-    this.setState({
-      isEditing: false,
-      inInvalid: false,
-    });
-  }
-
-  onInputChange({ target }, callback) {
-    if (target.value.length > 4) {
-      return;
-    }
-
-    if (this.refs.widthInput == target) {
-      this.setState({ width: target.value }, callback);
-      this.validateInput(target.value);
-    }
-
-    if (this.refs.heightInput == target) {
-      this.setState({ height: target.value }, callback);
-      this.validateInput(target.value);
-    }
-  }
-
-  onInputFocus() {
-    this.setState({
-      isEditing: true,
-    });
-  }
-
-  onInputKeyDown(event) {
-    const { target } = event;
-    const increment = getIncrement(event);
-    if (!increment) {
-      return;
-    }
-    target.value = parseInt(target.value, 10) + increment;
-    this.onInputChange(event, this.onInputSubmit);
-  }
-
-  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) {
-      const { width, height } = this.props.viewport;
-
-      this.setState({
-        width,
-        height,
-        isInvalid: false,
-      });
-
-      return;
-    }
-
-    // Change the device selector back to an unselected device
-    // TODO: Bug 1332754: Logic like this probably belongs in the action creator.
-    if (this.props.viewport.device) {
-      this.props.onRemoveDeviceAssociation();
-    }
-    this.props.onChangeSize(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,
-          onKeyDown: this.onInputKeyDown,
-          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,
-          onKeyDown: this.onInputKeyDown,
-          onKeyUp: this.onInputKeyUp,
-        })
-      )
-    );
-  }
-}
-
 module.exports = ViewportDimension;
--- a/devtools/client/responsive.html/components/Viewports.js
+++ b/devtools/client/responsive.html/components/Viewports.js
@@ -1,20 +1,21 @@
 /* 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 { Component, createFactory } = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
-const dom = require("devtools/client/shared/vendor/react-dom-factories");
+
+const ResizableViewport = createFactory(require("./ResizableViewport"));
 
 const Types = require("../types");
-const ResizableViewport = createFactory(require("./ResizableViewport"));
 
 class Viewports extends Component {
   static get propTypes() {
     return {
       screenshot: PropTypes.shape(Types.screenshot).isRequired,
       viewports: PropTypes.arrayOf(PropTypes.shape(Types.viewport)).isRequired,
       onBrowserMounted: PropTypes.func.isRequired,
       onContentResize: PropTypes.func.isRequired,
--- a/devtools/client/responsive.html/index.css
+++ b/devtools/client/responsive.html/index.css
@@ -366,58 +366,44 @@ select > option.divider {
   width: calc(100% - 16px);
   height: 5px;
   left: 0;
   bottom: -4px;
   cursor: s-resize;
 }
 
 /**
- * Viewport Dimension Label
+ * Viewport Dimension Input
  */
 
 .viewport-dimension {
   display: flex;
-  justify-content: center;
-  font: 10px sans-serif;
-  margin-bottom: 10px;
-}
-
-.viewport-dimension-editable {
-  border-bottom: 1px solid transparent;
-}
-
-.viewport-dimension-editable,
-.viewport-dimension-input {
-  color: var(--theme-body-color-inactive);
-  transition: all 0.25s ease;
-}
-
-.viewport-dimension-editable.editing,
-.viewport-dimension-input.editing {
-  color: var(--viewport-active-color);
-  outline: none;
-}
-
-.viewport-dimension-editable.editing {
-  border-bottom: 1px solid var(--theme-selection-background);
-}
-
-.viewport-dimension-editable.editing.invalid {
-  border-bottom: 1px solid #d92215;
+  align-items: center;
+  margin: 1px;
 }
 
 .viewport-dimension-input {
-  background: transparent;
-  border: none;
+  border: 1px solid var(--theme-splitter-color);
+  outline: none;
   text-align: center;
+  width: 3em;
+}
+
+.viewport-dimension-input:focus {
+  border: 1px solid var(--theme-selection-background);
+  transition: all 0.2s ease-in-out;
+}
+
+.viewport-dimension-input.invalid:focus {
+  border: 1px solid #d92215;
 }
 
 .viewport-dimension-separator {
   -moz-user-select: none;
+  padding: 0 0.3em;
 }
 
 /**
  * Device Modal
  */
 
 @keyframes fade-in-and-up {
   0% {
@@ -599,16 +585,33 @@ select > option.divider {
 }
 
 #device-adder label > input,
 #device-adder label > .viewport-dimension {
   flex: 1;
   margin: 0;
 }
 
+#device-adder label > .viewport-dimension {
+  border-bottom: 1px solid transparent;
+  color: var(--theme-body-color-inactive);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  transition: all 0.25s ease;
+}
+
+#device-adder label > .viewport-dimension.editing {
+  border-bottom-color: var(--theme-selection-background);
+}
+
+#device-adder label > .viewport-dimension.editing.invalid {
+  border-bottom-color: #d92215;
+}
+
 #device-adder input {
   background: transparent;
   border: 1px solid transparent;
   text-align: center;
   color: var(--theme-body-color-inactive);
   transition: all 0.25s ease;
 }