Bug 1266414: device modal fades in/out, r=jryans
authorHelen V. Holmes <helen.v.holmes@gmail.com>
Tue, 17 May 2016 13:49:29 -0400
changeset 305599 1ece89d2f73e34936a4b86df16ce30a7fd51e1e9
parent 305598 003901a846c74e63c614cc7fa6653a3683f71cd5
child 305600 7985dab6e65e467609a515497a6cf47b2e15785c
push id30467
push usercbook@mozilla.com
push dateWed, 20 Jul 2016 09:21:53 +0000
treeherdermozilla-central@e904e18d7dfc [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjryans
bugs1266414
milestone50.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 1266414: device modal fades in/out, r=jryans Device modal can be closed with escape and outside click MozReview-Commit-ID: eyQRxC6TDv
devtools/client/responsive.html/components/device-modal.js
devtools/client/responsive.html/index.css
devtools/client/responsive.html/test/browser/browser_device_modal_exit.js
devtools/client/responsive.html/test/browser/browser_device_modal_submit.js
devtools/client/responsive.html/test/browser/head.js
--- a/devtools/client/responsive.html/components/device-modal.js
+++ b/devtools/client/responsive.html/components/device-modal.js
@@ -1,12 +1,14 @@
 /* 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 { DOM: dom, createClass, PropTypes, addons } =
   require("devtools/client/shared/vendor/react");
 const { getStr } = require("../utils/l10n");
 const Types = require("../types");
 
 module.exports = createClass({
@@ -20,30 +22,38 @@ module.exports = createClass({
   },
 
   mixins: [ addons.PureRenderMixin ],
 
   getInitialState() {
     return {};
   },
 
+  componentDidMount() {
+    window.addEventListener("keydown", this.onKeyDown, true);
+  },
+
   componentWillReceiveProps(nextProps) {
     let {
       devices,
     } = nextProps;
 
     for (let type of devices.types) {
       for (let device of devices[type]) {
         this.setState({
           [device.name]: device.displayed,
         });
       }
     }
   },
 
+  componentWillUnmount() {
+    window.removeEventListener("keydown", this.onKeyDown, true);
+  },
+
   onDeviceCheckboxClick({ target }) {
     this.setState({
       [target.value]: !this.state[target.value]
     });
   },
 
   onDeviceModalSubmit() {
     let {
@@ -73,80 +83,99 @@ module.exports = createClass({
         }
       }
     }
 
     onDeviceListUpdate(preferredDevices);
     onUpdateDeviceModalOpen(false);
   },
 
+  onKeyDown(event) {
+    if (!this.props.devices.isModalOpen) {
+      return;
+    }
+    // Escape keycode
+    if (event.keyCode === 27) {
+      let {
+        onUpdateDeviceModalOpen
+      } = this.props;
+      onUpdateDeviceModalOpen(false);
+    }
+  },
+
   render() {
     let {
       devices,
       onUpdateDeviceModalOpen,
     } = this.props;
 
-    let modalClass = "device-modal container";
-
-    if (!devices.isModalOpen) {
-      modalClass += " hidden";
-    }
-
     const sortedDevices = {};
     for (let type of devices.types) {
       sortedDevices[type] = Object.assign([], devices[type])
         .sort((a, b) => a.name.localeCompare(b.name));
     }
 
     return dom.div(
       {
-        className: modalClass,
+        id: "device-modal-wrapper",
+        className: this.props.devices.isModalOpen ? "opened" : "closed",
       },
-      dom.button({
-        id: "device-close-button",
-        className: "toolbar-button devtools-button",
-        onClick: () => onUpdateDeviceModalOpen(false),
-      }),
       dom.div(
         {
-          className: "device-modal-content",
+          className: "device-modal container",
         },
-        devices.types.map(type => {
-          return dom.div(
-            {
-              className: "device-type",
-              key: type,
-            },
-            dom.header(
+        dom.button({
+          id: "device-close-button",
+          className: "toolbar-button devtools-button",
+          onClick: () => onUpdateDeviceModalOpen(false),
+        }),
+        dom.div(
+          {
+            className: "device-modal-content",
+          },
+          devices.types.map(type => {
+            return dom.div(
               {
-                className: "device-header",
+                className: "device-type",
+                key: type,
               },
-              type
-            ),
-            sortedDevices[type].map(device => {
-              return dom.label(
+              dom.header(
                 {
-                  className: "device-label",
-                  key: device.name,
+                  className: "device-header",
                 },
-                dom.input({
-                  className: "device-input-checkbox",
-                  type: "checkbox",
-                  value: device.name,
-                  checked: this.state[device.name],
-                  onChange: this.onDeviceCheckboxClick,
-                }),
-                device.name
-              );
-            })
-          );
-        })
+                type
+              ),
+              sortedDevices[type].map(device => {
+                return dom.label(
+                  {
+                    className: "device-label",
+                    key: device.name,
+                  },
+                  dom.input({
+                    className: "device-input-checkbox",
+                    type: "checkbox",
+                    value: device.name,
+                    checked: this.state[device.name],
+                    onChange: this.onDeviceCheckboxClick,
+                  }),
+                  device.name
+                );
+              })
+            );
+          })
+        ),
+        dom.button(
+          {
+            id: "device-submit-button",
+            onClick: this.onDeviceModalSubmit,
+          },
+          getStr("responsive.done")
+        )
       ),
-      dom.button(
+      dom.div(
         {
-          id: "device-submit-button",
-          onClick: this.onDeviceModalSubmit,
-        },
-        getStr("responsive.done")
+          className: "modal-overlay",
+          onClick: () => onUpdateDeviceModalOpen(false),
+        }
       )
     );
   },
 });
--- a/devtools/client/responsive.html/index.css
+++ b/devtools/client/responsive.html/index.css
@@ -34,30 +34,29 @@
 }
 
 * {
   box-sizing: border-box;
 }
 
 #root,
 html, body {
+  height: 100%;
   margin: 0;
 }
 
 #app {
   /* Center the viewports container */
   display: flex;
   align-items: center;
   flex-direction: column;
-  height: 100vh;
-
-  /* Snap to the top of the app when there isn't enough vertical space anymore
-     to center the viewports (so we don't lose the global toolbar) */
-  position: sticky;
-  top: 0;
+  padding-top: 15px;
+  padding-bottom: 1%;
+  position: relative;
+  height: 100%;
 }
 
 /**
  * Common style for containers and toolbar buttons
  */
 
 .container {
   background-color: var(--theme-toolbar-background);
@@ -81,17 +80,17 @@ html, body {
 /**
  * Global Toolbar
  */
 
 #global-toolbar {
   color: var(--theme-body-color-alt);
   border-radius: 2px;
   box-shadow: var(--rdm-box-shadow);
-  margin: 10% 0 30px 0;
+  margin: 0 0 15px 0;
   padding: 4px 5px;
   display: inline-flex;
   -moz-user-select: none;
 }
 
 #global-toolbar > .title {
   border-right: 1px solid var(--theme-splitter-color);
   padding: 1px 6px 0 2px;
@@ -331,31 +330,76 @@ html, body {
 .viewport-dimension-separator {
   -moz-user-select: none;
 }
 
 /**
  * Device Modal
  */
 
+@keyframes fade-in-and-up {
+  0% {
+    opacity: 0;
+    transform: translateY(5px);
+  }
+  100% {
+    opacity: 1;
+    transform: translateY(0px);
+  }
+}
+
+@keyframes fade-down-and-out {
+  0% {
+    opacity: 1;
+    transform: translateY(0px);
+  }
+  100% {
+    opacity: 0;
+    transform: translateY(5px);
+    visibility: hidden;
+  }
+}
+
 .device-modal {
   border-radius: 2px;
   box-shadow: var(--rdm-box-shadow);
+  display: none;
   position: absolute;
   margin: auto;
   top: 0;
   bottom: 0;
   left: 0;
   right: 0;
   width: 642px;
   height: 612px;
+  z-index: 1;
+}
+
+/* Handles the opening/closing of the modal */
+#device-modal-wrapper.opened .device-modal {
+  animation: fade-in-and-up 0.3s ease;
+  animation-fill-mode: forwards;
+  display: block;
 }
 
-.device-modal.hidden {
-  display: none;
+#device-modal-wrapper.closed .device-modal {
+  animation: fade-down-and-out 0.3s ease;
+  animation-fill-mode: forwards;
+  display: block;
+}
+
+#device-modal-wrapper.opened .modal-overlay {
+  background-color: var(--theme-splitter-color);
+  position: absolute;
+  top: 0;
+  left: 0;
+  height: 100%;
+  width: 100%;
+  z-index: 0;
+  opacity: 0.5;
 }
 
 .device-modal-content {
   display: flex;
   flex-direction: column;
   flex-wrap: wrap;
   overflow: auto;
   height: 550px;
--- a/devtools/client/responsive.html/test/browser/browser_device_modal_exit.js
+++ b/devtools/client/responsive.html/test/browser/browser_device_modal_exit.js
@@ -5,17 +5,17 @@ http://creativecommons.org/publicdomain/
 
 // Test submitting display device changes on the device modal
 
 const TEST_URL = "data:text/html;charset=utf-8,";
 const Types = require("devtools/client/responsive.html/types");
 
 addRDMTask(TEST_URL, function* ({ ui }) {
   let { store, document } = ui.toolWindow;
-  let modal = document.querySelector(".device-modal");
+  let modal = document.querySelector("#device-modal-wrapper");
   let closeButton = document.querySelector("#device-close-button");
 
   // Wait until the viewport has been added and the device list has been loaded
   yield waitUntilState(store, state => state.viewports.length == 1
     && state.devices.listState == Types.deviceListState.LOADED);
 
   openDeviceModal(ui);
 
@@ -23,18 +23,18 @@ addRDMTask(TEST_URL, function* ({ ui }) 
 
   info("Check the first unchecked device and exit the modal.");
   let uncheckedCb = [...document.querySelectorAll(".device-input-checkbox")]
     .filter(cb => !cb.checked)[0];
   let value = uncheckedCb.value;
   uncheckedCb.click();
   closeButton.click();
 
-  ok(modal.classList.contains("hidden"),
-    "The device modal is hidden on exit.");
+  ok(modal.classList.contains("closed") && !modal.classList.contains("opened"),
+    "The device modal is closed on exit.");
 
   info("Check that the device list remains unchanged after exitting.");
   let preferredDevicesAfter = _loadPreferredDevices();
 
   is(preferredDevicesBefore.added.size, preferredDevicesAfter.added.size,
     "Got expected number of added devices.");
 
   is(preferredDevicesBefore.removed.size, preferredDevicesAfter.removed.size,
--- a/devtools/client/responsive.html/test/browser/browser_device_modal_submit.js
+++ b/devtools/client/responsive.html/test/browser/browser_device_modal_submit.js
@@ -18,17 +18,17 @@ const addedDevice = {
   "featured": true,
 };
 
 const TEST_URL = "data:text/html;charset=utf-8,";
 const Types = require("devtools/client/responsive.html/types");
 
 addRDMTask(TEST_URL, function* ({ ui }) {
   let { store, document } = ui.toolWindow;
-  let modal = document.querySelector(".device-modal");
+  let modal = document.querySelector("#device-modal-wrapper");
   let select = document.querySelector(".viewport-device-selector");
   let submitButton = document.querySelector("#device-submit-button");
 
   // Wait until the viewport has been added and the device list has been loaded
   yield waitUntilState(store, state => state.viewports.length == 1
     && state.devices.listState == Types.deviceListState.LOADED);
 
   openDeviceModal(ui);
@@ -56,18 +56,18 @@ addRDMTask(TEST_URL, function* ({ ui }) 
   // Tests where the user adds a non-featured device
   info("Check the first unchecked device and submit new device list.");
   let uncheckedCb = [...document.querySelectorAll(".device-input-checkbox")]
     .filter(cb => !cb.checked)[0];
   let value = uncheckedCb.value;
   uncheckedCb.click();
   submitButton.click();
 
-  ok(modal.classList.contains("hidden"),
-    "The device modal is hidden on submit.");
+  ok(modal.classList.contains("closed") && !modal.classList.contains("opened"),
+    "The device modal is closed on submit.");
 
   info("Checking that the new device is added to the user preference list.");
   let preferredDevices = _loadPreferredDevices();
   ok(preferredDevices.added.has(value), value + " in user added list.");
 
   info("Checking new device is added to the device selector.");
   let options = [...select.options];
   is(options.length - 2, featuredCount + 1,
--- a/devtools/client/responsive.html/test/browser/head.js
+++ b/devtools/client/responsive.html/test/browser/head.js
@@ -130,32 +130,32 @@ var setViewportSize = Task.async(functio
     ui.setViewportSize(width, height);
     yield resized;
   }
 });
 
 function openDeviceModal(ui) {
   let { document } = ui.toolWindow;
   let select = document.querySelector(".viewport-device-selector");
-  let modal = document.querySelector(".device-modal");
+  let modal = document.querySelector("#device-modal-wrapper");
   let editDeviceOption = [...select.options].filter(o => {
     return o.value === OPEN_DEVICE_MODAL_VALUE;
   })[0];
 
   info("Checking initial device modal state");
-  ok(modal.classList.contains("hidden"),
-    "The device modal is hidden by default.");
+  ok(modal.classList.contains("closed") && !modal.classList.contains("opened"),
+    "The device modal is closed by default.");
 
   info("Opening device modal through device selector.");
   EventUtils.synthesizeMouseAtCenter(select, {type: "mousedown"},
     ui.toolWindow);
   EventUtils.synthesizeMouseAtCenter(editDeviceOption, {type: "mouseup"},
     ui.toolWindow);
 
-  ok(!modal.classList.contains("hidden"),
+  ok(modal.classList.contains("opened") && !modal.classList.contains("closed"),
     "The device modal is displayed.");
 }
 
 function getSessionHistory(browser) {
   return ContentTask.spawn(browser, {}, function* () {
     /* eslint-disable no-undef */
     let { interfaces: Ci } = Components;
     let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);