Bug 1467572 - Part 6: Add the viewport dimension to the global toolbar. r=jryans
☠☠ backed out by dc2c0b075c0e ☠ ☠
authorGabriel Luong <gabriel.luong@gmail.com>
Wed, 15 Aug 2018 17:27:36 -0400
changeset 486918 e83526778ccde7eb8d9deb9c2422efa7bfe9f02f
parent 486917 c42448ca6e4a14c9f860bff8ebb4cb0087d188e1
child 486919 0f19f84bb2edb6eb55a6e1f4e29319e95981cbd5
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;
 }