Merge inbound to mozilla-central. a=merge
authorMargareta Eliza Balazs <ebalazs@mozilla.com>
Thu, 16 Aug 2018 12:24:26 +0300
changeset 431877 161817e6d127e4a1fdc0ba9f041c709e09096dcb
parent 431672 9c569226e852c2df21769e2393a7bb02fc8a23cd (current diff)
parent 431876 57e568a7f9334e4b0610199a3fec7fce79e786bb (diff)
child 431878 4248cea4f9a1e5cb64c0d121ff248e11e003e9e2
child 431885 52c33d6997c37f71820c5e454530dea0982bbc0c
child 431949 97a37dbe32c9b88c54f006ada7d7e1ac79383c2a
push id34451
push userebalazs@mozilla.com
push dateThu, 16 Aug 2018 09:25:15 +0000
treeherdermozilla-central@161817e6d127 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone63.0a1
first release with
nightly linux32
161817e6d127 / 63.0a1 / 20180816100035 / files
nightly linux64
161817e6d127 / 63.0a1 / 20180816100035 / files
nightly mac
161817e6d127 / 63.0a1 / 20180816100035 / files
nightly win32
161817e6d127 / 63.0a1 / 20180816100035 / files
nightly win64
161817e6d127 / 63.0a1 / 20180816100035 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge inbound to mozilla-central. a=merge
browser/actors/moz.build
browser/components/nsBrowserGlue.js
browser/extensions/formautofill/bootstrap.js
browser/extensions/formautofill/install.rdf.in
browser/extensions/formautofill/skin/shared/autocomplete-item.css
browser/extensions/formautofill/skin/shared/editDialog.css
browser/extensions/fxmonitor/manifest.json
browser/extensions/fxmonitor/moz.build
browser/modules/ContentLinkHandler.jsm
build/telemetry-schema.json
devtools/client/netmonitor/src/utils/menu.js
devtools/client/responsive.html/app.js
devtools/client/responsive.html/components/DevicePixelRatioSelector.js
devtools/client/responsive.html/components/GlobalToolbar.js
devtools/client/responsive.html/components/ReloadConditions.js
devtools/client/responsive.html/components/ToggleMenu.js
devtools/client/responsive.html/components/Viewport.js
devtools/client/responsive.html/components/ViewportToolbar.js
devtools/client/responsive.html/responsive-ua.css
devtools/client/responsive.html/utils/css.js
devtools/client/shared/components/throttling/NetworkThrottlingSelector.js
dom/base/nsContentUtils.cpp
dom/base/nsContentUtils.h
dom/base/nsIDOMDOMCursor.idl
dom/base/nsIDocument.h
layout/reftests/flexbox/flexbox-justify-content-horizrev-001-ref.xhtml
layout/reftests/flexbox/flexbox-justify-content-horizrev-001.xhtml
layout/reftests/flexbox/flexbox-justify-content-vertrev-001-ref.xhtml
layout/reftests/flexbox/flexbox-justify-content-vertrev-001.xhtml
mobile/android/tests/browser/robocop/robocop_autophone2.ini
modules/libpref/init/all.js
netwerk/protocol/http/HttpChannelChild.cpp
netwerk/protocol/http/HttpChannelChild.h
netwerk/protocol/http/HttpChannelParent.cpp
netwerk/protocol/http/HttpChannelParent.h
netwerk/protocol/http/PHttpChannel.ipdl
netwerk/protocol/http/nsHttpHandler.cpp
netwerk/protocol/http/nsHttpHandler.h
testing/mozbase/mozrunner/mozrunner/devices/autophone.py
testing/web-platform/meta/2dcontext/imagebitmap/createImageBitmap-origin.sub.html.ini
testing/web-platform/meta/html/semantics/embedded-content/the-video-element/video_initially_paused.html.ini
testing/web-platform/tests/animation-worklet/interfaces.any.js
testing/web-platform/tests/background-fetch/resources/sw.js
testing/web-platform/tests/budget-api/interfaces.any.js
testing/web-platform/tests/compat/interfaces.any.js
testing/web-platform/tests/fullscreen/interfaces.html
testing/web-platform/tests/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/004-1.html
testing/web-platform/tests/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/007.html
testing/web-platform/tests/resources/test/tests/functional/worker-dedicated.html
testing/web-platform/tests/storage/interfaces.https.html
testing/web-platform/tests/storage/interfaces.https.worker.js
testing/web-platform/tests/tools/wptrunner/wptrunner/reduce.py
testing/web-platform/tests/trusted-types/HTMLAnchorElement-href.tentative.html
testing/web-platform/tests/trusted-types/HTMLAreaElement-href.tentative.html
testing/web-platform/tests/trusted-types/HTMLBaseElement-href.tentative.html
testing/web-platform/tests/trusted-types/HTMLIFrameElement-src.tentative.html
testing/web-platform/tests/trusted-types/HTMLImageElement-src.tentative.html
testing/web-platform/tests/trusted-types/HTMLLinkElement-href.tentative.html
testing/web-platform/tests/trusted-types/HTMLMediaElement-src.tentative.html
testing/web-platform/tests/trusted-types/HTMLObjectElement.tentative.html
testing/web-platform/tests/trusted-types/HTMLSourceElement-src.tentative.html
testing/web-platform/tests/trusted-types/block-string-assignment-to-HTMLAnchorElement-href.tentative.html
testing/web-platform/tests/trusted-types/block-string-assignment-to-HTMLAreaElement-href.tentative.html
testing/web-platform/tests/trusted-types/block-string-assignment-to-HTMLBaseElement-href.tentative.html
testing/web-platform/tests/trusted-types/block-string-assignment-to-HTMLIFrameElement-src.tentative.html
testing/web-platform/tests/trusted-types/block-string-assignment-to-HTMLImageElement-src.tentative.html
testing/web-platform/tests/trusted-types/block-string-assignment-to-HTMLLinkElement-href.tentative.html
testing/web-platform/tests/trusted-types/block-string-assignment-to-HTMLMediaElement-src.tentative.html
testing/web-platform/tests/trusted-types/block-string-assignment-to-HTMLObjectElement.tentative.html
testing/web-platform/tests/trusted-types/block-string-assignment-to-HTMLSourceElement-src.tentative.html
testing/web-platform/tests/trusted-types/block-string-assignment-to-embed-src.tentative.html
testing/web-platform/tests/trusted-types/block-string-assignment-to-frame-src.tentative.html
testing/web-platform/tests/trusted-types/block-string-assignment-to-input-src.tentative.html
testing/web-platform/tests/trusted-types/block-string-assignment-to-script-src.tentative.html
testing/web-platform/tests/trusted-types/block-string-assignment-to-track-src.tentative.html
testing/web-platform/tests/trusted-types/embed-src.tentative.html
testing/web-platform/tests/trusted-types/frame-src.tentative.html
testing/web-platform/tests/trusted-types/input-src.tentative.html
testing/web-platform/tests/trusted-types/script-src.tentative.html
testing/web-platform/tests/trusted-types/track-src.tentative.html
testing/web-platform/tests/url/interfaces.any.js
testing/web-platform/tests/xhr/interfaces.html
third_party/python/voluptuous/README.rst
toolkit/components/telemetry/Histograms.json
xpcom/ds/nsGkAtomList.h
xpcom/ds/nsStaticAtom.h
xpcom/io/nsDirectoryServiceAtomList.h
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1474,16 +1474,27 @@ version = "0.1.40"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "num-integer 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)",
  "num-iter 0.1.34 (registry+https://github.com/rust-lang/crates.io-index)",
  "num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
+name = "num-derive"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "num-traits 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "proc-macro2 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)",
+ "quote 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "syn 0.14.6 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
 name = "num-integer"
 version = "0.1.35"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -2024,16 +2035,17 @@ dependencies = [
  "lazy_static 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "log 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "malloc_size_of 0.0.1",
  "malloc_size_of_derive 0.0.1",
  "matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
  "new-ordered-float 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "new_debug_unreachable 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "nsstring 0.1.0",
+ "num-derive 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "num-integer 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)",
  "num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)",
  "num_cpus 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "owning_ref 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "parking_lot 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "precomputed-hash 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "rayon 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "regex 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -2738,16 +2750,17 @@ dependencies = [
 "checksum msdos_time 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "65ba9d75bcea84e07812618fedf284a64776c2f2ea0cad6bca7f69739695a958"
 "checksum net2 0.2.31 (registry+https://github.com/rust-lang/crates.io-index)" = "3a80f842784ef6c9a958b68b7516bc7e35883c614004dd94959a4dca1b716c09"
 "checksum new-ordered-float 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8ccbebba6fb53a6d2bdcfaf79cb339bc136dee3bfff54dc337a334bafe36476a"
 "checksum new_debug_unreachable 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0cdc457076c78ab54d5e0d6fa7c47981757f1e34dc39ff92787f217dede586c4"
 "checksum nodrop 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "9a2228dca57108069a5262f2ed8bd2e82496d2e074a06d1ccc7ce1687b6ae0a2"
 "checksum nom 1.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "a5b8c256fd9471521bcb84c3cdba98921497f1a331cbc15b8030fc63b82050ce"
 "checksum nom 3.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05aec50c70fd288702bcd93284a8444607f3292dbdf2a30de5ea5dcdbe72287b"
 "checksum num 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "a311b77ebdc5dd4cf6449d81e4135d9f0e3b153839ac90e648a8ef538f923525"
+"checksum num-derive 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0d2c31b75c36a993d30c7a13d70513cb93f02acafdd5b7ba250f9b0e18615de7"
 "checksum num-integer 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)" = "d1452e8b06e448a07f0e6ebb0bb1d92b8890eea63288c0b627331d53514d0fba"
 "checksum num-iter 0.1.34 (registry+https://github.com/rust-lang/crates.io-index)" = "7485fcc84f85b4ecd0ea527b14189281cf27d60e583ae65ebc9c088b13dffe01"
 "checksum num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31"
 "checksum num-traits 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e7de20f146db9d920c45ee8ed8f71681fd9ade71909b48c3acbd766aa504cf10"
 "checksum num_cpus 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "514f0d73e64be53ff320680ca671b64fe3fb91da01e1ae2ddc99eb51d453b20d"
 "checksum ordermap 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "a86ed3f5f244b372d6b1a00b72ef7f8876d0bc6a78a4c9985c53614041512063"
 "checksum owning_ref 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "cdf84f41639e037b484f93433aa3897863b561ed65c6e59c7073d7c561710f37"
 "checksum parking_lot 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)" = "69376b761943787ebd5cc85a5bc95958651a22609c5c1c2b65de21786baec72b"
--- a/Pipfile
+++ b/Pipfile
@@ -11,9 +11,9 @@ blessings = "==1.7"
 jsmin = "==2.1.0"
 json-e = "==2.5.0"
 pipenv = "==2018.5.18"
 pytest = "==3.6.2"
 python-hglib = "==2.4"
 requests = "==2.9.1"
 six = "==1.10.0"
 virtualenv = "==15.2.0"
-voluptuous = "==0.10.5"
+voluptuous = "==0.11.5"
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,12 +1,12 @@
 {
     "_meta": {
         "hash": {
-            "sha256": "609a35f65e9a4c07e0e1473ec982c6b5028622e9a795b6cfb8555ad8574804f3"
+            "sha256": "f718e0b6ec2c030d4becf157f8ca0fd1b2f32ca277d5d3d2407a2dee33119441"
         },
         "pipfile-spec": 6,
         "requires": {},
         "sources": [
             {
                 "name": "pypi",
                 "url": "https://pypi.org/simple",
                 "verify_ssl": true
@@ -64,21 +64,21 @@
             "hashes": [
                 "sha256:f9114a25ed4b575395fbb2daa1183c5b781a647b387fdf28596220bb114673e8"
             ],
             "index": "pypi",
             "version": "==2.5.0"
         },
         "more-itertools": {
             "hashes": [
-                "sha256:2b6b9893337bfd9166bee6a62c2b0c9fe7735dcf85948b387ec8cba30e85d8e8",
-                "sha256:6703844a52d3588f951883005efcf555e49566a48afd4db4e965d69b883980d3",
-                "sha256:a18d870ef2ffca2b8463c0070ad17b5978056f403fb64e3f15fe62a52db21cc0"
+                "sha256:c187a73da93e7a8acc0001572aebc7e3c69daf7bf6881a2cea10650bd4420092",
+                "sha256:c476b5d3a34e12d40130bc2f935028b5f636df8f372dc2c1c01dc19681b2039e",
+                "sha256:fcbfeaea0be121980e15bc97b3817b5202ca73d0eae185b4550cbfce2a3ebb3d"
             ],
-            "version": "==4.2.0"
+            "version": "==4.3.0"
         },
         "pipenv": {
             "hashes": [
                 "sha256:04b9a8b02a3ff12a5502b335850cfdb192adcfd1d6bbdb7a7c47cae9ab9ddece",
                 "sha256:e96d5bfa6822a17b2200d455aa5f9002c14361c50df1b1e51921479d7c09e741"
             ],
             "index": "pypi",
             "version": "==2018.5.18"
@@ -141,16 +141,17 @@
             "hashes": [
                 "sha256:4507071d81013fd03ea9930ec26bc8648b997927a11fa80e8ee81198b57e0ac7",
                 "sha256:b5cfe535d14dc68dfc1d1bb4ac1209ea28235b91156e2bba8e250d291c3fb4f8"
             ],
             "version": "==0.3.0"
         },
         "voluptuous": {
             "hashes": [
-                "sha256:7a7466f8dc3666a292d186d1d871a47bf2120836ccb900d5ba904674957a2396"
+                "sha256:303542b3fc07fb52ec3d7a1c614b329cdbee13a9d681935353d8ea56a7bfa9f1",
+                "sha256:567a56286ef82a9d7ae0628c5842f65f516abcb496e74f3f59f1d7b28df314ef"
             ],
             "index": "pypi",
-            "version": "==0.10.5"
+            "version": "==0.11.5"
         }
     },
     "develop": {}
 }
--- a/accessible/base/AccEvent.cpp
+++ b/accessible/base/AccEvent.cpp
@@ -271,11 +271,18 @@ a11y::MakeXPCEvent(AccEvent* aEvent)
     xpEvent = new xpcAccObjectAttributeChangedEvent(type,
                                                     ToXPC(acc),
                                                     ToXPCDocument(doc), node,
                                                     fromUser,
                                                     attribute);
     return xpEvent.forget();
   }
 
+  if (eventGroup & (1 << AccEvent::eScrollingEvent)) {
+    AccScrollingEvent* sa = downcast_accEvent(aEvent);
+    xpEvent = new xpcAccScrollingEvent(type, ToXPC(acc), ToXPCDocument(doc), node,
+                                       fromUser, sa->ScrollX(), sa->ScrollY(),
+                                       sa->MaxScrollX(), sa->MaxScrollY());
+  }
+
   xpEvent = new xpcAccEvent(type, ToXPC(acc), ToXPCDocument(doc), node, fromUser);
   return xpEvent.forget();
   }
--- a/accessible/base/AccEvent.h
+++ b/accessible/base/AccEvent.h
@@ -99,17 +99,18 @@ public:
     eReorderEvent,
     eHideEvent,
     eShowEvent,
     eCaretMoveEvent,
     eTextSelChangeEvent,
     eSelectionChangeEvent,
     eTableChangeEvent,
     eVirtualCursorChangeEvent,
-    eObjectAttrChangedEvent
+    eObjectAttrChangedEvent,
+    eScrollingEvent,
   };
 
   static const EventGroup kEventGroup = eGenericEvent;
   virtual unsigned int GetEventGroups() const
   {
     return 1U << eGenericEvent;
   }
 
@@ -543,16 +544,56 @@ public:
 
 private:
   RefPtr<nsAtom> mAttribute;
 
   virtual ~AccObjectAttrChangedEvent() { }
 };
 
 /**
+ * Accessible scroll event.
+ */
+class AccScrollingEvent : public AccEvent
+{
+public:
+  AccScrollingEvent(uint32_t aEventType, Accessible* aAccessible,
+                    uint32_t aScrollX, uint32_t aScrollY,
+                    uint32_t aMaxScrollX, uint32_t aMaxScrollY) :
+    AccEvent(aEventType, aAccessible),
+    mScrollX(aScrollX),
+    mScrollY(aScrollY),
+    mMaxScrollX(aMaxScrollX),
+    mMaxScrollY(aMaxScrollY) { }
+
+  virtual ~AccScrollingEvent() { }
+
+  // AccEvent
+  static const EventGroup kEventGroup = eScrollingEvent;
+  virtual unsigned int GetEventGroups() const override
+  {
+    return AccEvent::GetEventGroups() | (1U << eScrollingEvent);
+  }
+
+  // The X scrolling offset of the container when the event was fired.
+  uint32_t ScrollX() { return mScrollX; }
+  // The Y scrolling offset of the container when the event was fired.
+  uint32_t ScrollY() { return mScrollY; }
+  // The max X offset of the container.
+  uint32_t MaxScrollX() { return mMaxScrollX; }
+  // The max Y offset of the container.
+  uint32_t MaxScrollY() { return mMaxScrollY; }
+
+private:
+  uint32_t mScrollX;
+  uint32_t mScrollY;
+  uint32_t mMaxScrollX;
+  uint32_t mMaxScrollY;
+};
+
+/**
  * Downcast the generic accessible event object to derived type.
  */
 class downcast_accEvent
 {
 public:
   explicit downcast_accEvent(AccEvent* e) : mRawPtr(e) { }
 
   template<class Destination>
--- a/accessible/base/Platform.h
+++ b/accessible/base/Platform.h
@@ -108,13 +108,17 @@ void ProxyVirtualCursorChangeEvent(Proxy
                                    ProxyAccessible* aOldPosition,
                                    int32_t aOldStartOffset,
                                    int32_t aOldEndOffset,
                                    ProxyAccessible* aNewPosition,
                                    int32_t aNewStartOffset,
                                    int32_t aNewEndOffset,
                                    int16_t aReason, int16_t aBoundaryType,
                                    bool aFromUser);
+
+void ProxyScrollingEvent(ProxyAccessible* aTarget,
+                         uint32_t aScrollX, uint32_t aScrollY,
+                         uint32_t aMaxScrollX, uint32_t aMaxScrollY);
 #endif
 } // namespace a11y
 } // namespace mozilla
 
 #endif // mozilla_a11y_Platform_h
--- a/accessible/base/nsAccessibilityService.h
+++ b/accessible/base/nsAccessibilityService.h
@@ -468,11 +468,12 @@ static const char kEventTypeNames[][40] 
   "hypertext link activated",                // EVENT_HYPERTEXT_LINK_ACTIVATED
   "hypertext link selected",                 // EVENT_HYPERTEXT_LINK_SELECTED
   "hyperlink start index changed",           // EVENT_HYPERLINK_START_INDEX_CHANGED
   "hypertext changed",                       // EVENT_HYPERTEXT_CHANGED
   "hypertext links count changed",           // EVENT_HYPERTEXT_NLINKS_CHANGED
   "object attribute changed",                // EVENT_OBJECT_ATTRIBUTE_CHANGED
   "virtual cursor changed",                   // EVENT_VIRTUALCURSOR_CHANGED
   "text value change",                       // EVENT_TEXT_VALUE_CHANGE
+  "scrolling",                               // EVENT_SCROLLING
 };
 
 #endif
--- a/accessible/generic/Accessible.cpp
+++ b/accessible/generic/Accessible.cpp
@@ -947,16 +947,24 @@ Accessible::HandleAccEvent(AccEvent* aEv
           break;
         }
 #if defined(XP_WIN)
         case nsIAccessibleEvent::EVENT_FOCUS: {
           ipcDoc->SendFocusEvent(id);
           break;
         }
 #endif
+        case nsIAccessibleEvent::EVENT_SCROLLING_END:
+        case nsIAccessibleEvent::EVENT_SCROLLING: {
+          AccScrollingEvent* scrollingEvent = downcast_accEvent(aEvent);
+          ipcDoc->SendScrollingEvent(id, aEvent->GetEventType(),
+            scrollingEvent->ScrollX(), scrollingEvent->ScrollY(),
+            scrollingEvent->MaxScrollX(), scrollingEvent->MaxScrollY());
+          break;
+        }
         default:
           ipcDoc->SendEvent(id, aEvent->GetEventType());
       }
     }
   }
 
   if (nsCoreUtils::AccEventObserversExist()) {
     nsCoreUtils::DispatchAccEvent(MakeXPCEvent(aEvent));
--- a/accessible/generic/DocAccessible.cpp
+++ b/accessible/generic/DocAccessible.cpp
@@ -605,57 +605,57 @@ DocAccessible::RemoveEventListeners()
   return NS_OK;
 }
 
 void
 DocAccessible::ScrollTimerCallback(nsITimer* aTimer, void* aClosure)
 {
   DocAccessible* docAcc = reinterpret_cast<DocAccessible*>(aClosure);
 
-  if (docAcc && docAcc->mScrollPositionChangedTicks &&
-      ++docAcc->mScrollPositionChangedTicks > 2) {
-    // Whenever scroll position changes, mScrollPositionChangeTicks gets reset to 1
-    // We only want to fire accessibilty scroll event when scrolling stops or pauses
-    // Therefore, we wait for no scroll events to occur between 2 ticks of this timer
-    // That indicates a pause in scrolling, so we fire the accessibilty scroll event
-    nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_SCROLLING_END, docAcc);
+  if (docAcc) {
+    docAcc->DispatchScrollingEvent(nsIAccessibleEvent::EVENT_SCROLLING_END);
 
-    docAcc->mScrollPositionChangedTicks = 0;
     if (docAcc->mScrollWatchTimer) {
-      docAcc->mScrollWatchTimer->Cancel();
       docAcc->mScrollWatchTimer = nullptr;
       NS_RELEASE(docAcc); // Release kung fu death grip
     }
   }
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 // nsIScrollPositionListener
 
 void
 DocAccessible::ScrollPositionDidChange(nscoord aX, nscoord aY)
 {
-  // Start new timer, if the timer cycles at least 1 full cycle without more scroll position changes,
-  // then the ::Notify() method will fire the accessibility event for scroll position changes
-  const uint32_t kScrollPosCheckWait = 50;
+  const uint32_t kScrollEventInterval = 100;
+  TimeStamp timestamp = TimeStamp::Now();
+  if (mLastScrollingDispatch.IsNull() ||
+      (timestamp - mLastScrollingDispatch).ToMilliseconds() >= kScrollEventInterval) {
+    DispatchScrollingEvent(nsIAccessibleEvent::EVENT_SCROLLING);
+    mLastScrollingDispatch = timestamp;
+  }
+
+  // If timer callback is still pending, push it 100ms into the future.
+  // When scrolling ends and we don't fire this callback anymore, the
+  // timer callback will fire and dispatch an EVENT_SCROLLING_END.
   if (mScrollWatchTimer) {
-    mScrollWatchTimer->SetDelay(kScrollPosCheckWait);  // Create new timer, to avoid leaks
+    mScrollWatchTimer->SetDelay(kScrollEventInterval);
   }
   else {
     NS_NewTimerWithFuncCallback(getter_AddRefs(mScrollWatchTimer),
                                 ScrollTimerCallback,
                                 this,
-                                kScrollPosCheckWait,
-                                nsITimer::TYPE_REPEATING_SLACK,
+                                kScrollEventInterval,
+                                nsITimer::TYPE_ONE_SHOT,
                                 "a11y::DocAccessible::ScrollPositionDidChange");
     if (mScrollWatchTimer) {
       NS_ADDREF_THIS(); // Kung fu death grip
     }
   }
-  mScrollPositionChangedTicks = 1;
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 // nsIObserver
 
 NS_IMETHODIMP
 DocAccessible::Observe(nsISupports* aSubject, const char* aTopic,
                        const char16_t* aData)
@@ -2438,8 +2438,29 @@ DocAccessible::IsLoadEventTarget() const
     // while there's no parent document yet).
     DocAccessible* parentDoc = ParentDocument();
     return parentDoc && parentDoc->HasLoadState(eCompletelyLoaded);
   }
 
   // It's content (not chrome) root document.
   return (treeItem->ItemType() == nsIDocShellTreeItem::typeContent);
 }
+
+void
+DocAccessible::DispatchScrollingEvent(uint32_t aEventType)
+{
+  nsIScrollableFrame* sf = mPresShell->GetRootScrollFrameAsScrollable();
+
+  int32_t appUnitsPerDevPixel = mPresShell->GetPresContext()->AppUnitsPerDevPixel();
+  LayoutDevicePoint scrollPoint = LayoutDevicePoint::FromAppUnits(
+    sf->GetScrollPosition(), appUnitsPerDevPixel) * mPresShell->GetResolution();
+
+  LayoutDeviceRect scrollRange = LayoutDeviceRect::FromAppUnits(
+    sf->GetScrollRange(), appUnitsPerDevPixel);
+  scrollRange.ScaleRoundOut(mPresShell->GetResolution());
+
+  RefPtr<AccEvent> event = new AccScrollingEvent(aEventType, this,
+                                                 scrollPoint.x, scrollPoint.y,
+                                                 scrollRange.width,
+                                                 scrollRange.height);
+
+  nsEventShell::FireEvent(event);
+}
--- a/accessible/generic/DocAccessible.h
+++ b/accessible/generic/DocAccessible.h
@@ -577,16 +577,18 @@ protected:
   /**
    * Used to fire scrolling end event after page scroll.
    *
    * @param aTimer    [in] the timer object
    * @param aClosure  [in] the document accessible where scrolling happens
    */
   static void ScrollTimerCallback(nsITimer* aTimer, void* aClosure);
 
+  void DispatchScrollingEvent(uint32_t aEventType);
+
 protected:
 
   /**
    * State and property flags, kept by mDocFlags.
    */
   enum {
     // Whether scroll listeners were added.
     eScrollInitialized = 1 << 0,
@@ -598,18 +600,19 @@ protected:
   /**
    * Cache of accessibles within this document accessible.
    */
   AccessibleHashtable mAccessibleCache;
   nsDataHashtable<nsPtrHashKey<const nsINode>, Accessible*>
     mNodeToAccessibleMap;
 
   nsIDocument* mDocumentNode;
-    nsCOMPtr<nsITimer> mScrollWatchTimer;
-    uint16_t mScrollPositionChangedTicks; // Used for tracking scroll events
+  nsCOMPtr<nsITimer> mScrollWatchTimer;
+  uint16_t mScrollPositionChangedTicks; // Used for tracking scroll events
+  TimeStamp mLastScrollingDispatch;
 
   /**
    * Bit mask of document load states (@see LoadState).
    */
   uint32_t mLoadState : 3;
 
   /**
    * Bit mask of other states and props.
--- a/accessible/interfaces/moz.build
+++ b/accessible/interfaces/moz.build
@@ -18,16 +18,17 @@ XPIDL_SOURCES += [
     'nsIAccessibleHideEvent.idl',
     'nsIAccessibleHyperLink.idl',
     'nsIAccessibleHyperText.idl',
     'nsIAccessibleImage.idl',
     'nsIAccessibleObjectAttributeChangedEvent.idl',
     'nsIAccessiblePivot.idl',
     'nsIAccessibleRelation.idl',
     'nsIAccessibleRole.idl',
+    'nsIAccessibleScrollingEvent.idl',
     'nsIAccessibleSelectable.idl',
     'nsIAccessibleStateChangeEvent.idl',
     'nsIAccessibleStates.idl',
     'nsIAccessibleTable.idl',
     'nsIAccessibleTableChangeEvent.idl',
     'nsIAccessibleText.idl',
     'nsIAccessibleTextChangeEvent.idl',
     'nsIAccessibleTextRange.idl',
--- a/accessible/interfaces/nsIAccessibleEvent.idl
+++ b/accessible/interfaces/nsIAccessibleEvent.idl
@@ -414,19 +414,24 @@ interface nsIAccessibleEvent : nsISuppor
   const unsigned long EVENT_VIRTUALCURSOR_CHANGED = 0x0056;
 
   /**
    * An object's text Value has changed.
    */
   const unsigned long EVENT_TEXT_VALUE_CHANGE = 0x0057;
 
   /**
+   * An accessible's viewport is scrolling.
+   */
+  const unsigned long EVENT_SCROLLING = 0x0058;
+
+  /**
    * Help make sure event map does not get out-of-line.
    */
-  const unsigned long EVENT_LAST_ENTRY = 0x0058;
+  const unsigned long EVENT_LAST_ENTRY = 0x0059;
 
   /**
    * The type of event, based on the enumerated event values
    * defined in this interface.
    */
   readonly attribute unsigned long eventType;
   
   /**
new file mode 100644
--- /dev/null
+++ b/accessible/interfaces/nsIAccessibleScrollingEvent.idl
@@ -0,0 +1,34 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+/* 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/. */
+
+#include "nsIAccessibleEvent.idl"
+
+/*
+ * An interface scroll events.
+ * Stores new scroll position and max scroll position.
+ */
+[scriptable, builtinclass, uuid(f75f0b32-5342-4d60-b1a5-b7bd6888eef5)]
+interface nsIAccessibleScrollingEvent : nsIAccessibleEvent
+{
+  /**
+   * New X scroll position within a scrollable container in device pixels.
+   */
+  readonly attribute unsigned long scrollX;
+
+  /**
+   * New Y scroll position within a scrollable container in device pixels.
+   */
+  readonly attribute unsigned long scrollY;
+
+  /**
+  * Max X scroll position within a scrollable container in device pixels.
+   */
+  readonly attribute unsigned long maxScrollX;
+
+  /**
+   * Max Y scroll position within a scrollable container in device pixels.
+   */
+  readonly attribute unsigned long maxScrollY;
+};
--- a/accessible/ipc/DocAccessibleParent.cpp
+++ b/accessible/ipc/DocAccessibleParent.cpp
@@ -399,39 +399,83 @@ DocAccessibleParent::RecvVirtualCursorCh
                                                   const int16_t& aReason,
                                                   const int16_t& aBoundaryType,
                                                   const bool& aFromUser)
 {
   ProxyAccessible* target = GetAccessible(aID);
   ProxyAccessible* oldPosition = GetAccessible(aOldPositionID);
   ProxyAccessible* newPosition = GetAccessible(aNewPositionID);
 
+  if (!target) {
+    NS_ERROR("no proxy for event!");
+    return IPC_OK();
+  }
+
 #if defined(ANDROID)
   ProxyVirtualCursorChangeEvent(target,
                                 oldPosition, aOldStartOffset, aOldEndOffset,
                                 newPosition, aNewStartOffset, aNewEndOffset,
                                 aReason, aBoundaryType, aFromUser);
 #endif
 
+  if (!nsCoreUtils::AccEventObserversExist()) {
+    return IPC_OK();
+  }
+
   xpcAccessibleDocument* doc = GetAccService()->GetXPCDocument(this);
   RefPtr<xpcAccVirtualCursorChangeEvent> event =
     new xpcAccVirtualCursorChangeEvent(nsIAccessibleEvent::EVENT_VIRTUALCURSOR_CHANGED,
                                        GetXPCAccessible(target), doc,
                                        nullptr, aFromUser,
                                        GetXPCAccessible(oldPosition),
                                        aOldStartOffset, aOldEndOffset,
                                        GetXPCAccessible(newPosition),
                                        aNewStartOffset, aNewEndOffset,
                                        aBoundaryType, aReason);
   nsCoreUtils::DispatchAccEvent(std::move(event));
 
   return IPC_OK();
 }
 
 mozilla::ipc::IPCResult
+DocAccessibleParent::RecvScrollingEvent(const uint64_t& aID,
+                                        const uint64_t& aType,
+                                        const uint32_t& aScrollX,
+                                        const uint32_t& aScrollY,
+                                        const uint32_t& aMaxScrollX,
+                                        const uint32_t& aMaxScrollY)
+{
+  ProxyAccessible* target = GetAccessible(aID);
+
+  if (!target) {
+    NS_ERROR("no proxy for event!");
+    return IPC_OK();
+  }
+
+#if defined(ANDROID)
+  ProxyScrollingEvent(target, aScrollX, aScrollY, aMaxScrollX, aMaxScrollY);
+#endif
+
+  if (!nsCoreUtils::AccEventObserversExist()) {
+    return IPC_OK();
+  }
+
+  xpcAccessibleGeneric* xpcAcc = GetXPCAccessible(target);
+  xpcAccessibleDocument* doc = GetAccService()->GetXPCDocument(this);
+  nsINode* node = nullptr;
+  bool fromUser = true; // XXX: Determine if this was from user input.
+  RefPtr<xpcAccScrollingEvent> event =
+    new xpcAccScrollingEvent(aType, xpcAcc, doc, node, fromUser, aScrollX,
+                             aScrollY, aMaxScrollX, aMaxScrollY);
+  nsCoreUtils::DispatchAccEvent(std::move(event));
+
+  return IPC_OK();
+}
+
+mozilla::ipc::IPCResult
 DocAccessibleParent::RecvRoleChangedEvent(const a11y::role& aRole)
 {
   if (mShutdown) {
     return IPC_OK();
   }
 
   mRole = aRole;
   return IPC_OK();
--- a/accessible/ipc/DocAccessibleParent.h
+++ b/accessible/ipc/DocAccessibleParent.h
@@ -112,16 +112,23 @@ public:
                                                                const int32_t& aOldEndOffset,
                                                                const uint64_t& aNewPositionID,
                                                                const int32_t& aNewStartOffset,
                                                                const int32_t& aNewEndOffset,
                                                                const int16_t& aReason,
                                                                const int16_t& aBoundaryType,
                                                                const bool& aFromUser) override;
 
+  virtual mozilla::ipc::IPCResult RecvScrollingEvent(const uint64_t& aID,
+                                                     const uint64_t& aType,
+                                                     const uint32_t& aScrollX,
+                                                     const uint32_t& aScrollY,
+                                                     const uint32_t& aMaxScrollX,
+                                                     const uint32_t& aMaxScrollY) override;
+
   mozilla::ipc::IPCResult RecvRoleChangedEvent(const a11y::role& aRole) final;
 
   virtual mozilla::ipc::IPCResult RecvBindChildDoc(PDocAccessibleParent* aChildDoc, const uint64_t& aID) override;
 
   void Unbind()
   {
     if (DocAccessibleParent* parent = ParentDoc()) {
       parent->RemoveChildDoc(this);
--- a/accessible/ipc/other/PDocAccessible.ipdl
+++ b/accessible/ipc/other/PDocAccessible.ipdl
@@ -67,16 +67,19 @@ parent:
   async RoleChangedEvent(role aRole);
   async VirtualCursorChangeEvent(uint64_t aID,
                                  uint64_t aOldPosition,
                                  int32_t aOldStartOffset, int32_t aOldEndOffset,
                                  uint64_t aPosition,
                                  int32_t aStartOffset, int32_t aEndOffset,
                                  int16_t aReason, int16_t aBoundaryType,
                                  bool aFromUservcEvent);
+  async ScrollingEvent(uint64_t aID, uint64_t aType,
+                       uint32_t aScrollX, uint32_t aScrollY,
+                       uint32_t aMaxScrollX, uint32_t aMaxScrollY);
 
   /*
    * Tell the parent document to bind the existing document as a new child
    * document.
    */
   async BindChildDoc(PDocAccessible aChildDoc, uint64_t aID);
 
 child:
--- a/accessible/ipc/win/PDocAccessible.ipdl
+++ b/accessible/ipc/win/PDocAccessible.ipdl
@@ -65,16 +65,19 @@ parent:
   async FocusEvent(uint64_t aID, LayoutDeviceIntRect aCaretRect);
   async VirtualCursorChangeEvent(uint64_t aID,
                                  uint64_t aOldPosition,
                                  int32_t aOldStartOffset, int32_t aOldEndOffset,
                                  uint64_t aPosition,
                                  int32_t aStartOffset, int32_t aEndOffset,
                                  int16_t aReason, int16_t aBoundaryType,
                                  bool aFromUservcEvent);
+  async ScrollingEvent(uint64_t aID, uint64_t aType,
+                       uint32_t aScrollX, uint32_t aScrollY,
+                       uint32_t aMaxScrollX, uint32_t aMaxScrollY);
 
   /*
    * Tell the parent document to bind the existing document as a new child
    * document.
    */
   async BindChildDoc(PDocAccessible aChildDoc, uint64_t aID);
 
   sync GetWindowedPluginIAccessible(WindowsHandle aHwnd)
--- a/accessible/other/Platform.cpp
+++ b/accessible/other/Platform.cpp
@@ -62,9 +62,14 @@ a11y::ProxySelectionEvent(ProxyAccessibl
 
 #if defined(ANDROID)
 void
 a11y::ProxyVirtualCursorChangeEvent(ProxyAccessible*, ProxyAccessible*,
                                     int32_t, int32_t, ProxyAccessible*,
                                     int32_t, int32_t, int16_t, int16_t, bool)
 {
 }
+
+void
+a11y::ProxyScrollingEvent(ProxyAccessible*, uint32_t, uint32_t, uint32_t, uint32_t)
+{
+}
 #endif
--- a/accessible/tests/browser/events.js
+++ b/accessible/tests/browser/events.js
@@ -4,27 +4,30 @@
 
 "use strict";
 
 // This is loaded by head.js, so has the same globals, hence we import the
 // globals from there.
 /* import-globals-from shared-head.js */
 /* import-globals-from ../mochitest/common.js */
 
-/* exported EVENT_REORDER, EVENT_SHOW, EVENT_TEXT_INSERTED, EVENT_TEXT_REMOVED,
+/* exported EVENT_REORDER, EVENT_SCROLLING, EVENT_SCROLLING_END, EVENT_SHOW,
+            EVENT_TEXT_INSERTED, EVENT_TEXT_REMOVED,
             EVENT_DOCUMENT_LOAD_COMPLETE, EVENT_HIDE, EVENT_TEXT_CARET_MOVED,
             EVENT_DESCRIPTION_CHANGE, EVENT_NAME_CHANGE, EVENT_STATE_CHANGE,
             EVENT_VALUE_CHANGE, EVENT_TEXT_VALUE_CHANGE, EVENT_FOCUS,
             EVENT_DOCUMENT_RELOAD, EVENT_VIRTUALCURSOR_CHANGED,
             UnexpectedEvents, contentSpawnMutation, waitForEvent, waitForEvents,
             waitForOrderedEvents */
 
 const EVENT_DOCUMENT_LOAD_COMPLETE = nsIAccessibleEvent.EVENT_DOCUMENT_LOAD_COMPLETE;
 const EVENT_HIDE = nsIAccessibleEvent.EVENT_HIDE;
 const EVENT_REORDER = nsIAccessibleEvent.EVENT_REORDER;
+const EVENT_SCROLLING = nsIAccessibleEvent.EVENT_SCROLLING;
+const EVENT_SCROLLING_END = nsIAccessibleEvent.EVENT_SCROLLING_END;
 const EVENT_SHOW = nsIAccessibleEvent.EVENT_SHOW;
 const EVENT_STATE_CHANGE = nsIAccessibleEvent.EVENT_STATE_CHANGE;
 const EVENT_TEXT_CARET_MOVED = nsIAccessibleEvent.EVENT_TEXT_CARET_MOVED;
 const EVENT_TEXT_INSERTED = nsIAccessibleEvent.EVENT_TEXT_INSERTED;
 const EVENT_TEXT_REMOVED = nsIAccessibleEvent.EVENT_TEXT_REMOVED;
 const EVENT_DESCRIPTION_CHANGE = nsIAccessibleEvent.EVENT_DESCRIPTION_CHANGE;
 const EVENT_NAME_CHANGE = nsIAccessibleEvent.EVENT_NAME_CHANGE;
 const EVENT_VALUE_CHANGE = nsIAccessibleEvent.EVENT_VALUE_CHANGE;
--- a/accessible/tests/browser/events/browser.ini
+++ b/accessible/tests/browser/events/browser.ini
@@ -2,11 +2,12 @@
 support-files =
   head.js
   !/accessible/tests/browser/events.js
   !/accessible/tests/browser/shared-head.js
   !/accessible/tests/mochitest/*.js
 
 [browser_test_docload.js]
 skip-if = e10s
+[browser_test_scrolling.js]
 [browser_test_textcaret.js]
 [browser_test_focus_browserui.js]
 [browser_test_focus_dialog.js]
new file mode 100644
--- /dev/null
+++ b/accessible/tests/browser/events/browser_test_scrolling.js
@@ -0,0 +1,50 @@
+/* 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";
+
+addAccessibleTask(`
+    <div style="height: 100vh" id="one">one</div>
+    <div style="height: 100vh" id="two">two</div>
+    <div style="height: 100vh; width: 200vw" id="three">three</div>`,
+  async function(browser, accDoc) {
+    let onScrolling = waitForEvents([
+      [EVENT_SCROLLING, accDoc], [EVENT_SCROLLING_END, accDoc]]);
+    await ContentTask.spawn(browser, null, () => {
+      content.location.hash = "#two";
+    });
+    let [scrollEvent1, scrollEndEvent1] = await onScrolling;
+    scrollEvent1.QueryInterface(nsIAccessibleScrollingEvent);
+    ok(scrollEvent1.maxScrollY >= scrollEvent1.scrollY, "scrollY is within max");
+    scrollEndEvent1.QueryInterface(nsIAccessibleScrollingEvent);
+    ok(scrollEndEvent1.maxScrollY >= scrollEndEvent1.scrollY,
+      "scrollY is within max");
+
+    onScrolling = waitForEvents([
+      [EVENT_SCROLLING, accDoc], [EVENT_SCROLLING_END, accDoc]]);
+    await ContentTask.spawn(browser, null, () => {
+      content.location.hash = "#three";
+    });
+    let [scrollEvent2, scrollEndEvent2] = await onScrolling;
+    scrollEvent2.QueryInterface(nsIAccessibleScrollingEvent);
+    ok(scrollEvent2.scrollY > scrollEvent1.scrollY,
+      `${scrollEvent2.scrollY} > ${scrollEvent1.scrollY}`);
+    scrollEndEvent2.QueryInterface(nsIAccessibleScrollingEvent);
+    ok(scrollEndEvent2.maxScrollY >= scrollEndEvent2.scrollY,
+      "scrollY is within max");
+
+    onScrolling = waitForEvents([
+      [EVENT_SCROLLING, accDoc], [EVENT_SCROLLING_END, accDoc]]);
+    await ContentTask.spawn(browser, null, () => {
+      content.scrollTo(10, 0);
+    });
+    let [scrollEvent3, scrollEndEvent3] = await onScrolling;
+    scrollEvent3.QueryInterface(nsIAccessibleScrollingEvent);
+    ok(scrollEvent3.maxScrollX >= scrollEvent3.scrollX, "scrollX is within max");
+    scrollEndEvent3.QueryInterface(nsIAccessibleScrollingEvent);
+    ok(scrollEndEvent3.maxScrollX >= scrollEndEvent3.scrollX,
+      "scrollY is within max");
+    ok(scrollEvent3.scrollX > scrollEvent2.scrollX,
+      `${scrollEvent3.scrollX} > ${scrollEvent2.scrollX}`);
+  });
--- a/accessible/tests/mochitest/common.js
+++ b/accessible/tests/mochitest/common.js
@@ -3,16 +3,18 @@
 
 const nsIAccessibilityService = Ci.nsIAccessibilityService;
 
 const nsIAccessibleEvent = Ci.nsIAccessibleEvent;
 const nsIAccessibleStateChangeEvent =
   Ci.nsIAccessibleStateChangeEvent;
 const nsIAccessibleCaretMoveEvent =
   Ci.nsIAccessibleCaretMoveEvent;
+const nsIAccessibleScrollingEvent =
+  Ci.nsIAccessibleScrollingEvent;
 const nsIAccessibleTextChangeEvent =
   Ci.nsIAccessibleTextChangeEvent;
 const nsIAccessibleVirtualCursorChangeEvent =
   Ci.nsIAccessibleVirtualCursorChangeEvent;
 const nsIAccessibleObjectAttributeChangedEvent =
   Ci.nsIAccessibleObjectAttributeChangedEvent;
 
 const nsIAccessibleStates = Ci.nsIAccessibleStates;
--- a/accessible/windows/msaa/nsEventMap.h
+++ b/accessible/windows/msaa/nsEventMap.h
@@ -93,11 +93,11 @@ static const uint32_t gWinEventMap[] = {
   IA2_EVENT_HYPERLINK_SELECTED_LINK_CHANGED,         // nsIAccessibleEvent::EVENT_HYPERLINK_SELECTED_LINK_CHANGED
   IA2_EVENT_HYPERTEXT_LINK_ACTIVATED,                // nsIAccessibleEvent::EVENT_HYPERTEXT_LINK_ACTIVATED
   IA2_EVENT_HYPERTEXT_LINK_SELECTED,                 // nsIAccessibleEvent::EVENT_HYPERTEXT_LINK_SELECTED
   IA2_EVENT_HYPERLINK_START_INDEX_CHANGED,           // nsIAccessibleEvent::EVENT_HYPERLINK_START_INDEX_CHANGED
   IA2_EVENT_HYPERTEXT_CHANGED,                       // nsIAccessibleEvent::EVENT_HYPERTEXT_CHANGED
   IA2_EVENT_HYPERTEXT_NLINKS_CHANGED,                // nsIAccessibleEvent::EVENT_HYPERTEXT_NLINKS_CHANGED
   IA2_EVENT_OBJECT_ATTRIBUTE_CHANGED,                // nsIAccessibleEvent::EVENT_OBJECT_ATTRIBUTE_CHANGED
   kEVENT_WIN_UNKNOWN,                                 // nsIAccessibleEvent::EVENT_VIRTUALCURSOR_CHANGED
-  EVENT_OBJECT_VALUECHANGE                          // nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE
+  EVENT_OBJECT_VALUECHANGE,                          // nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE
+  kEVENT_WIN_UNKNOWN,                                // nsIAccessibleEvent::EVENT_SCROLLING
 };
-
--- a/accessible/xpcom/AccEvents.conf
+++ b/accessible/xpcom/AccEvents.conf
@@ -9,10 +9,11 @@
 simple_events = [
     'Event',
     'StateChangeEvent',
     'TextChangeEvent',
     'HideEvent',
     'CaretMoveEvent',
     'ObjectAttributeChangedEvent',
     'TableChangeEvent',
-    'VirtualCursorChangeEvent'
+    'VirtualCursorChangeEvent',
+    'ScrollingEvent'
   ]
rename from browser/modules/ContentLinkHandler.jsm
rename to browser/actors/LinkHandlerChild.jsm
--- a/browser/modules/ContentLinkHandler.jsm
+++ b/browser/actors/LinkHandlerChild.jsm
@@ -1,573 +1,94 @@
 /* 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 EXPORTED_SYMBOLS = ["ContentLinkHandler"];
+const EXPORTED_SYMBOLS = ["LinkHandlerChild"];
 
-ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
-
-XPCOMUtils.defineLazyGlobalGetters(this, ["Blob", "FileReader"]);
+ChromeUtils.import("resource://gre/modules/ActorChild.jsm");
 
 ChromeUtils.defineModuleGetter(this, "Feeds",
   "resource:///modules/Feeds.jsm");
-ChromeUtils.defineModuleGetter(this, "DeferredTask",
-  "resource://gre/modules/DeferredTask.jsm");
-ChromeUtils.defineModuleGetter(this, "PromiseUtils",
-  "resource://gre/modules/PromiseUtils.jsm");
-
-const BinaryInputStream = Components.Constructor("@mozilla.org/binaryinputstream;1",
-                                                 "nsIBinaryInputStream", "setInputStream");
-
-const SIZES_TELEMETRY_ENUM = {
-  NO_SIZES: 0,
-  ANY: 1,
-  DIMENSION: 2,
-  INVALID: 3,
-};
-
-const FAVICON_PARSING_TIMEOUT = 100;
-const FAVICON_RICH_ICON_MIN_WIDTH = 96;
-const PREFERRED_WIDTH = 16;
-
-// URL schemes that we don't want to load and convert to data URLs.
-const LOCAL_FAVICON_SCHEMES = [
-  "chrome",
-  "about",
-  "resource",
-  "data",
-];
-
-const MAX_FAVICON_EXPIRATION = 7 * 24 * 60 * 60 * 1000;
-
-const TYPE_ICO = "image/x-icon";
-const TYPE_SVG = "image/svg+xml";
-
-function promiseBlobAsDataURL(blob) {
-  return new Promise((resolve, reject) => {
-    let reader = new FileReader();
-    reader.addEventListener("load", () => resolve(reader.result));
-    reader.addEventListener("error", reject);
-    reader.readAsDataURL(blob);
-  });
-}
-
-function promiseBlobAsOctets(blob) {
-  return new Promise((resolve, reject) => {
-    let reader = new FileReader();
-    reader.addEventListener("load", () => {
-      resolve(Array.from(reader.result).map(c => c.charCodeAt(0)));
-    });
-    reader.addEventListener("error", reject);
-    reader.readAsBinaryString(blob);
-  });
-}
-
-class FaviconLoad {
-  constructor(iconInfo) {
-    this.buffers = [];
-    this.icon = iconInfo;
-
-    this.channel = Services.io.newChannelFromURI2(
-      iconInfo.iconUri,
-      iconInfo.node,
-      iconInfo.node.nodePrincipal,
-      iconInfo.node.nodePrincipal,
-      (Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_INHERITS |
-       Ci.nsILoadInfo.SEC_ALLOW_CHROME |
-       Ci.nsILoadInfo.SEC_DISALLOW_SCRIPT),
-      Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE_FAVICON);
-
-    this.channel.loadFlags |= Ci.nsIRequest.LOAD_BACKGROUND;
-    // Sometimes node is a document and sometimes it is an element. This is
-    // the easiest single way to get to the load group in both those cases.
-    this.channel.loadGroup = iconInfo.node.ownerGlobal.document.documentLoadGroup;
-    this.channel.notificationCallbacks = this;
-
-    if (Services.prefs.getBoolPref("network.http.tailing.enabled", true) &&
-        this.channel instanceof Ci.nsIClassOfService) {
-      this.channel.addClassFlags(Ci.nsIClassOfService.Tail | Ci.nsIClassOfService.Throttleable);
-    }
-  }
-
-  load() {
-    this._deferred = PromiseUtils.defer();
-    // Clear the channel reference when we succeed or fail.
-    this._deferred.promise.then(
-      () => this.channel = null,
-      () => this.channel = null
-    );
-
-    try {
-      this.channel.asyncOpen2(this);
-    } catch (e) {
-      this._deferred.reject(e);
-    }
-
-    return this._deferred.promise;
-  }
-
-  cancel() {
-    if (!this.channel) {
-      return;
-    }
-
-    this.channel.cancel(Cr.NS_BINDING_ABORTED);
-  }
-
-  onStartRequest(request, context) {
-  }
+ChromeUtils.defineModuleGetter(this, "FaviconLoader",
+  "resource:///modules/FaviconLoader.jsm");
 
-  onDataAvailable(request, context, inputStream, offset, count) {
-    let stream = new BinaryInputStream(inputStream);
-    let buffer = new ArrayBuffer(count);
-    stream.readArrayBuffer(buffer.byteLength, buffer);
-    this.buffers.push(new Uint8Array(buffer));
-  }
-
-  asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) {
-    if (oldChannel == this.channel) {
-      this.channel = newChannel;
-    }
-
-    callback.onRedirectVerifyCallback(Cr.NS_OK);
-  }
-
-  async onStopRequest(request, context, statusCode) {
-    if (request != this.channel) {
-      // Indicates that a redirect has occurred. We don't care about the result
-      // of the original channel.
-      return;
-    }
-
-    if (!Components.isSuccessCode(statusCode)) {
-      if (statusCode == Cr.NS_BINDING_ABORTED) {
-        this._deferred.reject(Components.Exception(`Favicon load from ${this.icon.iconUri.spec} was cancelled.`, statusCode));
-      } else {
-        this._deferred.reject(Components.Exception(`Favicon at "${this.icon.iconUri.spec}" failed to load.`, statusCode));
-      }
-      return;
-    }
-
-    if (this.channel instanceof Ci.nsIHttpChannel) {
-      if (!this.channel.requestSucceeded) {
-        this._deferred.reject(Components.Exception(`Favicon at "${this.icon.iconUri.spec}" failed to load: ${this.channel.responseStatusText}.`, Cr.NS_ERROR_FAILURE));
-        return;
-      }
-    }
-
-    // Attempt to get an expiration time from the cache.  If this fails, we'll
-    // use this default.
-    let expiration = Date.now() + MAX_FAVICON_EXPIRATION;
-
-    // This stuff isn't available after onStopRequest returns (so don't start
-    // any async operations before this!).
-    if (this.channel instanceof Ci.nsICacheInfoChannel) {
-      try {
-        expiration = Math.min(this.channel.cacheTokenExpirationTime * 1000, expiration);
-      } catch (e) {
-        // Ignore failures to get the expiration time.
-      }
-    }
-
-    try {
-      let type = this.channel.contentType;
-      let blob = new Blob(this.buffers, { type });
+class LinkHandlerChild extends ActorChild {
+  constructor(mm) {
+    super(mm);
 
-      if (type != "image/svg+xml") {
-        let octets = await promiseBlobAsOctets(blob);
-        let sniffer = Cc["@mozilla.org/image/loader;1"].
-                      createInstance(Ci.nsIContentSniffer);
-        type = sniffer.getMIMETypeFromContent(this.channel, octets, octets.length);
-
-        if (!type) {
-          throw Components.Exception(`Favicon at "${this.icon.iconUri.spec}" did not match a known mimetype.`, Cr.NS_ERROR_FAILURE);
-        }
-
-        blob = blob.slice(0, blob.size, type);
-      }
-
-      let dataURL = await promiseBlobAsDataURL(blob);
-
-      this._deferred.resolve({
-        expiration,
-        dataURL,
-      });
-    } catch (e) {
-      this._deferred.reject(e);
-    }
-  }
-
-  getInterface(iid) {
-    if (iid.equals(Ci.nsIChannelEventSink)) {
-      return this;
-    }
-    throw Cr.NS_ERROR_NO_INTERFACE;
-  }
-}
-
-/*
- * Extract the icon width from the size attribute. It also sends the telemetry
- * about the size type and size dimension info.
- *
- * @param {Array} aSizes An array of strings about size.
- * @return {Number} A width of the icon in pixel.
- */
-function extractIconSize(aSizes) {
-  let width = -1;
-  let sizesType;
-  const re = /^([1-9][0-9]*)x[1-9][0-9]*$/i;
-
-  if (aSizes.length) {
-    for (let size of aSizes) {
-      if (size.toLowerCase() == "any") {
-        sizesType = SIZES_TELEMETRY_ENUM.ANY;
-        break;
-      } else {
-        let values = re.exec(size);
-        if (values && values.length > 1) {
-          sizesType = SIZES_TELEMETRY_ENUM.DIMENSION;
-          width = parseInt(values[1]);
-          break;
-        } else {
-          sizesType = SIZES_TELEMETRY_ENUM.INVALID;
-          break;
-        }
-      }
-    }
-  } else {
-    sizesType = SIZES_TELEMETRY_ENUM.NO_SIZES;
+    this.seenTabIcon = false;
+    this._iconLoader = null;
   }
 
-  // Telemetry probes for measuring the sizes attribute
-  // usage and available dimensions.
-  Services.telemetry.getHistogramById("LINK_ICON_SIZES_ATTR_USAGE").add(sizesType);
-  if (width > 0)
-    Services.telemetry.getHistogramById("LINK_ICON_SIZES_ATTR_DIMENSION").add(width);
-
-  return width;
-}
-
-/*
- * Get link icon URI from a link dom node.
- *
- * @param {DOMNode} aLink A link dom node.
- * @return {nsIURI} A uri of the icon.
- */
-function getLinkIconURI(aLink) {
-  let targetDoc = aLink.ownerDocument;
-  let uri = Services.io.newURI(aLink.href, targetDoc.characterSet);
-  try {
-    uri = uri.mutate().setUserPass("").finalize();
-  } catch (e) {
-    // some URIs are immutable
-  }
-  return uri;
-}
-
-/**
- * Guess a type for an icon based on its declared type or file extension.
- */
-function guessType(icon) {
-  // No type with no icon
-  if (!icon) {
-    return "";
-  }
-
-  // Use the file extension to guess at a type we're interested in
-  if (!icon.type) {
-    let extension = icon.iconUri.filePath.split(".").pop();
-    switch (extension) {
-      case "ico":
-        return TYPE_ICO;
-      case "svg":
-        return TYPE_SVG;
+  get iconLoader() {
+    if (!this._iconLoader) {
+      this._iconLoader = new FaviconLoader(this.mm);
     }
-  }
-
-  // Fuzzily prefer the type or fall back to the declared type
-  return icon.type == "image/vnd.microsoft.icon" ? TYPE_ICO : icon.type || "";
-}
-
-/*
- * Selects the best rich icon and tab icon from a list of IconInfo objects.
- *
- * @param {Array} iconInfos A list of IconInfo objects.
- * @param {integer} preferredWidth The preferred width for tab icons.
- */
-function selectIcons(iconInfos, preferredWidth) {
-  if (iconInfos.length == 0) {
-    return {
-      richIcon: null,
-      tabIcon: null,
-    };
-  }
-
-  let preferredIcon;
-  let bestSizedIcon;
-  // Other links with the "icon" tag are the default icons
-  let defaultIcon;
-  // Rich icons are either apple-touch or fluid icons, or the ones of the
-  // dimension 96x96 or greater
-  let largestRichIcon;
-
-  for (let icon of iconInfos) {
-    if (!icon.isRichIcon) {
-      // First check for svg. If it's not available check for an icon with a
-      // size adapt to the current resolution. If both are not available, prefer
-      // ico files. When multiple icons are in the same set, the latest wins.
-      if (guessType(icon) == TYPE_SVG) {
-        preferredIcon = icon;
-      } else if (icon.width == preferredWidth && guessType(preferredIcon) != TYPE_SVG) {
-        preferredIcon = icon;
-      } else if (guessType(icon) == TYPE_ICO && (!preferredIcon || guessType(preferredIcon) == TYPE_ICO)) {
-        preferredIcon = icon;
-      }
-
-      // Check for an icon larger yet closest to preferredWidth, that can be
-      // downscaled efficiently.
-      if (icon.width >= preferredWidth &&
-          (!bestSizedIcon || bestSizedIcon.width >= icon.width)) {
-        bestSizedIcon = icon;
-      }
-    }
-
-    // Note that some sites use hi-res icons without specifying them as
-    // apple-touch or fluid icons.
-    if (icon.isRichIcon || icon.width >= FAVICON_RICH_ICON_MIN_WIDTH) {
-      if (!largestRichIcon || largestRichIcon.width < icon.width) {
-        largestRichIcon = icon;
-      }
-    } else {
-      defaultIcon = icon;
-    }
-  }
-
-  // Now set the favicons for the page in the following order:
-  // 1. Set the best rich icon if any.
-  // 2. Set the preferred one if any, otherwise check if there's a better
-  //    sized fit.
-  // This order allows smaller icon frames to eventually override rich icon
-  // frames.
-
-  let tabIcon = null;
-  if (preferredIcon) {
-    tabIcon = preferredIcon;
-  } else if (bestSizedIcon) {
-    tabIcon = bestSizedIcon;
-  } else if (defaultIcon) {
-    tabIcon = defaultIcon;
+    return this._iconLoader;
   }
 
-  return {
-    richIcon: largestRichIcon,
-    tabIcon
-  };
-}
-
-function makeFaviconFromLink(aLink, aIsRichIcon) {
-  let iconUri = getLinkIconURI(aLink);
-  if (!iconUri)
-    return null;
-
-  // Extract the size type and width.
-  let width = extractIconSize(aLink.sizes);
-
-  return {
-    iconUri,
-    width,
-    isRichIcon: aIsRichIcon,
-    type: aLink.type,
-    node: aLink,
-  };
-}
-
-class IconLoader {
-  constructor(chromeGlobal) {
-    this.chromeGlobal = chromeGlobal;
-  }
-
-  async load(iconInfo) {
-    if (this._loader) {
-      this._loader.cancel();
-    }
-
-    if (LOCAL_FAVICON_SCHEMES.includes(iconInfo.iconUri.scheme)) {
-      this.chromeGlobal.sendAsyncMessage("Link:SetIcon", {
-        originalURL: iconInfo.iconUri.spec,
-        canUseForTab: !iconInfo.isRichIcon,
-        expiration: undefined,
-        iconURL: iconInfo.iconUri.spec,
-      });
-      return;
-    }
-
-    try {
-      this._loader = new FaviconLoad(iconInfo);
-      let { dataURL, expiration } = await this._loader.load();
-
-      this.chromeGlobal.sendAsyncMessage("Link:SetIcon", {
-        originalURL: iconInfo.iconUri.spec,
-        canUseForTab: !iconInfo.isRichIcon,
-        expiration,
-        iconURL: dataURL,
-      });
-    } catch (e) {
-      if (e.resultCode != Cr.NS_BINDING_ABORTED) {
-        Cu.reportError(e);
-
-        // Used mainly for tests currently.
-        this.chromeGlobal.sendAsyncMessage("Link:SetFailedIcon", {
-          originalURL: iconInfo.iconUri.spec,
-          canUseForTab: !iconInfo.isRichIcon,
-        });
+  addRootIcon() {
+    if (!this.seenTabIcon && Services.prefs.getBoolPref("browser.chrome.guess_favicon", true) &&
+        Services.prefs.getBoolPref("browser.chrome.site_icons", true)) {
+      // Inject the default icon. Use documentURIObject so that we do the right
+      // thing with about:-style error pages. See bug 453442
+      let baseURI = this.content.document.documentURIObject;
+      if (["http", "https"].includes(baseURI.scheme)) {
+        this.seenTabIcon = true;
+        this.iconLoader.addDefaultIcon(baseURI);
       }
-    } finally {
-      this._loader = null;
-    }
-  }
-
-  cancel() {
-    if (!this._loader) {
-      return;
-    }
-
-    this._loader.cancel();
-    this._loader = null;
-  }
-}
-
-class ContentLinkHandler {
-  constructor(chromeGlobal) {
-    this.chromeGlobal = chromeGlobal;
-    this.iconInfos = [];
-    this.seenTabIcon = false;
-
-    chromeGlobal.addEventListener("DOMLinkAdded", this);
-    chromeGlobal.addEventListener("DOMLinkChanged", this);
-    chromeGlobal.addEventListener("pageshow", this);
-    chromeGlobal.addEventListener("pagehide", this);
-    chromeGlobal.addEventListener("DOMHeadElementParsed", this);
-
-    // For every page we attempt to find a rich icon and a tab icon. These
-    // objects take care of the load process for each.
-    this.richIconLoader = new IconLoader(chromeGlobal);
-    this.tabIconLoader = new IconLoader(chromeGlobal);
-
-    this.iconTask = new DeferredTask(() => this.loadIcons(), FAVICON_PARSING_TIMEOUT);
-  }
-
-  loadIcons() {
-    let preferredWidth = PREFERRED_WIDTH * Math.ceil(this.chromeGlobal.content.devicePixelRatio);
-    let { richIcon, tabIcon } = selectIcons(this.iconInfos, preferredWidth);
-    this.iconInfos = [];
-
-    if (richIcon) {
-      this.richIconLoader.load(richIcon);
-    }
-
-    if (tabIcon) {
-      this.tabIconLoader.load(tabIcon);
-    }
-  }
-
-  addIcon(iconInfo) {
-    if (!Services.prefs.getBoolPref("browser.chrome.site_icons", true)) {
-      return;
-    }
-
-    if (!iconInfo.isRichIcon) {
-      this.seenTabIcon = true;
-    }
-    this.iconInfos.push(iconInfo);
-    this.iconTask.arm();
-  }
-
-  addRootIcon(document) {
-    // If we've already seen a tab icon or if root favicons are disabled then
-    // bail out.
-    if (this.seenTabIcon || !Services.prefs.getBoolPref("browser.chrome.guess_favicon", true)) {
-      return;
-    }
-
-    // Currently ImageDocuments will just load the default favicon, see bug
-    // 403651 for discussion.
-
-    // Inject the default icon. Use documentURIObject so that we do the right
-    // thing with about:-style error pages. See bug 453442
-    let baseURI = document.documentURIObject;
-    if (baseURI.schemeIs("http") || baseURI.schemeIs("https")) {
-      let iconUri = baseURI.mutate().setPathQueryRef("/favicon.ico").finalize();
-      this.addIcon({
-        iconUri,
-        width: -1,
-        isRichIcon: false,
-        type: TYPE_ICO,
-        node: document,
-      });
     }
   }
 
   onHeadParsed(event) {
-    let document = this.chromeGlobal.content.document;
-    if (event.target.ownerDocument != document) {
+    if (event.target.ownerDocument != this.content.document) {
       return;
     }
 
     // Per spec icons are meant to be in the <head> tag so we should have seen
     // all the icons now so add the root icon if no other tab icons have been
     // seen.
-    this.addRootIcon(document);
+    this.addRootIcon();
 
     // We're likely done with icon parsing so load the pending icons now.
-    if (this.iconTask.isArmed) {
-      this.iconTask.disarm();
-      this.loadIcons();
+    if (this._iconLoader) {
+      this._iconLoader.onPageShow();
     }
   }
 
   onPageShow(event) {
-    let document = this.chromeGlobal.content.document;
-    if (event.target != document) {
+    if (event.target != this.content.document) {
       return;
     }
 
-    // Add the root icon if it hasn't already been added. We encounter this case
-    // for documents that do not have a <head> tag.
-    this.addRootIcon(document);
+    this.addRootIcon();
 
-    // If we've seen any additional icons since the start of the body element
-    // load them now.
-    if (this.iconTask.isArmed) {
-      this.iconTask.disarm();
-      this.loadIcons();
+    if (this._iconLoader) {
+      this._iconLoader.onPageShow();
     }
   }
 
   onPageHide(event) {
-    if (event.target != this.chromeGlobal.content.document) {
+    if (event.target != this.content.document) {
       return;
     }
 
-    this.richIconLoader.cancel();
-    this.tabIconLoader.cancel();
-
-    this.iconTask.disarm();
-    this.iconInfos = [];
-    this.seenTabIcon = false;
+    if (this._iconLoader) {
+      this._iconLoader.onPageHide();
+    }
   }
 
   onLinkEvent(event) {
     let link = event.target;
     // Ignore sub-frames (bugs 305472, 479408).
-    if (link.ownerGlobal != this.chromeGlobal.content) {
+    if (link.ownerGlobal != this.content) {
       return;
     }
 
     let rel = link.rel && link.rel.toLowerCase();
     if (!rel || !link.href)
       return;
 
     // Note: following booleans only work for the current link, not for the
@@ -575,83 +96,87 @@ class ContentLinkHandler {
     let feedAdded = false;
     let iconAdded = false;
     let searchAdded = false;
     let rels = {};
     for (let relString of rel.split(/\s+/))
       rels[relString] = true;
 
     for (let relVal in rels) {
-      let isRichIcon = true;
+      let isRichIcon = false;
 
       switch (relVal) {
         case "feed":
         case "alternate":
           if (!feedAdded && event.type == "DOMLinkAdded") {
             if (!rels.feed && rels.alternate && rels.stylesheet)
               break;
 
             if (Feeds.isValidFeed(link, link.ownerDocument.nodePrincipal, "feed" in rels)) {
-              this.chromeGlobal.sendAsyncMessage("Link:AddFeed", {
+              this.mm.sendAsyncMessage("Link:AddFeed", {
                 type: link.type,
                 href: link.href,
                 title: link.title,
               });
               feedAdded = true;
             }
           }
           break;
-        case "icon":
-          isRichIcon = false;
-          // Fall through to rich icon handling
         case "apple-touch-icon":
         case "apple-touch-icon-precomposed":
         case "fluid-icon":
+          isRichIcon = true;
+        case "icon":
           if (iconAdded || link.hasAttribute("mask")) { // Masked icons are not supported yet.
             break;
           }
 
-          let iconInfo = makeFaviconFromLink(link, isRichIcon);
+          if (!Services.prefs.getBoolPref("browser.chrome.site_icons", true)) {
+            return;
+          }
+
+          let iconInfo = FaviconLoader.makeFaviconFromLink(link, isRichIcon);
           if (iconInfo) {
-            iconAdded = this.addIcon(iconInfo);
+            iconAdded = true;
+            if (!isRichIcon) {
+              this.seenTabIcon = true;
+            }
+            this.iconLoader.addIcon(iconInfo);
           }
           break;
         case "search":
           if (Services.policies && !Services.policies.isAllowed("installSearchEngine")) {
             break;
           }
 
           if (!searchAdded && event.type == "DOMLinkAdded") {
             let type = link.type && link.type.toLowerCase();
             type = type.replace(/^\s+|\s*(?:;.*)?$/g, "");
 
             let re = /^(?:https?|ftp):/i;
             if (type == "application/opensearchdescription+xml" && link.title &&
                 re.test(link.href)) {
               let engine = { title: link.title, href: link.href };
-              this.chromeGlobal.sendAsyncMessage("Link:AddSearch", {
+              this.mm.sendAsyncMessage("Link:AddSearch", {
                 engine,
                 url: link.ownerDocument.documentURI,
               });
               searchAdded = true;
             }
           }
           break;
       }
     }
   }
 
   handleEvent(event) {
     switch (event.type) {
       case "pageshow":
-        this.onPageShow(event);
-        break;
+        return this.onPageShow(event);
       case "pagehide":
-        this.onPageHide(event);
-        break;
+        return this.onPageHide(event);
       case "DOMHeadElementParsed":
-        this.onHeadParsed(event);
-        break;
+        return this.onHeadParsed(event);
       default:
-        this.onLinkEvent(event);
+        return this.onLinkEvent(event);
     }
   }
 }
--- a/browser/actors/moz.build
+++ b/browser/actors/moz.build
@@ -26,16 +26,17 @@ FINAL_TARGET_FILES.actors += [
     'AboutReaderChild.jsm',
     'BlockedSiteChild.jsm',
     'BrowserTabChild.jsm',
     'ClickHandlerChild.jsm',
     'ContentSearchChild.jsm',
     'ContextMenuChild.jsm',
     'DOMFullscreenChild.jsm',
     'LightWeightThemeInstallChild.jsm',
+    'LinkHandlerChild.jsm',
     'NetErrorChild.jsm',
     'OfflineAppsChild.jsm',
     'PageInfoChild.jsm',
     'PageMetadataChild.jsm',
     'PageStyleChild.jsm',
     'PluginChild.jsm',
     'UAWidgetsChild.jsm',
     'URIFixupChild.jsm',
--- a/browser/base/content/content.js
+++ b/browser/base/content/content.js
@@ -11,17 +11,16 @@
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 // TabChildGlobal
 var global = this;
 
 XPCOMUtils.defineLazyModuleGetters(this, {
-  ContentLinkHandler: "resource:///modules/ContentLinkHandler.jsm",
   ContentMetaHandler: "resource:///modules/ContentMetaHandler.jsm",
   LoginFormFactory: "resource://gre/modules/LoginManagerContent.jsm",
   InsecurePasswordUtils: "resource://gre/modules/InsecurePasswordUtils.jsm",
   FormSubmitObserver: "resource:///modules/FormSubmitObserver.jsm",
   ContextMenuChild: "resource:///actors/ContextMenuChild.jsm",
 });
 
 XPCOMUtils.defineLazyGetter(this, "LoginManagerContent", () => {
@@ -55,17 +54,16 @@ addEventListener("DOMInputPasswordAdded"
   LoginManagerContent.onDOMInputPasswordAdded(event, content);
   let formLike = LoginFormFactory.createFromField(event.originalTarget);
   InsecurePasswordUtils.reportInsecurePasswords(formLike);
 });
 addEventListener("DOMAutoComplete", function(event) {
   LoginManagerContent.onUsernameInput(event);
 });
 
-new ContentLinkHandler(this);
 ContentMetaHandler.init(this);
 
 // This is a temporary hack to prevent regressions (bug 1471327).
 void content;
 
 addEventListener("DOMWindowFocus", function(event) {
   sendAsyncMessage("DOMWindowFocus", {});
 }, false);
--- a/browser/base/content/tabbrowser.js
+++ b/browser/base/content/tabbrowser.js
@@ -1680,19 +1680,17 @@ window._gBrowser = {
 
     evt = document.createEvent("Events");
     evt.initEvent("TabRemotenessChange", true, false);
     tab.dispatchEvent(evt);
 
     return true;
   },
 
-  updateBrowserRemotenessByURL(aBrowser, aURL, aOptions) {
-    aOptions = aOptions || {};
-
+  updateBrowserRemotenessByURL(aBrowser, aURL, aOptions = {}) {
     if (!gMultiProcessBrowser)
       return this.updateBrowserRemoteness(aBrowser, false);
 
     let oldRemoteType = aBrowser.getAttribute("remoteType") || null;
 
     aOptions.remoteType =
       E10SUtils.getRemoteTypeForURI(aURL,
         gMultiProcessBrowser,
--- a/browser/base/content/test/performance/browser.ini
+++ b/browser/base/content/test/performance/browser.ini
@@ -13,17 +13,17 @@ prefs =
 support-files =
   head.js
 [browser_appmenu.js]
 skip-if = asan || debug || (os == 'win' && bits == 32) # Bug 1382809, bug 1369959, Win32 because of intermittent OOM failures
 [browser_preferences_usage.js]
 skip-if = !debug
 [browser_startup.js]
 [browser_startup_content.js]
-skip-if = !e10s
+skip-if = !e10s || verify
 [browser_startup_flicker.js]
 run-if = debug || devedition || nightly_build # Requires startupRecorder.js, which isn't shipped everywhere by default
 [browser_tabclose_grow.js]
 [browser_tabclose.js]
 [browser_tabopen.js]
 skip-if = (verify && (os == 'mac'))
 [browser_tabopen_squeeze.js]
 [browser_tabstrip_overflow_underflow.js]
--- a/browser/base/content/test/performance/browser_startup_content.js
+++ b/browser/base/content/test/performance/browser_startup_content.js
@@ -45,18 +45,18 @@ const whitelist = {
 
     // Forms and passwords
     "resource://formautofill/FormAutofill.jsm",
     "resource://formautofill/FormAutofillContent.jsm",
 
     // Browser front-end
     "resource:///actors/AboutReaderChild.jsm",
     "resource:///actors/BrowserTabChild.jsm",
-    "resource:///modules/ContentLinkHandler.jsm",
     "resource:///modules/ContentMetaHandler.jsm",
+    "resource:///actors/LinkHandlerChild.jsm",
     "resource:///actors/PageStyleChild.jsm",
     "resource://gre/modules/ActorChild.jsm",
     "resource://gre/modules/ActorManagerChild.jsm",
     "resource://gre/modules/E10SUtils.jsm",
     "resource://gre/modules/PrivateBrowsingUtils.jsm",
     "resource://gre/modules/ReaderMode.jsm",
     "resource://gre/modules/WebProgressChild.jsm",
 
--- a/browser/base/content/test/static/browser_parsable_css.js
+++ b/browser/base/content/test/static/browser_parsable_css.js
@@ -22,20 +22,16 @@ let whitelist = [
   // PDFjs rules needed for compat with other UAs.
   {sourceName: /web\/viewer\.css$/i,
    errorMessage: /Unknown property.*(appearance|user-select)/i,
    isFromDevTools: false},
   // Highlighter CSS uses a UA-only pseudo-class, see bug 985597.
   {sourceName: /highlighters\.css$/i,
    errorMessage: /Unknown pseudo-class.*moz-native-anonymous/i,
    isFromDevTools: true},
-  // Responsive Design Mode CSS uses a UA-only pseudo-class, see Bug 1241714.
-  {sourceName: /responsive-ua\.css$/i,
-   errorMessage: /Unknown pseudo-class.*moz-dropdown-list/i,
-   isFromDevTools: true},
   // UA-only media features.
   {sourceName: /\b(autocomplete-item|svg)\.css$/,
    errorMessage: /Expected media feature name but found \u2018-moz.*/i,
    isFromDevTools: false},
 
   {sourceName: /\b(contenteditable|EditorOverride|svg|forms|html|mathml|ua)\.css$/i,
    errorMessage: /Unknown pseudo-class.*-moz-/i,
    isFromDevTools: false},
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -126,16 +126,29 @@ let ACTORS = {
       events: {
         "InstallBrowserTheme": {wantUntrusted: true},
         "PreviewBrowserTheme": {wantUntrusted: true},
         "ResetBrowserThemePreview": {wantUntrusted: true},
       },
     },
   },
 
+  LinkHandler: {
+    child: {
+      module: "resource:///actors/LinkHandlerChild.jsm",
+      events: {
+        "DOMHeadElementParsed": {},
+        "DOMLinkAdded": {},
+        "DOMLinkChanged": {},
+        "pageshow": {},
+        "pagehide": {},
+      },
+    },
+  },
+
   NetError: {
     child: {
       module: "resource:///actors/NetErrorChild.jsm",
       events: {
         "AboutNetErrorLoad": {wantUntrusted: true},
         "AboutNetErrorOpenCaptivePortal": {wantUntrusted: true},
         "AboutNetErrorSetAutomatic": {wantUntrusted: true},
         "AboutNetErrorResetPreferences": {wantUntrusted: true},
--- a/browser/components/payments/res/containers/address-form.css
+++ b/browser/components/payments/res/containers/address-form.css
@@ -2,33 +2,16 @@
  * 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/. */
 
 /* Hide the message about autofill availability since it's not relevant */
 #country-warning-message {
   display: none;
 }
 
-/* Hide all form fields that are not explicitly requested
- * by the paymentOptions object.
- */
-address-form[address-fields]:not([address-fields~='name']) #name-container,
-address-form[address-fields] #organization-container,
-address-form[address-fields] #street-address-container,
-address-form[address-fields] #address-level2-container,
-address-form[address-fields] #address-level1-container,
-address-form[address-fields] #postal-code-container,
-address-form[address-fields] #country-container,
-address-form[address-fields]:not([address-fields~='email']) #email-container,
-address-form[address-fields]:not([address-fields~='tel']) #tel-container {
-  /* !important is needed because autofillEditForms.js sets
-     inline styles on the form fields with display: flex; */
-  display: none !important;
-}
-
 .error-text:not(:empty) {
   color: #fff;
   background-color: #d70022;
   border-radius: 2px;
   /* The padding-top and padding-bottom are referenced by address-form.js */
   padding: 5px 12px;
   position: absolute;
   z-index: 1;
--- a/browser/components/payments/res/containers/address-form.js
+++ b/browser/components/payments/res/containers/address-form.js
@@ -20,16 +20,18 @@ import paymentRequest from "../paymentRe
  * as it will be much easier to share the logic once we switch to Fluent.
  */
 
 export default class AddressForm extends PaymentStateSubscriberMixin(PaymentRequestPage) {
   constructor() {
     super();
 
     this.genericErrorText = document.createElement("div");
+    this.genericErrorText.setAttribute("aria-live", "polite");
+    this.genericErrorText.classList.add("page-error");
 
     this.cancelButton = document.createElement("button");
     this.cancelButton.className = "cancel-button";
     this.cancelButton.addEventListener("click", this);
 
     this.backButton = document.createElement("button");
     this.backButton.className = "back-button";
     this.backButton.addEventListener("click", this);
@@ -83,16 +85,22 @@ export default class AddressForm extends
       this.formHandler = new EditAddress({
         form,
       }, record, {
         DEFAULT_REGION: PaymentDialogUtils.DEFAULT_REGION,
         getFormFormat: PaymentDialogUtils.getFormFormat,
         supportedCountries: PaymentDialogUtils.supportedCountries,
       });
 
+      // The EditAddress constructor adds `input` event listeners on the same element,
+      // which update field validity. By adding our event listeners after this constructor,
+      // validity will be updated before our handlers get the event
+      this.form.addEventListener("input", this);
+      this.form.addEventListener("invalid", this);
+
       this.body.appendChild(this.persistCheckbox);
       this.body.appendChild(this.genericErrorText);
 
       this.footer.appendChild(this.cancelButton);
       this.footer.appendChild(this.backButton);
       this.footer.appendChild(this.saveButton);
       // Only call the connected super callback(s) once our markup is fully
       // connected, including the shared form fetched asynchronously.
@@ -118,22 +126,16 @@ export default class AddressForm extends
     this.backButton.textContent = this.dataset.backButtonLabel;
     this.saveButton.textContent = editing ? this.dataset.updateButtonLabel :
                                             this.dataset.addButtonLabel;
     this.persistCheckbox.label = this.dataset.persistCheckboxLabel;
 
     this.backButton.hidden = page.onboardingWizard;
     this.cancelButton.hidden = !page.onboardingWizard;
 
-    if (addressPage.addressFields) {
-      this.setAttribute("address-fields", addressPage.addressFields);
-    } else {
-      this.removeAttribute("address-fields");
-    }
-
     this.pageTitleHeading.textContent = addressPage.title;
     this.genericErrorText.textContent = page.error;
 
     let addresses = paymentRequest.getAddresses(state);
 
     // If an address is selected we want to edit it.
     if (editing) {
       record = addresses[addressPage.guid];
@@ -149,16 +151,21 @@ export default class AddressForm extends
           PaymentDialogUtils.getDefaultPreferences(): ${typeof saveAddressDefaultChecked}`);
       }
       // Adding a new record: default persistence to the pref value when in a not-private session
       this.persistCheckbox.hidden = false;
       this.persistCheckbox.checked = state.isPrivate ? false :
                                                        saveAddressDefaultChecked;
     }
 
+    if (addressPage.addressFields) {
+      this.form.dataset.addressFields = addressPage.addressFields;
+    } else {
+      this.form.dataset.addressFields = "mailing-address tel";
+    }
     this.formHandler.loadRecord(record);
 
     // Add validation to some address fields
     this.updateRequiredState();
 
     let shippingAddressErrors = request.paymentDetails.shippingAddressErrors;
     for (let [errorName, errorSelector] of Object.entries(this._errorFieldMap)) {
       let container = this.form.querySelector(errorSelector + "-container");
@@ -192,24 +199,34 @@ export default class AddressForm extends
       // Subtract 10px for the padding-top and padding-bottom.
       data.span.style.top = (data.top - 10) + "px";
       if (isRTL) {
         data.span.style.right = data.right + "px";
       } else {
         data.span.style.left = data.left + "px";
       }
     }
+
+    this.updateSaveButtonState();
   }
 
   handleEvent(event) {
     switch (event.type) {
       case "click": {
         this.onClick(event);
         break;
       }
+      case "input": {
+        this.onInput(event);
+        break;
+      }
+      case "invalid": {
+        this.onInvalid(event);
+        break;
+      }
     }
   }
 
   onClick(evt) {
     switch (evt.target) {
       case this.cancelButton: {
         paymentRequest.cancel();
         break;
@@ -226,42 +243,57 @@ export default class AddressForm extends
           state[previousId] = Object.assign({}, currentState[previousId], {
             preserveFieldValues: true,
           });
         }
         this.requestStore.setState(state);
         break;
       }
       case this.saveButton: {
-        this.saveRecord();
+        if (this.form.checkValidity()) {
+          this.saveRecord();
+        }
         break;
       }
       default: {
         throw new Error("Unexpected click target");
       }
     }
   }
 
+  onInput(event) {
+    this.updateSaveButtonState();
+  }
+
+  onInvalid(event) {
+    this.saveButton.disabled = true;
+  }
+
   updateRequiredState() {
-    for (let formElement of this.form.elements) {
-      let container = formElement.closest(`#${formElement.id}-container`);
-      if (formElement.localName == "button" || !container) {
+    for (let field of this.form.elements) {
+      let container = field.closest(`#${field.id}-container`);
+      if (field.localName == "button" || !container) {
         continue;
       }
       let span = container.querySelector("span");
       span.setAttribute("fieldRequiredSymbol", this.dataset.fieldRequiredSymbol);
-      let required = formElement.required && !formElement.disabled;
+      let required = field.required && !field.disabled;
       if (required) {
         container.setAttribute("required", "true");
       } else {
         container.removeAttribute("required");
       }
     }
   }
 
+  updateSaveButtonState() {
+    this.saveButton.disabled = !this.form.checkValidity();
+    log.debug("updateSaveButtonState", this.saveButton.disabled);
+  }
+
   async saveRecord() {
     let record = this.formHandler.buildFormObject();
     let currentState = this.requestStore.getState();
     let {
       page,
       tempAddresses,
       savedBasicCards,
       "address-page": addressPage,
--- a/browser/components/payments/res/containers/address-picker.js
+++ b/browser/components/payments/res/containers/address-picker.js
@@ -26,24 +26,24 @@ export default class AddressPicker exten
     super.attributeChangedCallback(name, oldValue, newValue);
     if (name == "address-fields" && oldValue !== newValue) {
       this.render(this.requestStore.getState());
     }
   }
 
   get fieldNames() {
     if (this.hasAttribute("address-fields")) {
-      let names = this.getAttribute("address-fields").split(/\s+/);
+      let names = this.getAttribute("address-fields").trim().split(/\s+/);
       if (names.length) {
         return names;
       }
     }
 
     return [
-      "address-level1",
+      // "address-level1", // TODO: bug 1481481 - not required for some countries e.g. DE
       "address-level2",
       "country",
       "name",
       "postal-code",
       "street-address",
     ];
   }
 
--- a/browser/components/payments/res/containers/basic-card-form.js
+++ b/browser/components/payments/res/containers/basic-card-form.js
@@ -81,17 +81,17 @@ export default class BasicCardForm exten
       let addresses = [];
       this.formHandler = new EditCreditCard({
         form,
       }, record, addresses, {
         isCCNumber: PaymentDialogUtils.isCCNumber,
         getAddressLabel: PaymentDialogUtils.getAddressLabel,
       });
 
-      // The EditCreditCard constructor adds input event listeners on the same element,
+      // The EditCreditCard constructor adds `input` event listeners on the same element,
       // which update field validity. By adding our event listeners after this constructor,
       // validity will be updated before our handlers get the event
       form.addEventListener("input", this);
       form.addEventListener("invalid", this);
 
       let fragment = document.createDocumentFragment();
       fragment.append(this.addressAddLink);
       fragment.append(" ");
--- a/browser/components/payments/res/containers/form.css
+++ b/browser/components/payments/res/containers/form.css
@@ -1,7 +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/. */
 
+div[required] > label > span:first-of-type::after,
 :-moz-any(label, div)[required] > span:first-of-type::after {
   content: attr(fieldRequiredSymbol);
 }
--- a/browser/components/payments/res/debugging.js
+++ b/browser/components/payments/res/debugging.js
@@ -179,81 +179,104 @@ let REQUEST_2 = {
 };
 
 let ADDRESSES_1 = {
   "48bnds6854t": {
     "address-level1": "MI",
     "address-level2": "Some City",
     "country": "US",
     "email": "foo@bar.com",
+    "family-name": "Smith",
+    "given-name": "John",
     "guid": "48bnds6854t",
-    "name": "Mr. Foo",
+    "name": "John Smith",
     "postal-code": "90210",
     "street-address": "123 Sesame Street,\nApt 40",
     "tel": "+1 519 555-5555",
   },
   "68gjdh354j": {
+    "additional-name": "Z.",
     "address-level1": "CA",
     "address-level2": "Mountain View",
     "country": "US",
+    "family-name": "Doe",
+    "given-name": "Jane",
     "guid": "68gjdh354j",
-    "name": "Mrs. Bar",
+    "name": "Jane Z. Doe",
     "postal-code": "94041",
     "street-address": "P.O. Box 123",
     "tel": "+1 650 555-5555",
   },
   "abcde12345": {
     "address-level2": "Mountain View",
     "country": "US",
     "guid": "abcde12345",
     "name": "Mrs. Fields",
   },
+  "missing-country": {
+    "address-level1": "ON",
+    "address-level2": "Toronto",
+    "family-name": "Bogard",
+    "given-name": "Kristin",
+    "guid": "missing-country",
+    "name": "Kristin Bogard",
+    "postal-code": "H0H 0H0",
+    "street-address": "123 Yonge Street\nSuite 2300",
+    "tel": "+1 416 555-5555",
+  },
 };
 
 let DUPED_ADDRESSES = {
   "a9e830667189": {
     "street-address": "Unit 1\n1505 Northeast Kentucky Industrial Parkway \n",
     "address-level2": "Greenup",
     "address-level1": "KY",
     "postal-code": "41144",
     "country": "US",
     "email": "bob@example.com",
+    "family-name": "Smith",
+    "given-name": "Bob",
     "guid": "a9e830667189",
     "tel": "+19871234567",
     "name": "Bob Smith",
   },
   "72a15aed206d": {
     "street-address": "1 New St",
     "address-level2": "York",
     "address-level1": "SC",
     "postal-code": "29745",
     "country": "US",
+    "given-name": "Mary Sue",
     "guid": "72a15aed206d",
     "tel": "+19871234567",
     "name": "Mary Sue",
     "address-line1": "1 New St",
   },
   "2b4dce0fbc1f": {
     "street-address": "123 Park St",
     "address-level2": "Springfield",
     "address-level1": "OR",
     "postal-code": "97403",
     "country": "US",
     "email": "rita@foo.com",
+    "family-name": "Foo",
+    "given-name": "Rita",
     "guid": "2b4dce0fbc1f",
     "name": "Rita Foo",
     "address-line1": "123 Park St",
   },
   "46b2635a5b26": {
     "street-address": "432 Another St",
     "address-level2": "Springfield",
     "address-level1": "OR",
     "postal-code": "97402",
     "country": "US",
     "email": "rita@foo.com",
+    "family-name": "Foo",
+    "given-name": "Rita",
     "guid": "46b2635a5b26",
     "name": "Rita Foo",
     "address-line1": "432 Another St",
   },
 };
 
 let BASIC_CARDS_1 = {
   "53f9d009aed2": {
@@ -300,16 +323,29 @@ let BASIC_CARDS_1 = {
     "timeLastModified": 1517890564518,
     "timeLastUsed": 0,
     "timesUsed": 0,
     "cc-name": "Jane Fields",
     "cc-given-name": "Jane",
     "cc-additional-name": "",
     "cc-family-name": "Fields",
   },
+  "missing-cc-name": {
+    methodName: "basic-card",
+    "cc-number": "************8563",
+    "guid": "missing-cc-name",
+    "version": 1,
+    "timeCreated": 1517890536491,
+    "timeLastModified": 1517890564518,
+    "timeLastUsed": 0,
+    "timesUsed": 0,
+    "cc-exp-month": 8,
+    "cc-exp-year": 2024,
+    "cc-exp": "2024-08",
+  },
 };
 
 let buttonActions = {
   debugFrame() {
     let event = new CustomEvent("paymentContentToChrome", {
       bubbles: true,
       detail: {
         messageType: "debugFrame",
--- a/browser/components/payments/test/PaymentTestUtils.jsm
+++ b/browser/components/payments/test/PaymentTestUtils.jsm
@@ -443,17 +443,17 @@ var PaymentTestUtils = {
     },
     TimBL2: {
       "given-name": "Timothy",
       "additional-name": "Johann",
       "family-name": "Berners-Lee",
       organization: "World Wide Web Consortium",
       "street-address": "1 Pommes Frittes Place",
       "address-level2": "Berlin",
-      "address-level1": "BE",
+      // address-level1 isn't used in our forms for Germany
       "postal-code": "02138",
       country: "DE",
       tel: "+16172535702",
       email: "timbl@example.org",
     },
     /* Used as a temporary (not persisted in autofill storage) address in tests */
     Temp: {
       "given-name": "Temp",
--- a/browser/components/payments/test/browser/browser.ini
+++ b/browser/components/payments/test/browser/browser.ini
@@ -4,16 +4,17 @@ prefs =
   dom.payments.request.enabled=true
 skip-if = !e10s # Bug 1365964 - Payment Request isn't implemented for non-e10s
 support-files =
   blank_page.html
 
 [browser_address_edit.js]
 skip-if = verify && debug && os == 'mac'
 [browser_card_edit.js]
+skip-if = os == 'linux' && debug # bug 1465673
 [browser_change_shipping.js]
 [browser_dropdowns.js]
 [browser_host_name.js]
 [browser_payment_completion.js]
 [browser_payments_onboarding_wizard.js]
 [browser_profile_storage.js]
 [browser_request_serialization.js]
 [browser_request_shipping.js]
--- a/browser/components/payments/test/browser/browser_address_edit.js
+++ b/browser/components/payments/test/browser/browser_address_edit.js
@@ -46,24 +46,26 @@ add_task(async function test_add_link() 
       is(Object.keys(tempAddresses).length, 0, "No temporary addresses at the start of test");
       is(Object.keys(savedAddresses).length, 1, "1 saved address at the start of test");
     });
 
     let testOptions = [
       { setPersistCheckedValue: true, expectPersist: true },
       { setPersistCheckedValue: false, expectPersist: false },
     ];
-    let newAddress = PTU.Addresses.TimBL2;
+    let newAddress = Object.assign({}, PTU.Addresses.TimBL2);
+    // Emails aren't part of shipping addresses
+    delete newAddress.email;
 
     for (let options of testOptions) {
       let shippingAddressChangePromise = ContentTask.spawn(browser, {
         eventName: "shippingaddresschange",
       }, PTU.ContentTasks.awaitPaymentRequestEventPromise);
 
-      await manuallyAddAddress(frame, newAddress, options);
+      await manuallyAddShippingAddress(frame, newAddress, options);
       await shippingAddressChangePromise;
       info("got shippingaddresschange event");
 
       await spawnPaymentDialogTask(frame, async ({address, options, prefilledGuids}) => {
         let {
           PaymentTestUtils: PTU,
         } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
 
@@ -142,17 +144,17 @@ add_task(async function test_edit_link()
       is(title.textContent, "Edit Shipping Address", "Page title should be set");
     });
 
     let editOptions = {
       checkboxSelector: "#address-page .persist-checkbox",
       isEditing: true,
       expectPersist: true,
     };
-    await fillInAddressForm(frame, EXPECTED_ADDRESS, editOptions);
+    await fillInShippingAddressForm(frame, EXPECTED_ADDRESS, editOptions);
     await verifyPersistCheckbox(frame, editOptions);
     await submitAddressForm(frame, EXPECTED_ADDRESS, editOptions);
 
     await spawnPaymentDialogTask(frame, async (address) => {
       let {
         PaymentTestUtils: PTU,
       } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
 
@@ -235,17 +237,17 @@ add_task(async function test_add_payer_c
 
       info("check that non-payer requested fields are hidden");
       for (let selector of ["#organization", "#tel"]) {
         let element = content.document.querySelector(selector);
         ok(content.isHidden(element), selector + " should be hidden");
       }
     });
 
-    await fillInAddressForm(frame, EXPECTED_ADDRESS, addOptions);
+    await fillInPayerAddressForm(frame, EXPECTED_ADDRESS, addOptions);
     await verifyPersistCheckbox(frame, addOptions);
     await submitAddressForm(frame, EXPECTED_ADDRESS, addOptions);
 
     await spawnPaymentDialogTask(frame, async (address) => {
       let {
         PaymentTestUtils: PTU,
       } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
 
@@ -392,17 +394,19 @@ add_task(async function test_private_per
         methodData: [PTU.MethodData.basicCard],
         details: Object.assign({}, PTU.Details.twoShippingOptions, PTU.Details.total2USD),
         options: PTU.Options.requestShippingOption,
         merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
       }
     );
     info("/setupPaymentDialog");
 
-    let addressToAdd = PTU.Addresses.Temp;
+    let addressToAdd = Object.assign({}, PTU.Addresses.Temp);
+    // Emails aren't part of shipping addresses
+    delete addressToAdd.email;
     const addOptions = {
       checkboxSelector: "#address-page .persist-checkbox",
       expectPersist: false,
       isPrivate: true,
     };
 
     await navigateToAddAddressPage(frame);
     await spawnPaymentDialogTask(frame, async () => {
@@ -417,17 +421,17 @@ add_task(async function test_private_per
     });
 
     info("wait for initialAddresses");
     let initialAddresses =
       await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.getShippingAddresses);
     is(initialAddresses.options.length, 1,
        "Got expected number of pre-filled shipping addresses");
 
-    await fillInAddressForm(frame, addressToAdd, addOptions);
+    await fillInShippingAddressForm(frame, addressToAdd, addOptions);
     await verifyPersistCheckbox(frame, addOptions);
     await submitAddressForm(frame, addressToAdd, addOptions);
 
     let shippingAddresses =
       await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.getShippingAddresses);
     info("shippingAddresses", shippingAddresses);
     let addressOptions = shippingAddresses.options;
     // expect the prefilled address + the new temporary address
--- a/browser/components/payments/test/browser/browser_card_edit.js
+++ b/browser/components/payments/test/browser/browser_card_edit.js
@@ -25,17 +25,17 @@ async function add_link(aOptions = {}) {
     info("add_link, aOptions: " + JSON.stringify(aOptions, null, 2));
     await navigateToAddCardPage(frame);
     info(`add_link, from the add card page,
           verifyPersistCheckbox with expectPersist: ${aOptions.expectDefaultCardPersist}`);
     await verifyPersistCheckbox(frame, {
       checkboxSelector: "basic-card-form .persist-checkbox",
       expectPersist: aOptions.expectDefaultCardPersist,
     });
-    await spawnPaymentDialogTask(frame, async (testArgs = {}) => {
+    await spawnPaymentDialogTask(frame, async function checkState(testArgs = {}) {
       let {
         PaymentTestUtils: PTU,
       } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
 
       let state = await PTU.DialogContentUtils.waitForState(content, (state) => {
         return Object.keys(state.savedBasicCards).length == 1 &&
                Object.keys(state.savedAddresses).length == 1;
       }, "Check no cards or addresses present at beginning of test");
@@ -53,17 +53,17 @@ async function add_link(aOptions = {}) {
     });
     if (aOptions.hasOwnProperty("setCardPersistCheckedValue")) {
       cardOptions.setPersistCheckedValue = aOptions.setCardPersistCheckedValue;
     }
     await fillInCardForm(frame, PTU.BasicCards.JaneMasterCard, cardOptions);
 
     await verifyPersistCheckbox(frame, cardOptions);
 
-    await spawnPaymentDialogTask(frame, async (testArgs = {}) => {
+    await spawnPaymentDialogTask(frame, async function checkBillingAddressPicker(testArgs = {}) {
       let billingAddressSelect = content.document.querySelector("#billingAddressGUID");
       ok(content.isVisible(billingAddressSelect),
          "The billing address selector should always be visible");
       is(billingAddressSelect.childElementCount, 2,
          "Only 2 child options should exist by default");
       is(billingAddressSelect.children[0].value, "",
          "The first option should be the blank/empty option");
       ok(billingAddressSelect.children[1].value, "",
@@ -77,17 +77,17 @@ async function add_link(aOptions = {}) {
       expectPersist: aOptions.expectDefaultAddressPersist,
     });
     if (aOptions.hasOwnProperty("setAddressPersistCheckedValue")) {
       addressOptions.setPersistCheckedValue = aOptions.setAddressPersistCheckedValue;
     }
 
     await navigateToAddAddressPage(frame, addressOptions);
 
-    await spawnPaymentDialogTask(frame, async (testArgs = {}) => {
+    await spawnPaymentDialogTask(frame, async function checkTask(testArgs = {}) {
       let {
         PaymentTestUtils: PTU,
       } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
 
       let title = content.document.querySelector("basic-card-form h2");
       let card = Object.assign({}, PTU.BasicCards.JaneMasterCard);
 
       let addressTitle = content.document.querySelector("address-form h2");
@@ -116,23 +116,23 @@ async function add_link(aOptions = {}) {
         let field = content.document.getElementById(key);
         is(field.value, val, "Field should still have previous value entered");
         ok(!field.disabled, "Fields should still be enabled for editing");
       }
     }, aOptions);
 
     await navigateToAddAddressPage(frame, addressOptions);
 
-    await fillInAddressForm(frame, PTU.Addresses.TimBL2, addressOptions);
+    await fillInBillingAddressForm(frame, PTU.Addresses.TimBL2, addressOptions);
 
     await verifyPersistCheckbox(frame, addressOptions);
 
     await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.clickPrimaryButton);
 
-    await spawnPaymentDialogTask(frame, async (testArgs = {}) => {
+    await spawnPaymentDialogTask(frame, async function checkCardPage(testArgs = {}) {
       let {
         PaymentTestUtils: PTU,
       } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
 
       let state = await PTU.DialogContentUtils.waitForState(content, (state) => {
         return state.page.id == "basic-card-page" && !state["basic-card-page"].guid;
       }, "Check address was added and we're back on basic-card page (add)");
 
@@ -163,31 +163,31 @@ async function add_link(aOptions = {}) {
       checkboxSelector: "basic-card-form .persist-checkbox",
       expectPersist: aOptions.expectCardPersist,
     });
 
     await verifyPersistCheckbox(frame, cardOptions);
 
     await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.clickPrimaryButton);
 
-    await spawnPaymentDialogTask(frame, async (testArgs = {}) => {
+    await spawnPaymentDialogTask(frame, async function waitForSummaryPage(testArgs = {}) {
       let {
         PaymentTestUtils: PTU,
       } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
 
       await PTU.DialogContentUtils.waitForState(content, (state) => {
         return state.page.id == "payment-summary";
       }, "Check we are back on the summary page");
     });
 
     await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.setSecurityCode, {
       securityCode: "123",
     });
 
-    await spawnPaymentDialogTask(frame, async (testArgs = {}) => {
+    await spawnPaymentDialogTask(frame, async function checkCardState(testArgs = {}) {
       let {
         PaymentTestUtils: PTU,
       } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
 
       let {prefilledGuids} = testArgs;
       let card = Object.assign({}, PTU.BasicCards.JaneMasterCard);
       let state = await PTU.DialogContentUtils.getCurrentState(content);
 
@@ -471,23 +471,25 @@ add_task(async function test_edit_link()
                      billingAddressSelect.selectedOptions[0];
     ok(selectedOption && selectedOption.value, "select should have a selected option value");
 
     addressEditLink.click();
     state = await PTU.DialogContentUtils.waitForState(content, (state) => {
       return state.page.id == "address-page" && state["address-page"].guid;
     }, "Check address page state (editing)");
 
-    info("filling address fields");
-    for (let [key, val] of Object.entries(PTU.Addresses.TimBL)) {
+    info("modify some address fields");
+    for (let key of ["given-name", "tel", "organization", "street-address"]) {
       let field = content.document.getElementById(key);
       if (!field) {
         ok(false, `${key} field not found`);
       }
-      field.value = val.slice(0, -1) + "7";
+      field.focus();
+      EventUtils.sendKey("BACK_SPACE", content.window);
+      EventUtils.sendString("7", content.window);
       ok(!field.disabled, `Field #${key} shouldn't be disabled`);
     }
 
     content.document.querySelector("address-form button.save-button").click();
     state = await PTU.DialogContentUtils.waitForState(content, (state) => {
       return state.page.id == "basic-card-page" && state["basic-card-page"].guid &&
              Object.keys(state.savedAddresses).length == 1;
     }, "Check still only one address and we're back on basic-card page");
--- a/browser/components/payments/test/browser/browser_change_shipping.js
+++ b/browser/components/payments/test/browser/browser_change_shipping.js
@@ -95,16 +95,17 @@ add_task(async function test_change_ship
       is(items[1].amountValue, "1.70", "2nd display item has 1.70 value");
 
       // verify the updated modifiers
       items = [...container.querySelectorAll(".footer-items-list payment-details-item")]
               .map(item => Cu.waiveXrays(item));
       is(items.length, 1, "1 additional display item");
       is(items[0].amountCurrency, "EUR", "First display item is in Euros");
       is(items[0].amountValue, "1.00", "First display item has 1.00 value");
+      btn.click();
     });
 
     info("clicking pay");
     spawnPaymentDialogTask(frame, PTU.DialogContentTasks.completePayment);
 
     // Add a handler to complete the payment above.
     info("acknowledging the completion from the merchant page");
     let result = await ContentTask.spawn(browser, {}, PTU.ContentTasks.addCompletionHandler);
--- a/browser/components/payments/test/browser/browser_payments_onboarding_wizard.js
+++ b/browser/components/payments/test/browser/browser_payments_onboarding_wizard.js
@@ -54,26 +54,25 @@ add_task(async function test_onboarding_
       let addressPageTitle = content.document.querySelector("address-form h2");
       ok(content.isVisible(addressPageTitle), "Address page title is visible");
       is(addressPageTitle.textContent, "Add Shipping Address",
          "Address page title is correctly shown");
 
       let addressCancelButton = content.document.querySelector("address-form .cancel-button");
       ok(content.isVisible(addressCancelButton),
          "The cancel button on the address page is visible");
+    });
 
-      for (let [key, val] of Object.entries(PTU.Addresses.TimBL2)) {
-        let field = content.document.getElementById(key);
-        if (!field) {
-          ok(false, `${key} field not found`);
-        }
-        field.value = val;
-        ok(!field.disabled, `Field #${key} shouldn't be disabled`);
-      }
-      content.document.querySelector("address-form .save-button").click();
+    await fillInShippingAddressForm(frame, PTU.Addresses.TimBL2);
+    await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.clickPrimaryButton);
+
+    await spawnPaymentDialogTask(frame, async function() {
+      let {
+        PaymentTestUtils: PTU,
+      } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
 
       await PTU.DialogContentUtils.waitForState(content, (state) => {
         return state.page.id == "basic-card-page";
       }, "Basic card page is shown after the address page during on boarding");
 
       let cardSaveButton = content.document.querySelector("basic-card-form .save-button");
       ok(content.isVisible(cardSaveButton), "Basic card page is rendered");
 
@@ -266,26 +265,26 @@ add_task(async function test_onboarding_
 
       await PTU.DialogContentUtils.waitForState(content, (state) => {
         return state.page.id == "address-page";
       }, "Address page is shown first if there are saved addresses during on boarding");
 
       info("Checking if the address page has been rendered");
       let addressSaveButton = content.document.querySelector("address-form .save-button");
       ok(content.isVisible(addressSaveButton), "Address save button is rendered");
+    });
 
-      for (let [key, val] of Object.entries(PTU.Addresses.TimBL2)) {
-        let field = content.document.getElementById(key);
-        if (!field) {
-          ok(false, `${key} field not found`);
-        }
-        field.value = val;
-        ok(!field.disabled, `Field #${key} shouldn't be disabled`);
-      }
-      content.document.querySelector("address-form .save-button").click();
+
+    await fillInShippingAddressForm(frame, PTU.Addresses.TimBL2);
+    await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.clickPrimaryButton);
+
+    await spawnPaymentDialogTask(frame, async function checkSavedAndCancelButton() {
+      let {
+        PaymentTestUtils: PTU,
+      } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
 
       await PTU.DialogContentUtils.waitForState(content, (state) => {
         return state.page.id == "payment-summary";
       }, "payment-summary is now visible");
 
       let cancelButton = content.document.querySelector("#cancel");
       ok(content.isVisible(cancelButton), "Payment summary page is shown next");
     });
@@ -329,26 +328,25 @@ add_task(async function test_onboarding_
       ok(content.isVisible(addressSaveButton),
          "Address save button is rendered");
 
       info("Check if the page title is visible on the address page");
       let addressPageTitle = content.document.querySelector("address-form h2");
       ok(content.isVisible(addressPageTitle), "Address page title is visible");
       is(addressPageTitle.textContent, "Add Billing Address",
          "Address page title is correctly shown");
+    });
 
-      for (let [key, val] of Object.entries(PTU.Addresses.TimBL2)) {
-        let field = content.document.getElementById(key);
-        if (!field) {
-          ok(false, `${key} field not found`);
-        }
-        field.value = val;
-        ok(!field.disabled, `Field #${key} shouldn't be disabled`);
-      }
-      content.document.querySelector("address-form .save-button").click();
+    await fillInBillingAddressForm(frame, PTU.Addresses.TimBL2);
+    await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.clickPrimaryButton);
+
+    await spawnPaymentDialogTask(frame, async function() {
+      let {
+        PaymentTestUtils: PTU,
+      } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
 
       await PTU.DialogContentUtils.waitForState(content, (state) => {
         return state.page.id == "basic-card-page";
       // eslint-disable-next-line max-len
       }, "Basic card page is shown after the billing address page during onboarding if requestShipping is turned off");
 
       let cardSaveButton = content.document.querySelector("basic-card-form .save-button");
       ok(content.isVisible(cardSaveButton), "Basic card page is rendered");
@@ -438,38 +436,39 @@ add_task(async function test_back_button
 
       await PTU.DialogContentUtils.waitForState(content, (state) => {
         return state.page.id == "address-page";
       }, "Billing address page is shown first if there are no saved addresses " +
          "and requestShipping is false during on boarding");
       info("Checking if the address page has been rendered");
       let addressSaveButton = content.document.querySelector("address-form .save-button");
       ok(content.isVisible(addressSaveButton), "Address save button is rendered");
+    });
 
-      for (let [key, val] of Object.entries(PTU.Addresses.TimBL2)) {
-        let field = content.document.getElementById(key);
-        if (!field) {
-          ok(false, `${key} field not found`);
-        }
-        field.value = val;
-        ok(!field.disabled, `Field #${key} shouldn't be disabled`);
-      }
-      content.document.querySelector("address-form .save-button").click();
+    await fillInBillingAddressForm(frame, PTU.Addresses.TimBL2);
+    await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.clickPrimaryButton);
+
+    await spawnPaymentDialogTask(frame, async function() {
+      let {
+        PaymentTestUtils: PTU,
+      } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
+
+      let addressSaveButton = content.document.querySelector("address-form .save-button");
 
       await PTU.DialogContentUtils.waitForState(content, (state) => {
         return state.page.id == "basic-card-page";
       }, "Basic card page is shown next");
 
       info("Checking if basic card page is rendered");
       let basicCardBackButton = content.document.querySelector("basic-card-form .back-button");
       ok(content.isVisible(basicCardBackButton), "Back button is visible on the basic card page");
 
       info("Partially fill basic card form");
       let field = content.document.getElementById("cc-number");
-      field.value = PTU.BasicCards.JohnDoe["cc-number"];
+      content.fillField(field, PTU.BasicCards.JohnDoe["cc-number"]);
 
       info("Clicking on the back button to edit address saved in the previous step");
       basicCardBackButton.click();
 
       await PTU.DialogContentUtils.waitForState(content, (state) => {
         return state.page.id == "address-page" &&
                state["address-page"].guid == state["basic-card-page"].billingAddressGUID;
       }, "Address page is shown again");
@@ -479,17 +478,17 @@ add_task(async function test_back_button
       ok(content.isVisible(addressSaveButton), "Address save button is rendered");
 
       info("Checking if the address saved in the last step is correctly loaded in the form");
       field = content.document.getElementById("given-name");
       ok(field.value, PTU.Addresses.TimBL2["given-name"],
          "Given name field value is correctly loaded");
 
       info("Editing the address and saving again");
-      field.value = "John";
+      content.fillField(field, "John");
       addressSaveButton.click();
 
       info("Checking if the address was correctly edited");
       await PTU.DialogContentUtils.waitForState(content, (state) => {
         return state.page.id == "basic-card-page" &&
                // eslint-disable-next-line max-len
                state.savedAddresses[state["basic-card-page"].billingAddressGUID]["given-name"] == "John";
       }, "Address was correctly edited and saved");
--- a/browser/components/payments/test/browser/browser_shippingaddresschange_error.js
+++ b/browser/components/payments/test/browser/browser_shippingaddresschange_error.js
@@ -149,16 +149,30 @@ add_task(async function test_show_field_
                              PTU.Details.total2USD),
     }, PTU.ContentTasks.updateWith);
 
     await spawnPaymentDialogTask(frame, async () => {
       let {
         PaymentTestUtils: PTU,
       } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
 
+      // TODO: bug 1482808 - Clear setCustomValidity from merchant errors since
+      // they don't currently ever get cleared.
+      for (let field of content.document.querySelector("address-form form").elements) {
+        if (!field.validity.customError) {
+          continue;
+        }
+        field.setCustomValidity("");
+        todo(false, `Clearing custom validity on #${field.id}`);
+      }
+
+      Cu.waiveXrays(content.document.querySelector("address-form")).updateSaveButtonState();
+
+      // End bug 1482808 TODO
+
       info("saving corrections");
       content.document.querySelector("address-form .save-button").click();
 
       await PTU.DialogContentUtils.waitForState(content, (state) => {
         return state.page.id == "payment-summary";
       }, "Check we're back on summary view");
 
       await PTU.DialogContentUtils.waitForState(content, (state) => {
--- a/browser/components/payments/test/browser/head.js
+++ b/browser/components/payments/test/browser/head.js
@@ -194,17 +194,17 @@ async function addSampleAddressesAndBasi
  * @param {string} msg to describe the check
  */
 function checkPaymentAddressMatchesStorageAddress(paymentAddress, storageAddress, msg) {
   info(msg);
   let addressLines = storageAddress["street-address"].split("\n");
   is(paymentAddress.addressLine[0], addressLines[0], "Address line 1 should match");
   is(paymentAddress.addressLine[1], addressLines[1], "Address line 2 should match");
   is(paymentAddress.country, storageAddress.country, "Country should match");
-  is(paymentAddress.region, storageAddress["address-level1"], "Region should match");
+  is(paymentAddress.region, storageAddress["address-level1"] || "", "Region should match");
   is(paymentAddress.city, storageAddress["address-level2"], "City should match");
   is(paymentAddress.postalCode, storageAddress["postal-code"], "Zip code should match");
   is(paymentAddress.organization, storageAddress.organization, "Org should match");
   is(paymentAddress.recipient,
      `${storageAddress["given-name"]} ${storageAddress["additional-name"]} ` +
      `${storageAddress["family-name"]}`,
      "Recipient name should match");
   is(paymentAddress.phone, storageAddress.tel, "Phone should match");
@@ -261,16 +261,36 @@ async function setupPaymentDialog(browse
   await dialogReadyPromise;
   info("dialog ready");
 
   await spawnPaymentDialogTask(frame, () => {
     let elementHeight = (element) =>
       element.getBoundingClientRect().height;
     content.isHidden = (element) => elementHeight(element) == 0;
     content.isVisible = (element) => elementHeight(element) > 0;
+    content.fillField = async function fillField(field, value) {
+      // Keep in-sync with the copy in payments_common.js but with EventUtils methods called on a
+      // EventUtils object.
+      field.focus();
+      if (field.localName == "select") {
+        if (field.value == value) {
+          // Do nothing
+          return;
+        }
+        field.value = value;
+        field.dispatchEvent(new content.window.Event("input"));
+        field.dispatchEvent(new content.window.Event("change"));
+        return;
+      }
+      while (field.value) {
+        EventUtils.sendKey("BACK_SPACE", content.window);
+      }
+      EventUtils.sendString(value, content.window);
+    }
+;
   });
   await injectEventUtilsInContentTask(frame);
   info("helper functions injected into frame");
 
   return {win, requestId, frame};
 }
 
 /**
@@ -319,17 +339,17 @@ add_task(async function setup_head() {
     if (msg.isWarning || !msg.errorMessage) {
       // Ignore warnings and non-errors.
       return;
     }
     if (msg.category == "CSP_CSPViolationWithURI" && msg.errorMessage.includes("at inline")) {
       // Ignore unknown CSP error.
       return;
     }
-    if (msg.message.match(/docShell is null.*BrowserUtils.jsm/)) {
+    if (msg.message && msg.message.match(/docShell is null.*BrowserUtils.jsm/)) {
       // Bug 1478142 - Console spam from the Find Toolbar.
       return;
     }
     ok(false, msg.message || msg.errorMessage);
   });
   await setupFormAutofillStorage();
   registerCleanupFunction(function cleanup() {
     paymentSrv.cleanup();
@@ -347,17 +367,17 @@ function deepClone(obj) {
 
 async function selectPaymentDialogShippingAddressByCountry(frame, country) {
   await spawnPaymentDialogTask(frame,
                                PTU.DialogContentTasks.selectShippingAddressByCountry,
                                country);
 }
 
 async function navigateToAddAddressPage(frame, aOptions = {
-  addLinkSelector: "address-picker a.add-link",
+  addLinkSelector: "address-picker[selected-state-key=\"selectedShippingAddress\"] a.add-link",
   initialPageId: "payment-summary",
 }) {
   await spawnPaymentDialogTask(frame, async (options) => {
     let {
       PaymentTestUtils,
     } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
 
     info("navigateToAddAddressPage: check were on the expected page first");
@@ -373,34 +393,69 @@ async function navigateToAddAddressPage(
 
     info("navigateToAddAddressPage: wait for address page");
     await PaymentTestUtils.DialogContentUtils.waitForState(content, (state) => {
       return state.page.id == "address-page" && !state.page.guid;
     }, "Check add page state");
   }, aOptions);
 }
 
+async function fillInBillingAddressForm(frame, aAddress) {
+  // For now billing and shipping address forms have the same fields but that may
+  // change so use separarate helpers.
+  return fillInShippingAddressForm(frame, aAddress);
+}
+
+async function fillInShippingAddressForm(frame, aAddress, aOptions) {
+  let address = Object.assign({}, aAddress);
+  // Email isn't used on address forms, only payer/contact ones.
+  delete address.email;
+  return fillInAddressForm(frame, address, aOptions);
+}
+
+async function fillInPayerAddressForm(frame, aAddress) {
+  let address = Object.assign({}, aAddress);
+  let payerFields = ["given-name", "additional-name", "family-name", "tel", "email"];
+  for (let fieldName of Object.keys(address)) {
+    if (payerFields.includes(fieldName)) {
+      continue;
+    }
+    delete address[fieldName];
+  }
+  return fillInAddressForm(frame, address);
+}
+
 async function fillInAddressForm(frame, aAddress, aOptions = {}) {
   await spawnPaymentDialogTask(frame, async (args) => {
     let {address, options = {}} = args;
 
+    if (typeof(address.country) != "undefined") {
+      // Set the country first so that the appropriate fields are visible.
+      let countryField = content.document.getElementById("country");
+      ok(!countryField.disabled, "Country Field shouldn't be disabled");
+      await content.fillField(countryField, address.country);
+      is(countryField.value, address.country, "country value is correct after fillField");
+    }
+
     // fill the form
-    info("manuallyAddAddress: fill the form with address: " + JSON.stringify(address));
+    info("fillInAddressForm: fill the form with address: " + JSON.stringify(address));
     for (let [key, val] of Object.entries(address)) {
       let field = content.document.getElementById(key);
       if (!field) {
         ok(false, `${key} field not found`);
       }
-      field.value = val;
+      ok(!field.disabled, `Field #${key} shouldn't be disabled`);
+      await content.fillField(field, val);
+      is(field.value, val, `${key} value is correct after fillField`);
     }
     let persistCheckbox = Cu.waiveXrays(
-        content.document.querySelector(options.checkboxSelector));
+        content.document.querySelector("#address-page .persist-checkbox"));
     // only touch the checked state if explicitly told to in the options
     if (options.hasOwnProperty("setPersistCheckedValue")) {
-      info("fillInCardForm: Manually setting the persist checkbox checkedness to: " +
+      info("fillInAddressForm: Manually setting the persist checkbox checkedness to: " +
             options.setPersistCheckedValue);
       Cu.waiveXrays(persistCheckbox).checked = options.setPersistCheckedValue;
     }
     info(`fillInAddressForm, persistCheckbox.checked: ${persistCheckbox.checked}`);
   }, {address: aAddress, options: aOptions});
 }
 
 async function verifyPersistCheckbox(frame, aOptions = {}) {
@@ -430,17 +485,17 @@ async function submitAddressForm(frame, 
 
     let oldAddresses = await PaymentTestUtils.DialogContentUtils.getCurrentState(content);
 
     // submit the form to return to summary page
     content.document.querySelector("address-form button:last-of-type").click();
 
     let currState = await PaymentTestUtils.DialogContentUtils.waitForState(content, (state) => {
       return state.page.id == "payment-summary";
-    }, "Switched back to payment-summary");
+    }, "submitAddressForm: Switched back to payment-summary");
 
     let savedCount = Object.keys(currState.savedAddresses).length;
     let tempCount = Object.keys(currState.tempAddresses).length;
     let oldSavedCount = Object.keys(oldAddresses.savedAddresses).length;
     let oldTempCount = Object.keys(oldAddresses.tempAddresses).length;
 
     if (options.isEditing) {
       is(tempCount, oldTempCount, "tempAddresses count didn't change");
@@ -450,27 +505,28 @@ async function submitAddressForm(frame, 
       is(savedCount, oldSavedCount + 1, "Entry added to savedAddresses");
     } else {
       is(tempCount, oldTempCount + 1, "Entry added to tempAddresses");
       is(savedCount, oldSavedCount, "savedAddresses count didn't change");
     }
   }, {address: aAddress, options: aOptions});
 }
 
-async function manuallyAddAddress(frame, aAddress, aOptions = {}) {
+async function manuallyAddShippingAddress(frame, aAddress, aOptions = {}) {
   let options = Object.assign({
     expectPersist: true,
     isEditing: false,
   }, aOptions, {
     checkboxSelector: "#address-page .persist-checkbox",
   });
   await navigateToAddAddressPage(frame);
-  info("manuallyAddAddress, fill in address form with options: " + JSON.stringify(options));
-  await fillInAddressForm(frame, aAddress, options);
-  info("manuallyAddAddress, verifyPersistCheckbox with options: " + JSON.stringify(options));
+  info("manuallyAddShippingAddress, fill in address form with options: " + JSON.stringify(options));
+  await fillInShippingAddressForm(frame, aAddress, options);
+  info("manuallyAddShippingAddress, verifyPersistCheckbox with options: " +
+       JSON.stringify(options));
   await verifyPersistCheckbox(frame, options);
   await submitAddressForm(frame, aAddress, options);
 }
 
 async function navigateToAddCardPage(frame, aOptions = {
   addLinkSelector: "payment-method-picker .add-link",
 }) {
   await spawnPaymentDialogTask(frame, async (options) => {
--- a/browser/components/payments/test/mochitest/payments_common.js
+++ b/browser/components/payments/test/mochitest/payments_common.js
@@ -1,12 +1,12 @@
 "use strict";
 
 /* exported asyncElementRendered, promiseStateChange, promiseContentToChromeMessage, deepClone,
-   PTU, registerConsoleFilter */
+   PTU, registerConsoleFilter, fillField */
 
 const PTU = SpecialPowers.Cu.import("resource://testing-common/PaymentTestUtils.jsm", {})
                             .PaymentTestUtils;
 
 /**
  * A helper to await on while waiting for an asynchronous rendering of a Custom
  * Element.
  * @returns {Promise}
@@ -43,16 +43,40 @@ function promiseContentToChromeMessage(m
   });
 }
 
 function deepClone(obj) {
   return JSON.parse(JSON.stringify(obj));
 }
 
 /**
+ * @param {HTMLElement} field
+ * @param {string} value
+ * @note This is async in case we need to make it async to handle focus in the future.
+ * @note Keep in sync with the copy in head.js
+ */
+async function fillField(field, value) {
+  field.focus();
+  if (field.localName == "select") {
+    if (field.value == value) {
+      // Do nothing
+      return;
+    }
+    field.value = value;
+    field.dispatchEvent(new Event("input"));
+    field.dispatchEvent(new Event("change"));
+    return;
+  }
+  while (field.value) {
+    sendKey("BACK_SPACE");
+  }
+  sendString(value);
+}
+
+/**
  * If filterFunction is a function which returns true given a console message
  * then the test won't fail from that message.
  */
 let filterFunction = null;
 function registerConsoleFilter(filterFn) {
   filterFunction = filterFn;
 }
 
--- a/browser/components/payments/test/mochitest/test_address_form.html
+++ b/browser/components/payments/test/mochitest/test_address_form.html
@@ -54,21 +54,17 @@ function checkAddressForm(customEl, expe
     let expectedVal = expectedAddress[propName] || "";
     is(document.getElementById(propName).value,
        expectedVal.toString(),
        `Check ${propName}`);
   }
 }
 
 function sendStringAndCheckValidity(element, string, isValid) {
-  element.focus();
-  while (element.value) {
-    sendKey("BACK_SPACE");
-  }
-  sendString(string);
+  fillField(element, string);
   ok(element.checkValidity() == isValid,
      `${element.id} should be ${isValid ? "valid" : "invalid"} (${string})`);
 }
 
 add_task(async function test_initialState() {
   let form = new AddressForm();
   let {page} = form.requestStore.getState();
   is(page.id, "payment-summary", "Check initial page");
@@ -110,35 +106,36 @@ add_task(async function test_backButton(
 add_task(async function test_saveButton() {
   let form = new AddressForm();
   form.dataset.addButtonLabel = "Add";
   form.dataset.errorGenericSave = "Generic error";
   await form.promiseReady;
   display.appendChild(form);
   await asyncElementRendered();
 
-  form.form.querySelector("#given-name").focus();
-  sendString("Jaws");
-  form.form.querySelector("#family-name").focus();
-  sendString("Swaj");
-  form.form.querySelector("#organization").focus();
-  sendString("Allizom");
-  form.form.querySelector("#street-address").focus();
-  sendString("404 Internet Super Highway");
-  form.form.querySelector("#address-level2").focus();
-  sendString("Firefoxity City");
-  form.form.querySelector("#address-level1").focus();
-  sendString("CA");
-  form.form.querySelector("#postal-code").focus();
-  sendString("00001");
-  form.form.querySelector("#country option[value='US']").selected = true;
-  form.form.querySelector("#email").focus();
-  sendString("test@example.com");
-  form.form.querySelector("#tel").focus();
-  sendString("+15555551212");
+  ok(form.saveButton.disabled, "Save button initially disabled");
+  fillField(form.form.querySelector("#given-name"), "Jaws");
+  fillField(form.form.querySelector("#family-name"), "Swaj");
+  fillField(form.form.querySelector("#organization"), "Allizom");
+  fillField(form.form.querySelector("#street-address"), "404 Internet Super Highway");
+  fillField(form.form.querySelector("#address-level2"), "Firefoxity City");
+  fillField(form.form.querySelector("#address-level1"), "CA");
+  fillField(form.form.querySelector("#postal-code"), "00001");
+  fillField(form.form.querySelector("#country"), "US");
+  fillField(form.form.querySelector("#email"), "test@example.com");
+  fillField(form.form.querySelector("#tel"), "+15555551212");
+
+  ok(!form.saveButton.disabled, "Save button is enabled after filling");
+
+  info("blanking the street-address");
+  fillField(form.form.querySelector("#street-address"), "");
+  ok(form.saveButton.disabled, "Save button is disabled after blanking street-address");
+
+  fillField(form.form.querySelector("#street-address"), "404 Internet Super Highway");
+  ok(!form.saveButton.disabled, "Save button is enabled after re-filling street-address");
 
   let messagePromise = promiseContentToChromeMessage("updateAutofillRecord");
   is(form.saveButton.textContent, "Add", "Check label");
   form.saveButton.scrollIntoView();
   synthesizeMouseAtCenter(form.saveButton, {});
 
   let details = await messagePromise;
   ok(typeof(details.messageID) == "number" && details.messageID > 0, "Check messageID type");
@@ -153,17 +150,16 @@ add_task(async function test_saveButton(
       "family-name": "Swaj",
       "additional-name": "",
       "organization": "Allizom",
       "street-address": "404 Internet Super Highway",
       "address-level2": "Firefoxity City",
       "address-level1": "CA",
       "postal-code": "00001",
       "country": "US",
-      "email": "test@example.com",
       "tel": "+15555551212",
     },
   }, "Check event details for the message to chrome");
   form.remove();
 });
 
 add_task(async function test_genericError() {
   let form = new AddressForm();
@@ -201,16 +197,18 @@ add_task(async function test_edit() {
     },
     savedAddresses: {
       [address1.guid]: deepClone(address1),
     },
   });
   await asyncElementRendered();
   checkAddressForm(form, address1);
 
+  ok(!form.saveButton.disabled, "Save button should be enabled upon edit for a valid address");
+
   info("test change to minimal record");
   let minimalAddress = {
     "given-name": address1["given-name"],
     guid: "9gnjdhen46",
   };
   await form.requestStore.setState({
     page: {
       id: "address-page",
@@ -220,38 +218,46 @@ add_task(async function test_edit() {
     },
     savedAddresses: {
       [minimalAddress.guid]: deepClone(minimalAddress),
     },
   });
   await asyncElementRendered();
   is(form.saveButton.textContent, "Update", "Check label");
   checkAddressForm(form, minimalAddress);
+  ok(form.saveButton.disabled, "Save button should be disabled if only the name is filled");
 
   info("change to no selected address");
   await form.requestStore.setState({
     page: {
       id: "address-page",
     },
     "address-page": {},
   });
   await asyncElementRendered();
   checkAddressForm(form, {});
+  ok(form.saveButton.disabled, "Save button should be disabled for an empty form");
 
   form.remove();
 });
 
 add_task(async function test_restricted_address_fields() {
   let form = new AddressForm();
   form.dataset.addButtonLabel = "Add";
   form.dataset.errorGenericSave = "Generic error";
   await form.promiseReady;
   display.appendChild(form);
+  await form.requestStore.setState({
+    "address-page": {
+      addressFields: "name email tel",
+    },
+  });
   await asyncElementRendered();
-  form.setAttribute("address-fields", "name email tel");
+
+  ok(form.saveButton.disabled, "Save button should be disabled due to empty fields");
 
   ok(!isHidden(form.form.querySelector("#given-name")),
      "given-name should be visible");
   ok(!isHidden(form.form.querySelector("#additional-name")),
      "additional-name should be visible");
   ok(!isHidden(form.form.querySelector("#family-name")),
      "family-name should be visible");
   ok(isHidden(form.form.querySelector("#organization")),
@@ -266,17 +272,28 @@ add_task(async function test_restricted_
      "postal-code should be hidden");
   ok(isHidden(form.form.querySelector("#country")),
      "country should be hidden");
   ok(!isHidden(form.form.querySelector("#email")),
      "email should be visible");
   ok(!isHidden(form.form.querySelector("#tel")),
      "tel should be visible");
 
+  fillField(form.form.querySelector("#given-name"), "John");
+  ok(form.saveButton.disabled, "Save button should be disabled due to empty fields");
+  fillField(form.form.querySelector("#email"), "john@example.com");
+  todo(form.saveButton.disabled,
+       "Save button should be disabled due to empty fields - Bug 1483412");
+  fillField(form.form.querySelector("#tel"), "+15555555555");
+  ok(!form.saveButton.disabled, "Save button should be enabled with all required fields filled");
+
   form.remove();
+  await form.requestStore.setState({
+    "address-page": {},
+  });
 });
 
 add_task(async function test_field_validation() {
   let form = new AddressForm();
   form.dataset.fieldRequiredSymbol = "*";
   await form.promiseReady;
   display.appendChild(form);
   await asyncElementRendered();
@@ -293,40 +310,40 @@ add_task(async function test_field_valid
     form.form.querySelector("#given-name"),
     form.form.querySelector("#street-address"),
     form.form.querySelector("#address-level2"),
     postalCodeInput,
     addressLevel1Input,
     countrySelect,
   ];
   for (let field of requiredFields) {
-    let container = field.closest("label");
-    ok(container.hasAttribute("required"), "Container should have required attribute");
+    let container = field.closest(`#${field.id}-container`);
+    ok(container.hasAttribute("required"), `#${field.id} container should have required attribute`);
     let span = container.querySelector("span");
     is(span.getAttribute("fieldRequiredSymbol"), "*",
        "span should have asterisk as fieldRequiredSymbol");
     is(getComputedStyle(span, "::after").content, "attr(fieldRequiredSymbol)",
        "Asterisk should be on " + field.id);
   }
 
-  countrySelect.selectedIndex = [...countrySelect.options].findIndex(o => o.value == "US");
-  countrySelect.dispatchEvent(new Event("change"));
+  ok(form.saveButton.disabled, "Save button should be disabled upon load");
+
+  fillField(countrySelect, "US");
 
   sendStringAndCheckValidity(addressLevel1Input, "MI", true);
   sendStringAndCheckValidity(addressLevel1Input, "", false);
   sendStringAndCheckValidity(postalCodeInput, "B4N4N4", false);
   sendStringAndCheckValidity(addressLevel1Input, "Nova Scotia", true);
   sendStringAndCheckValidity(postalCodeInput, "R3J 3C7", false);
   sendStringAndCheckValidity(addressLevel1Input, "", false);
   sendStringAndCheckValidity(postalCodeInput, "11109", true);
   sendStringAndCheckValidity(addressLevel1Input, "Nova Scotia", true);
   sendStringAndCheckValidity(postalCodeInput, "06390-0001", true);
 
-  countrySelect.selectedIndex = [...countrySelect.options].findIndex(o => o.value == "CA");
-  countrySelect.dispatchEvent(new Event("change"));
+  fillField(countrySelect, "CA");
 
   sendStringAndCheckValidity(postalCodeInput, "00001", false);
   sendStringAndCheckValidity(addressLevel1Input, "CA", true);
   sendStringAndCheckValidity(postalCodeInput, "94043", false);
   sendStringAndCheckValidity(addressLevel1Input, "", false);
   sendStringAndCheckValidity(postalCodeInput, "B4N4N4", true);
   sendStringAndCheckValidity(addressLevel1Input, "MI", true);
   sendStringAndCheckValidity(postalCodeInput, "R3J 3C7", true);
@@ -368,25 +385,29 @@ add_task(async function test_customValid
   await asyncElementRendered();
 
   function checkValidationMessage(selector, property) {
     is(form.form.querySelector(selector).validationMessage,
        state.request.paymentDetails.shippingAddressErrors[property],
        "Validation message should match for " + selector);
   }
 
+  ok(form.saveButton.disabled, "Save button should be disabled due to validation errors");
+
   checkValidationMessage("#street-address", "addressLine");
   checkValidationMessage("#address-level2", "city");
   checkValidationMessage("#country", "country");
   checkValidationMessage("#organization", "organization");
   checkValidationMessage("#tel", "phone");
   checkValidationMessage("#postal-code", "postalCode");
   checkValidationMessage("#given-name", "recipient");
   checkValidationMessage("#address-level1", "region");
 
+  // TODO: bug 1482808 - the save button should be enabled after editing the fields
+
   form.remove();
 });
 
 add_task(async function test_field_validation() {
   sinon.stub(PaymentDialogUtils, "getFormFormat").returns({
     addressLevel1Label: "state",
     postalCodeLabel: "US",
     fieldsOrder: [
@@ -411,16 +432,18 @@ add_task(async function test_field_valid
         shippingAddressErrors: {},
       },
     },
   };
   await form.requestStore.setState(state);
   display.appendChild(form);
   await asyncElementRendered();
 
+  ok(form.saveButton.disabled, "Save button should be disabled due to empty fields");
+
   let postalCodeInput = form.form.querySelector("#postal-code");
   let addressLevel1Input = form.form.querySelector("#address-level1");
   ok(!postalCodeInput.value, "postal-code should be empty by default");
   ok(!addressLevel1Input.value, "address-level1 should be empty by default");
   ok(postalCodeInput.checkValidity(),
      "postal-code should be valid by default when it is not visible");
   ok(addressLevel1Input.checkValidity(),
      "address-level1 should be valid by default when it is not visible");
--- a/browser/extensions/formautofill/content/autofillEditForms.js
+++ b/browser/extensions/formautofill/content/autofillEditForms.js
@@ -109,37 +109,92 @@ class EditAddress extends EditAutofillFo
         country: this.supportedCountries.find(supported => supported == this.DEFAULT_REGION),
       };
     }
     super.loadRecord(record);
     this.formatForm(record.country);
   }
 
   /**
+   * `mailing-address` is a special attribute token to indicate mailing fields + country.
+   *
+   * @param {object[]} mailingFieldsOrder - `fieldsOrder` from `getFormFormat`
+   * @returns {object[]} in the same structure as `mailingFieldsOrder` but including non-mail fields
+   */
+  computeVisibleFields(mailingFieldsOrder) {
+    let addressFields = this._elements.form.dataset.addressFields;
+    if (addressFields) {
+      let requestedFieldClasses = addressFields.trim().split(/\s+/);
+      let fieldClasses = [];
+      if (requestedFieldClasses.includes("mailing-address")) {
+        fieldClasses = fieldClasses.concat(mailingFieldsOrder);
+        // `country` isn't part of the `mailingFieldsOrder` so add it when filling a mailing-address
+        requestedFieldClasses.splice(requestedFieldClasses.indexOf("mailing-address"), 1,
+                                     "country");
+      }
+
+      for (let fieldClassName of requestedFieldClasses) {
+        fieldClasses.push({
+          fieldId: fieldClassName,
+          newLine: fieldClassName == "name",
+        });
+      }
+      return fieldClasses;
+    }
+
+    // This is the default which is shown in the management interface and includes all fields.
+    return mailingFieldsOrder.concat([
+      {
+        fieldId: "country",
+      },
+      {
+        fieldId: "tel",
+      },
+      {
+        fieldId: "email",
+        newLine: true,
+      },
+    ]);
+  }
+
+  /**
    * Format the form based on country. The address-level1 and postal-code labels
    * should be specific to the given country.
    * @param  {string} country
    */
   formatForm(country) {
-    const {addressLevel1Label, postalCodeLabel, fieldsOrder, postalCodePattern} =
-      this.getFormFormat(country);
+    const {
+      addressLevel1Label,
+      postalCodeLabel,
+      fieldsOrder: mailingFieldsOrder,
+      postalCodePattern,
+    } = this.getFormFormat(country);
     this._elements.addressLevel1Label.dataset.localization = addressLevel1Label;
     this._elements.postalCodeLabel.dataset.localization = postalCodeLabel;
-    this.arrangeFields(fieldsOrder);
+    let fieldClasses = this.computeVisibleFields(mailingFieldsOrder);
+    this.arrangeFields(fieldClasses);
     this.updatePostalCodeValidation(postalCodePattern);
   }
 
+  /**
+   * Update address field visibility and order based on libaddressinput data.
+   *
+   * @param {object[]} fieldsOrder array of objects with `fieldId` and optional `newLine` properties
+   */
   arrangeFields(fieldsOrder) {
     let fields = [
       "name",
       "organization",
       "street-address",
       "address-level2",
       "address-level1",
       "postal-code",
+      "country",
+      "tel",
+      "email",
     ];
     let inputs = [];
     for (let i = 0; i < fieldsOrder.length; i++) {
       let {fieldId, newLine} = fieldsOrder[i];
       let container = this._elements.form.querySelector(`#${fieldId}-container`);
       let containerInputs = [...container.querySelectorAll("input, textarea, select")];
       containerInputs.forEach(function(input) { input.disabled = false; });
       inputs.push(...containerInputs);
--- a/browser/extensions/formautofill/content/editAddress.xhtml
+++ b/browser/extensions/formautofill/content/editAddress.xhtml
@@ -17,63 +17,65 @@
   <script src="chrome://formautofill/content/autofillEditForms.js"></script>
 </head>
 <body dir="&locale.dir;">
   <form id="form" autocomplete="off">
     <div>
       <div id="name-container">
         <label id="given-name-container">
           <span data-localization="givenName"/>
-          <input id="given-name" type="text" required="true"/>
+          <input id="given-name" type="text" required="required"/>
         </label>
         <label id="additional-name-container">
           <span data-localization="additionalName"/>
           <input id="additional-name" type="text"/>
         </label>
         <label id="family-name-container">
           <span data-localization="familyName"/>
           <input id="family-name" type="text"/>
         </label>
       </div>
       <label id="organization-container">
         <span data-localization="organization2"/>
         <input id="organization" type="text"/>
       </label>
       <label id="street-address-container">
         <span data-localization="streetAddress"/>
-        <textarea id="street-address" rows="3" required="true"/>
+        <textarea id="street-address" rows="3" required="required"/>
       </label>
       <label id="address-level2-container">
         <span data-localization="city"/>
-        <input id="address-level2" type="text" required="true"/>
+        <input id="address-level2" type="text" required="required"/>
       </label>
       <label id="address-level1-container">
         <span/>
-        <input id="address-level1" type="text" required="true"/>
+        <input id="address-level1" type="text" required="required"/>
       </label>
       <label id="postal-code-container">
         <span/>
-        <input id="postal-code" type="text" required="true"/>
+        <input id="postal-code" type="text" required="required"/>
+      </label>
+      <div id="country-container">
+        <label id="country-label">
+          <span data-localization="country"/>
+          <select id="country" required="required">
+            <option/>
+          </select>
+        </label>
+        <p id="country-warning-message" data-localization="countryWarningMessage2"/>
+      </div>
+      <label id="tel-container">
+        <span data-localization="tel"/>
+        <input id="tel" type="tel"/>
+      </label>
+      <label id="email-container">
+        <span data-localization="email"/>
+        <input id="email" type="email" required="required"/>
       </label>
     </div>
-    <label id="country-container">
-      <span data-localization="country"/>
-      <select id="country" required="true">
-        <option/>
-      </select>
-    </label>
-    <p id="country-warning-message" data-localization="countryWarningMessage2"/>
-    <label id="email-container">
-      <span data-localization="email"/>
-      <input id="email" type="email"/>
-    </label>
-    <label id="tel-container">
-      <span data-localization="tel"/>
-      <input id="tel" type="tel"/>
-    </label>
   </form>
   <div id="controls-container">
     <button id="cancel" data-localization="cancelBtnLabel"/>
     <button id="save" disabled="disabled" data-localization="saveBtnLabel"/>
   </div>
   <script type="application/javascript"><![CDATA[
     "use strict";
 
--- a/browser/extensions/formautofill/skin/shared/editAddress.css
+++ b/browser/extensions/formautofill/skin/shared/editAddress.css
@@ -16,31 +16,32 @@ select {
   width: calc(50% - 9.5em);
   margin: 0;
 }
 
 #given-name-container,
 #additional-name-container,
 #address-level1-container,
 #postal-code-container,
-#country-container,
+#country-label,
 #country-warning-message,
 #family-name-container,
 #organization-container,
 #address-level2-container,
 #tel-container {
   flex: 0 1 50%;
 }
 
 #tel-container {
   padding-inline-end: 50%;
 }
 
 #name-container,
 #street-address-container,
+#country-container,
 #email-container {
   flex: 0 1 100%;
 }
 
 #street-address,
 #email {
   flex: 1 0 auto;
 }
--- a/browser/extensions/formautofill/test/browser/browser_editAddressDialog.js
+++ b/browser/extensions/formautofill/test/browser/browser_editAddressDialog.js
@@ -69,19 +69,19 @@ add_task(async function test_saveAddress
       TEST_ADDRESS_1["address-level2"],
       "VK_TAB",
       TEST_ADDRESS_1["address-level1"],
       "VK_TAB",
       TEST_ADDRESS_1["postal-code"],
       "VK_TAB",
       TEST_ADDRESS_1.country,
       "VK_TAB",
-      TEST_ADDRESS_1.email,
+      TEST_ADDRESS_1.tel,
       "VK_TAB",
-      TEST_ADDRESS_1.tel,
+      TEST_ADDRESS_1.email,
       "VK_TAB",
       "VK_TAB",
       "VK_RETURN",
     ];
     keyInputs.forEach(input => EventUtils.synthesizeKey(input, {}, win));
   });
   let addresses = await getAddresses();
 
@@ -140,19 +140,19 @@ add_task(async function test_saveAddress
       TEST_ADDRESS_CA_1["address-level2"],
       "VK_TAB",
       TEST_ADDRESS_CA_1["address-level1"],
       "VK_TAB",
       TEST_ADDRESS_CA_1["postal-code"],
       "VK_TAB",
       TEST_ADDRESS_CA_1.country,
       "VK_TAB",
-      TEST_ADDRESS_CA_1.email,
+      TEST_ADDRESS_CA_1.tel,
       "VK_TAB",
-      TEST_ADDRESS_CA_1.tel,
+      TEST_ADDRESS_CA_1.email,
       "VK_TAB",
       "VK_TAB",
       "VK_RETURN",
     ];
     keyInputs.forEach(input => EventUtils.synthesizeKey(input, {}, win));
   });
   let addresses = await getAddresses();
   for (let [fieldName, fieldValue] of Object.entries(TEST_ADDRESS_CA_1)) {
@@ -188,19 +188,19 @@ add_task(async function test_saveAddress
       TEST_ADDRESS_DE_1["street-address"],
       "VK_TAB",
       TEST_ADDRESS_DE_1["postal-code"],
       "VK_TAB",
       TEST_ADDRESS_DE_1["address-level2"],
       "VK_TAB",
       TEST_ADDRESS_DE_1.country,
       "VK_TAB",
-      TEST_ADDRESS_DE_1.email,
+      TEST_ADDRESS_DE_1.tel,
       "VK_TAB",
-      TEST_ADDRESS_DE_1.tel,
+      TEST_ADDRESS_DE_1.email,
       "VK_TAB",
       "VK_TAB",
       "VK_RETURN",
     ];
     keyInputs.forEach(input => EventUtils.synthesizeKey(input, {}, win));
   });
   let addresses = await getAddresses();
   for (let [fieldName, fieldValue] of Object.entries(TEST_ADDRESS_DE_1)) {
deleted file mode 100644
--- a/browser/extensions/fxmonitor/manifest.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
-  "manifest_version": 2,
-  "name": "Firefox Monitor",
-  "version": "2.0",
-  "applications": {
-    "gecko": {
-      "id": "fxmonitor@mozilla.org",
-      "strict_min_version": "62.0"
-    }
-  }
-}
deleted file mode 100644
--- a/browser/extensions/fxmonitor/moz.build
+++ /dev/null
@@ -1,12 +0,0 @@
-# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
-# 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/.
-
-with Files("**"):
-    BUG_COMPONENT = ("Firefox", "Firefox Monitor")
-
-FINAL_TARGET_FILES.features['fxmonitor@mozilla.org'] += [
-  'manifest.json'
-]
--- a/browser/extensions/moz.build
+++ b/browser/extensions/moz.build
@@ -2,17 +2,16 @@
 # 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/.
 
 DIRS += [
     'aushelper',
     'formautofill',
-    'fxmonitor',
     'onboarding',
     'pdfjs',
     'pocket',
     'screenshots',
     'webcompat',
     'webcompat-reporter'
 ]
 
copy from browser/modules/ContentLinkHandler.jsm
copy to browser/modules/FaviconLoader.jsm
--- a/browser/modules/ContentLinkHandler.jsm
+++ b/browser/modules/FaviconLoader.jsm
@@ -1,23 +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 EXPORTED_SYMBOLS = ["ContentLinkHandler"];
+const EXPORTED_SYMBOLS = ["FaviconLoader"];
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyGlobalGetters(this, ["Blob", "FileReader"]);
 
-ChromeUtils.defineModuleGetter(this, "Feeds",
-  "resource:///modules/Feeds.jsm");
 ChromeUtils.defineModuleGetter(this, "DeferredTask",
   "resource://gre/modules/DeferredTask.jsm");
 ChromeUtils.defineModuleGetter(this, "PromiseUtils",
   "resource://gre/modules/PromiseUtils.jsm");
 
 const BinaryInputStream = Components.Constructor("@mozilla.org/binaryinputstream;1",
                                                  "nsIBinaryInputStream", "setInputStream");
 
@@ -362,69 +360,52 @@ function selectIcons(iconInfos, preferre
   }
 
   return {
     richIcon: largestRichIcon,
     tabIcon
   };
 }
 
-function makeFaviconFromLink(aLink, aIsRichIcon) {
-  let iconUri = getLinkIconURI(aLink);
-  if (!iconUri)
-    return null;
-
-  // Extract the size type and width.
-  let width = extractIconSize(aLink.sizes);
-
-  return {
-    iconUri,
-    width,
-    isRichIcon: aIsRichIcon,
-    type: aLink.type,
-    node: aLink,
-  };
-}
-
 class IconLoader {
-  constructor(chromeGlobal) {
-    this.chromeGlobal = chromeGlobal;
+  constructor(mm) {
+    this.mm = mm;
   }
 
   async load(iconInfo) {
     if (this._loader) {
       this._loader.cancel();
     }
 
     if (LOCAL_FAVICON_SCHEMES.includes(iconInfo.iconUri.scheme)) {
-      this.chromeGlobal.sendAsyncMessage("Link:SetIcon", {
+      this.mm.sendAsyncMessage("Link:SetIcon", {
         originalURL: iconInfo.iconUri.spec,
         canUseForTab: !iconInfo.isRichIcon,
         expiration: undefined,
         iconURL: iconInfo.iconUri.spec,
       });
       return;
     }
 
     try {
       this._loader = new FaviconLoad(iconInfo);
       let { dataURL, expiration } = await this._loader.load();
 
-      this.chromeGlobal.sendAsyncMessage("Link:SetIcon", {
+      this.mm.sendAsyncMessage("Link:SetIcon", {
         originalURL: iconInfo.iconUri.spec,
         canUseForTab: !iconInfo.isRichIcon,
         expiration,
         iconURL: dataURL,
       });
     } catch (e) {
-      if (e.resultCode != Cr.NS_BINDING_ABORTED) {
+      if (e.result != Cr.NS_BINDING_ABORTED) {
         Cu.reportError(e);
 
         // Used mainly for tests currently.
-        this.chromeGlobal.sendAsyncMessage("Link:SetFailedIcon", {
+        this.mm.sendAsyncMessage("Link:SetFailedIcon", {
           originalURL: iconInfo.iconUri.spec,
           canUseForTab: !iconInfo.isRichIcon,
         });
       }
     } finally {
       this._loader = null;
     }
   }
@@ -434,224 +415,85 @@ class IconLoader {
       return;
     }
 
     this._loader.cancel();
     this._loader = null;
   }
 }
 
-class ContentLinkHandler {
-  constructor(chromeGlobal) {
-    this.chromeGlobal = chromeGlobal;
+class FaviconLoader {
+  constructor(mm) {
+    this.mm = mm;
     this.iconInfos = [];
-    this.seenTabIcon = false;
-
-    chromeGlobal.addEventListener("DOMLinkAdded", this);
-    chromeGlobal.addEventListener("DOMLinkChanged", this);
-    chromeGlobal.addEventListener("pageshow", this);
-    chromeGlobal.addEventListener("pagehide", this);
-    chromeGlobal.addEventListener("DOMHeadElementParsed", this);
 
     // For every page we attempt to find a rich icon and a tab icon. These
     // objects take care of the load process for each.
-    this.richIconLoader = new IconLoader(chromeGlobal);
-    this.tabIconLoader = new IconLoader(chromeGlobal);
+    this.richIconLoader = new IconLoader(mm);
+    this.tabIconLoader = new IconLoader(mm);
 
     this.iconTask = new DeferredTask(() => this.loadIcons(), FAVICON_PARSING_TIMEOUT);
   }
 
   loadIcons() {
-    let preferredWidth = PREFERRED_WIDTH * Math.ceil(this.chromeGlobal.content.devicePixelRatio);
+    let preferredWidth = PREFERRED_WIDTH * Math.ceil(this.mm.content.devicePixelRatio);
     let { richIcon, tabIcon } = selectIcons(this.iconInfos, preferredWidth);
     this.iconInfos = [];
 
     if (richIcon) {
       this.richIconLoader.load(richIcon);
     }
 
     if (tabIcon) {
       this.tabIconLoader.load(tabIcon);
     }
   }
 
   addIcon(iconInfo) {
-    if (!Services.prefs.getBoolPref("browser.chrome.site_icons", true)) {
-      return;
-    }
-
-    if (!iconInfo.isRichIcon) {
-      this.seenTabIcon = true;
-    }
     this.iconInfos.push(iconInfo);
     this.iconTask.arm();
   }
 
-  addRootIcon(document) {
-    // If we've already seen a tab icon or if root favicons are disabled then
-    // bail out.
-    if (this.seenTabIcon || !Services.prefs.getBoolPref("browser.chrome.guess_favicon", true)) {
-      return;
-    }
-
+  addDefaultIcon(baseURI) {
     // Currently ImageDocuments will just load the default favicon, see bug
     // 403651 for discussion.
-
-    // Inject the default icon. Use documentURIObject so that we do the right
-    // thing with about:-style error pages. See bug 453442
-    let baseURI = document.documentURIObject;
-    if (baseURI.schemeIs("http") || baseURI.schemeIs("https")) {
-      let iconUri = baseURI.mutate().setPathQueryRef("/favicon.ico").finalize();
-      this.addIcon({
-        iconUri,
-        width: -1,
-        isRichIcon: false,
-        type: TYPE_ICO,
-        node: document,
-      });
-    }
+    this.addIcon({
+      iconUri: baseURI.mutate().setPathQueryRef("/favicon.ico").finalize(),
+      width: -1,
+      isRichIcon: false,
+      type: TYPE_ICO,
+      node: this.mm.content.document,
+    });
   }
 
-  onHeadParsed(event) {
-    let document = this.chromeGlobal.content.document;
-    if (event.target.ownerDocument != document) {
-      return;
-    }
-
-    // Per spec icons are meant to be in the <head> tag so we should have seen
-    // all the icons now so add the root icon if no other tab icons have been
-    // seen.
-    this.addRootIcon(document);
-
+  onPageShow() {
     // We're likely done with icon parsing so load the pending icons now.
     if (this.iconTask.isArmed) {
       this.iconTask.disarm();
       this.loadIcons();
     }
   }
 
-  onPageShow(event) {
-    let document = this.chromeGlobal.content.document;
-    if (event.target != document) {
-      return;
-    }
-
-    // Add the root icon if it hasn't already been added. We encounter this case
-    // for documents that do not have a <head> tag.
-    this.addRootIcon(document);
-
-    // If we've seen any additional icons since the start of the body element
-    // load them now.
-    if (this.iconTask.isArmed) {
-      this.iconTask.disarm();
-      this.loadIcons();
-    }
-  }
-
-  onPageHide(event) {
-    if (event.target != this.chromeGlobal.content.document) {
-      return;
-    }
-
+  onPageHide() {
     this.richIconLoader.cancel();
     this.tabIconLoader.cancel();
 
     this.iconTask.disarm();
     this.iconInfos = [];
-    this.seenTabIcon = false;
   }
 
-  onLinkEvent(event) {
-    let link = event.target;
-    // Ignore sub-frames (bugs 305472, 479408).
-    if (link.ownerGlobal != this.chromeGlobal.content) {
-      return;
-    }
-
-    let rel = link.rel && link.rel.toLowerCase();
-    if (!rel || !link.href)
-      return;
+  static makeFaviconFromLink(aLink, aIsRichIcon) {
+    let iconUri = getLinkIconURI(aLink);
+    if (!iconUri)
+      return null;
 
-    // Note: following booleans only work for the current link, not for the
-    // whole content
-    let feedAdded = false;
-    let iconAdded = false;
-    let searchAdded = false;
-    let rels = {};
-    for (let relString of rel.split(/\s+/))
-      rels[relString] = true;
-
-    for (let relVal in rels) {
-      let isRichIcon = true;
-
-      switch (relVal) {
-        case "feed":
-        case "alternate":
-          if (!feedAdded && event.type == "DOMLinkAdded") {
-            if (!rels.feed && rels.alternate && rels.stylesheet)
-              break;
+    // Extract the size type and width.
+    let width = extractIconSize(aLink.sizes);
 
-            if (Feeds.isValidFeed(link, link.ownerDocument.nodePrincipal, "feed" in rels)) {
-              this.chromeGlobal.sendAsyncMessage("Link:AddFeed", {
-                type: link.type,
-                href: link.href,
-                title: link.title,
-              });
-              feedAdded = true;
-            }
-          }
-          break;
-        case "icon":
-          isRichIcon = false;
-          // Fall through to rich icon handling
-        case "apple-touch-icon":
-        case "apple-touch-icon-precomposed":
-        case "fluid-icon":
-          if (iconAdded || link.hasAttribute("mask")) { // Masked icons are not supported yet.
-            break;
-          }
-
-          let iconInfo = makeFaviconFromLink(link, isRichIcon);
-          if (iconInfo) {
-            iconAdded = this.addIcon(iconInfo);
-          }
-          break;
-        case "search":
-          if (Services.policies && !Services.policies.isAllowed("installSearchEngine")) {
-            break;
-          }
-
-          if (!searchAdded && event.type == "DOMLinkAdded") {
-            let type = link.type && link.type.toLowerCase();
-            type = type.replace(/^\s+|\s*(?:;.*)?$/g, "");
-
-            let re = /^(?:https?|ftp):/i;
-            if (type == "application/opensearchdescription+xml" && link.title &&
-                re.test(link.href)) {
-              let engine = { title: link.title, href: link.href };
-              this.chromeGlobal.sendAsyncMessage("Link:AddSearch", {
-                engine,
-                url: link.ownerDocument.documentURI,
-              });
-              searchAdded = true;
-            }
-          }
-          break;
-      }
-    }
-  }
-
-  handleEvent(event) {
-    switch (event.type) {
-      case "pageshow":
-        this.onPageShow(event);
-        break;
-      case "pagehide":
-        this.onPageHide(event);
-        break;
-      case "DOMHeadElementParsed":
-        this.onHeadParsed(event);
-        break;
-      default:
-        this.onLinkEvent(event);
-    }
+    return {
+      iconUri,
+      width,
+      isRichIcon: aIsRichIcon,
+      type: aLink.type,
+      node: aLink,
+    };
   }
 }
--- a/browser/modules/moz.build
+++ b/browser/modules/moz.build
@@ -119,21 +119,21 @@ XPCSHELL_TESTS_MANIFESTS += ['test/unit/
 EXTRA_JS_MODULES += [
     'AboutNewTab.jsm',
     'AsyncTabSwitcher.jsm',
     'BrowserErrorReporter.jsm',
     'BrowserUsageTelemetry.jsm',
     'BrowserWindowTracker.jsm',
     'ContentClick.jsm',
     'ContentCrashHandlers.jsm',
-    'ContentLinkHandler.jsm',
     'ContentMetaHandler.jsm',
     'ContentObservers.js',
     'ContentSearch.jsm',
     'ExtensionsUI.jsm',
+    'FaviconLoader.jsm',
     'Feeds.jsm',
     'FormSubmitObserver.jsm',
     'FormValidationHandler.jsm',
     'HomePage.jsm',
     'LaterRun.jsm',
     'LightweightThemeChildHelper.jsm',
     'OpenInTabsUtils.jsm',
     'PageActions.jsm',
--- a/build/build-clang/clang-6-linux64.json
+++ b/build/build-clang/clang-6-linux64.json
@@ -13,11 +13,12 @@
     "python_path": "/usr/bin/python2.7",
     "gcc_dir": "/builds/worker/workspace/build/src/gcc",
     "cc": "/builds/worker/workspace/build/src/gcc/bin/gcc",
     "cxx": "/builds/worker/workspace/build/src/gcc/bin/g++",
     "as": "/builds/worker/workspace/build/src/gcc/bin/gcc",
     "patches": [
       "find_symbolizer_linux.patch",
       "r322401.patch",
-      "r325356.patch"
+      "r325356.patch",
+      "r339636.patch"
     ]
 }
--- a/build/build-clang/clang-6-macosx64.json
+++ b/build/build-clang/clang-6-macosx64.json
@@ -19,11 +19,12 @@
     "ar": "/builds/worker/workspace/build/src/cctools/bin/x86_64-apple-darwin11-ar",
     "ranlib": "/builds/worker/workspace/build/src/cctools/bin/x86_64-apple-darwin11-ranlib",
     "libtool": "/builds/worker/workspace/build/src/cctools/bin/x86_64-apple-darwin11-libtool",
     "ld": "/builds/worker/workspace/build/src/clang/bin/clang",
     "patches": [
       "compiler-rt-cross-compile.patch",
       "compiler-rt-no-codesign.patch",
       "r322401.patch",
-      "r325356.patch"
+      "r325356.patch",
+      "r339636.patch"
     ]
 }
--- a/build/build-clang/clang-7-pre-linux64.json
+++ b/build/build-clang/clang-7-pre-linux64.json
@@ -12,11 +12,12 @@
     "libcxxabi_repo": "https://llvm.org/svn/llvm-project/libcxxabi/tags/RELEASE_700/rc1",
     "python_path": "/usr/bin/python2.7",
     "gcc_dir": "/builds/worker/workspace/build/src/gcc",
     "cc": "/builds/worker/workspace/build/src/gcc/bin/gcc",
     "cxx": "/builds/worker/workspace/build/src/gcc/bin/g++",
     "as": "/builds/worker/workspace/build/src/gcc/bin/gcc",
     "patches": [
       "find_symbolizer_linux.patch",
-      "rename_gcov_flush.patch"
+      "rename_gcov_flush.patch",
+      "r339636.patch"
     ]
 }
--- a/build/build-clang/clang-7-pre-mingw.json
+++ b/build/build-clang/clang-7-pre-mingw.json
@@ -9,10 +9,13 @@
     "lld_repo": "https://llvm.org/svn/llvm-project/lld/tags/RELEASE_700/rc1",
     "compiler_repo": "https://llvm.org/svn/llvm-project/compiler-rt/tags/RELEASE_700/rc1",
     "libcxx_repo": "https://llvm.org/svn/llvm-project/libcxx/tags/RELEASE_700/rc1",
     "libcxxabi_repo": "https://llvm.org/svn/llvm-project/libcxxabi/tags/RELEASE_700/rc1",
     "python_path": "/usr/bin/python2.7",
     "gcc_dir": "/builds/worker/workspace/build/src/gcc",
     "cc": "/builds/worker/workspace/build/src/gcc/bin/gcc",
     "cxx": "/builds/worker/workspace/build/src/gcc/bin/g++",
-    "as": "/builds/worker/workspace/build/src/gcc/bin/gcc"
+    "as": "/builds/worker/workspace/build/src/gcc/bin/gcc",
+    "patches": [
+      "r339636.patch"
+    ]
 }
--- a/build/build-clang/clang-win64.json
+++ b/build/build-clang/clang-win64.json
@@ -9,11 +9,12 @@
     "lld_repo": "https://llvm.org/svn/llvm-project/lld/trunk",
     "compiler_repo": "https://llvm.org/svn/llvm-project/compiler-rt/trunk",
     "libcxx_repo": "https://llvm.org/svn/llvm-project/libcxx/trunk",
     "python_path": "c:/mozilla-build/python/python.exe",
     "cc": "cl.exe",
     "cxx": "cl.exe",
     "ml": "ml64.exe",
     "patches": [
-      "loosen-msvc-detection.patch"
+      "loosen-msvc-detection.patch",
+      "r339636.patch"
     ]
 }
new file mode 100644
--- /dev/null
+++ b/build/build-clang/r339636.patch
@@ -0,0 +1,110 @@
+From 3ea7b0a0b1e41cf3871891919a3a1567f3865d42 Mon Sep 17 00:00:00 2001
+From: Reid Kleckner <rnk@google.com>
+Date: Tue, 14 Aug 2018 01:24:35 +0000
+Subject: [PATCH] [BasicAA] Don't assume tail calls with byval don't alias
+ allocas
+
+Summary:
+Calls marked 'tail' cannot read or write allocas from the current frame
+because the current frame might be destroyed by the time they run.
+However, a tail call may use an alloca with byval. Calling with byval
+copies the contents of the alloca into argument registers or stack
+slots, so there is no lifetime issue. Tail calls never modify allocas,
+so we can return just ModRefInfo::Ref.
+
+Fixes PR38466, a longstanding bug.
+
+Reviewers: hfinkel, nlewycky, gbiv, george.burgess.iv
+
+Subscribers: hiraditya, llvm-commits
+
+Differential Revision: https://reviews.llvm.org/D50679
+
+git-svn-id: https://llvm.org/svn/llvm-project/llvm/trunk@339636 91177308-0d34-0410-b5e6-96231b3b80d8
+---
+ lib/Analysis/BasicAliasAnalysis.cpp           | 13 ++++++-----
+ test/Analysis/BasicAA/tail-byval.ll           | 15 ++++++++++++
+ .../DeadStoreElimination/tail-byval.ll        | 23 +++++++++++++++++++
+ 3 files changed, 45 insertions(+), 6 deletions(-)
+ create mode 100644 test/Analysis/BasicAA/tail-byval.ll
+ create mode 100644 test/Transforms/DeadStoreElimination/tail-byval.ll
+
+diff --git a/llvm/lib/Analysis/BasicAliasAnalysis.cpp b/llvm/lib/Analysis/BasicAliasAnalysis.cpp
+index 1a24ae3dba1..f9ecbc04326 100644
+--- a/llvm/lib/Analysis/BasicAliasAnalysis.cpp
++++ b/llvm/lib/Analysis/BasicAliasAnalysis.cpp
+@@ -801,14 +801,15 @@ ModRefInfo BasicAAResult::getModRefInfo(ImmutableCallSite CS,
+ 
+   const Value *Object = GetUnderlyingObject(Loc.Ptr, DL);
+ 
+-  // If this is a tail call and Loc.Ptr points to a stack location, we know that
+-  // the tail call cannot access or modify the local stack.
+-  // We cannot exclude byval arguments here; these belong to the caller of
+-  // the current function not to the current function, and a tail callee
+-  // may reference them.
++  // Calls marked 'tail' cannot read or write allocas from the current frame
++  // because the current frame might be destroyed by the time they run. However,
++  // a tail call may use an alloca with byval. Calling with byval copies the
++  // contents of the alloca into argument registers or stack slots, so there is
++  // no lifetime issue.
+   if (isa<AllocaInst>(Object))
+     if (const CallInst *CI = dyn_cast<CallInst>(CS.getInstruction()))
+-      if (CI->isTailCall())
++      if (CI->isTailCall() &&
++          !CI->getAttributes().hasAttrSomewhere(Attribute::ByVal))
+         return ModRefInfo::NoModRef;
+ 
+   // If the pointer is to a locally allocated object that does not escape,
+diff --git a/llvm/test/Analysis/BasicAA/tail-byval.ll b/llvm/test/Analysis/BasicAA/tail-byval.ll
+new file mode 100644
+index 00000000000..0aa8dfdaedf
+--- /dev/null
++++ b/llvm/test/Analysis/BasicAA/tail-byval.ll
+@@ -0,0 +1,15 @@
++; RUN: opt -basicaa -aa-eval -print-all-alias-modref-info -disable-output < %s 2>&1 | FileCheck %s
++
++declare void @takebyval(i32* byval %p)
++
++define i32 @tailbyval() {
++entry:
++  %p = alloca i32
++  store i32 42, i32* %p
++  tail call void @takebyval(i32* byval %p)
++  %rv = load i32, i32* %p
++  ret i32 %rv
++}
++; FIXME: This should be Just Ref.
++; CHECK-LABEL: Function: tailbyval: 1 pointers, 1 call sites
++; CHECK-NEXT:   Both ModRef:  Ptr: i32* %p       <->  tail call void @takebyval(i32* byval %p)
+diff --git a/llvm/test/Transforms/DeadStoreElimination/tail-byval.ll b/llvm/test/Transforms/DeadStoreElimination/tail-byval.ll
+new file mode 100644
+index 00000000000..ed2fbd434a7
+--- /dev/null
++++ b/llvm/test/Transforms/DeadStoreElimination/tail-byval.ll
+@@ -0,0 +1,23 @@
++; RUN: opt -dse -S < %s | FileCheck %s
++
++; Don't eliminate stores to allocas before tail calls to functions that use
++; byval. It's correct to mark calls like these as 'tail'. To implement this tail
++; call, the backend should copy the bytes from the alloca into the argument area
++; before clearing the stack.
++
++target datalayout = "e-m:e-p:32:32-f64:32:64-f80:32-n8:16:32-S128"
++target triple = "i386-unknown-linux-gnu"
++
++declare void @g(i32* byval %p)
++
++define void @f(i32* byval %x) {
++entry:
++  %p = alloca i32
++  %v = load i32, i32* %x
++  store i32 %v, i32* %p
++  tail call void @g(i32* byval %p)
++  ret void
++}
++; CHECK-LABEL: define void @f(i32* byval %x)
++; CHECK:   store i32 %v, i32* %p
++; CHECK:   tail call void @g(i32* byval %p)
+-- 
+2.18.0
+
deleted file mode 100644
--- a/build/telemetry-schema.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
-  "$schema": "http://json-schema.org/draft-04/schema#",
-  "type": "object",
-  "properties": {
-    "argv": {"type": "array"},
-    "system": {
-      "type": "object",
-      "properties": {
-        "architecture": {"type": "array"},
-        "linux_distribution": {"type": "array"},
-        "mac_ver": {"type": "array"},
-        "machine": {"type": "string"},
-        "python_version": {"type": "string"},
-        "release": {"type": "string"},
-        "system": {"type": "string"},
-        "version": {"type": "string"},
-        "win_ver": {"type": "array"}
-      },
-      "required": ["architecture", "machine", "python_version",
-                   "release", "system", "version"]
-    }
-  },
-  "required": ["argv", "system"]
-}
--- a/build/unix/mozconfig.linux
+++ b/build/unix/mozconfig.linux
@@ -2,17 +2,21 @@ if [ "x$IS_NIGHTLY" = "xyes" ]; then
   # Some nightlies (eg: Mulet) don't want these set.
   MOZ_AUTOMATION_UPDATE_PACKAGING=${MOZ_AUTOMATION_UPDATE_PACKAGING-1}
 fi
 
 . "$topsrcdir/build/mozconfig.common"
 
 TOOLTOOL_DIR=${TOOLTOOL_DIR:-$topsrcdir}
 
-# We deal with valgrind builds here
-CC="$TOOLTOOL_DIR/gcc/bin/gcc"
-CXX="$TOOLTOOL_DIR/gcc/bin/g++"
+if [ -n "$FORCE_GCC" ]; then
+    CC="$TOOLTOOL_DIR/gcc/bin/gcc"
+    CXX="$TOOLTOOL_DIR/gcc/bin/g++"
+else
+    CC="$TOOLTOOL_DIR/clang/bin/clang"
+    CXX="$TOOLTOOL_DIR/clang/bin/clang++"
+fi
 
 # We want to make sure we use binutils and other binaries in the tooltool
 # package.
 mk_add_options "export PATH=$TOOLTOOL_DIR/gcc/bin:$PATH"
 
 . "$topsrcdir/build/unix/mozconfig.stdcxx"
--- a/devtools/client/debugger/new/README.mozilla
+++ b/devtools/client/debugger/new/README.mozilla
@@ -1,13 +1,13 @@
 This is the debugger.html project output.
 See https://github.com/devtools-html/debugger.html
 
-Version 81
+Version 82
 
-Comparison: https://github.com/devtools-html/debugger.html/compare/release-80...release-81
+Comparison: https://github.com/devtools-html/debugger.html/compare/release-81...release-82
 
 Packages:
 - babel-plugin-transform-es2015-modules-commonjs @6.26.2
 - babel-preset-react @6.24.1
 - react @16.4.1
 - react-dom @16.4.1
 - webpack @3.12.0
--- a/devtools/client/debugger/new/dist/debugger.css
+++ b/devtools/client/debugger/new/dist/debugger.css
@@ -1372,16 +1372,17 @@ html .toggle-button.end.vertical svg {
 }
 
 .sources-pane {
   display: flex;
   flex: 1;
   overflow-x: auto;
   overflow-y: auto;
   height: 100%;
+  padding: 4px 0;
 }
 
 .sources-list {
   flex: 1;
   display: flex;
   overflow: hidden;
 }
 
@@ -1672,17 +1673,17 @@ menuseparator {
   padding: 0.5em;
   user-select: none;
   font-size: 12px;
   overflow: hidden;
 }
 
 .outline-list {
   list-style-type: none;
-  padding: 10px 0;
+  padding: 4px 0;
   margin: 0;
   font-family: var(--monospace-font-family);
   overflow: auto;
   position: absolute;
   top: 0;
   right: 0;
   left: 0;
   bottom: 25px;
--- a/devtools/client/debugger/new/dist/parser-worker.js
+++ b/devtools/client/debugger/new/dist/parser-worker.js
@@ -25332,33 +25332,34 @@ function onEnter(node, ancestors, state)
   const grandParentNode = grandParent && grandParent.node;
   const startLocation = node.loc.start;
 
   if (isImport(node) || t.isClassDeclaration(node) || isExport(node) || t.isDebuggerStatement(node) || t.isThrowStatement(node) || t.isBreakStatement(node) || t.isContinueStatement(node)) {
     return addStopPoint(state, startLocation);
   }
 
   if (isControlFlow(node)) {
-    addPoint(state, startLocation, isForStatement(node));
-
+    addStopPoint(state, startLocation);
+
+    // We want to pause at tests so that we can pause at each iteration
+    // e.g `while (i++ < 3) { }`
     const test = node.test || node.discriminant;
     if (test) {
       addStopPoint(state, test.loc.start);
     }
     return;
   }
 
   if (t.isBlockStatement(node) || t.isArrayExpression(node)) {
     return addEmptyPoint(state, startLocation);
   }
 
   if (isReturn(node)) {
     // We do not want to pause at the return if the
     // argument is a call on the same line e.g. return foo()
-
     return addPoint(state, startLocation, !isCall(node.argument) || getStartLine(node) != getStartLine(node.argument));
   }
 
   if (isAssignment(node)) {
     // step at assignments unless the right side is a call or default assignment
     // e.g. `var a = b()`,  `a = b(c = 2)`, `a = [ b() ]`
     const value = node.right || node.init;
     const defaultAssignment = t.isFunction(parentNode) && parent.key === "params";
@@ -25585,16 +25586,17 @@ const getAllGeneratedLocations = dispatc
   queue: true
 });
 const getOriginalLocation = dispatcher.task("getOriginalLocation");
 const getLocationScopes = dispatcher.task("getLocationScopes");
 const getOriginalSourceText = dispatcher.task("getOriginalSourceText");
 const applySourceMap = dispatcher.task("applySourceMap");
 const clearSourceMaps = dispatcher.task("clearSourceMaps");
 const hasMappedSource = dispatcher.task("hasMappedSource");
+const getOriginalStackFrames = dispatcher.task("getOriginalStackFrames");
 
 module.exports = {
   originalToGeneratedId,
   generatedToOriginalId,
   isGeneratedId,
   isOriginalId,
   hasMappedSource,
   getOriginalURLs,
@@ -25602,16 +25604,17 @@ module.exports = {
   getGeneratedRanges,
   getGeneratedLocation,
   getAllGeneratedLocations,
   getOriginalLocation,
   getLocationScopes,
   getOriginalSourceText,
   applySourceMap,
   clearSourceMaps,
+  getOriginalStackFrames,
   startSourceMapWorker: dispatcher.start.bind(dispatcher),
   stopSourceMapWorker: dispatcher.stop.bind(dispatcher)
 };
 
 /***/ }),
 
 /***/ 3651:
 /***/ (function(module, exports, __webpack_require__) {
--- a/devtools/client/debugger/new/panel.js
+++ b/devtools/client/debugger/new/panel.js
@@ -100,17 +100,17 @@ DebuggerPanel.prototype = {
     return this._actions.getMappedExpression(expression);
   },
 
   isPaused() {
     return this._selectors.isPaused(this._getState());
   },
 
   selectSource(url, line) {
-    this._actions.selectSourceURL(url, { location: { line } });
+    this._actions.selectSourceURL(url, { line });
   },
 
   getSource(sourceURL) {
     return this._selectors.getSourceByURL(this._getState(), sourceURL);
   },
 
   destroy: function() {
     this.panelWin.Debugger.destroy();
--- a/devtools/client/debugger/new/src/actions/ast.js
+++ b/devtools/client/debugger/new/src/actions/ast.js
@@ -85,17 +85,17 @@ function setOutOfScopeLocations() {
 
     if (!location) {
       return;
     }
 
     const source = (0, _selectors.getSourceFromId)(getState(), location.sourceId);
     let locations = null;
 
-    if (location.line && source && (0, _selectors.isPaused)(getState())) {
+    if (location.line && source && !source.isWasm && (0, _selectors.isPaused)(getState())) {
       locations = await (0, _parser.findOutOfScopeLocations)(source.id, location);
     }
 
     dispatch({
       type: "OUT_OF_SCOPE_LOCATIONS",
       locations
     });
     dispatch((0, _setInScopeLines.setInScopeLines)());
--- a/devtools/client/debugger/new/src/actions/breakpoints/addBreakpoint.js
+++ b/devtools/client/debugger/new/src/actions/breakpoints/addBreakpoint.js
@@ -2,16 +2,18 @@
 
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
 exports.addHiddenBreakpoint = addHiddenBreakpoint;
 exports.enableBreakpoint = enableBreakpoint;
 exports.addBreakpoint = addBreakpoint;
 
+var _devtoolsSourceMap = require("devtools/client/shared/source-map/index.js");
+
 var _breakpoint = require("../../utils/breakpoint/index");
 
 var _promise = require("../utils/middleware/promise");
 
 var _selectors = require("../../selectors/index");
 
 var _sourceMaps = require("../../utils/source-maps");
 
@@ -44,17 +46,17 @@ async function addBreakpointPromise(getS
       breakpoint: newBreakpoint
     };
   }
 
   const {
     id,
     hitCount,
     actualLocation
-  } = await client.setBreakpoint(generatedLocation, breakpoint.condition, sourceMaps.isOriginalId(location.sourceId));
+  } = await client.setBreakpoint(generatedLocation, breakpoint.condition, (0, _devtoolsSourceMap.isOriginalId)(location.sourceId));
   const newGeneratedLocation = actualLocation || generatedLocation;
   const newLocation = await sourceMaps.getOriginalLocation(newGeneratedLocation);
   const symbols = (0, _selectors.getSymbols)(getState(), source);
   const astLocation = await (0, _breakpoint.getASTLocation)(source, symbols, newLocation);
   const originalText = (0, _source.getTextAtPosition)(source, location);
   const text = (0, _source.getTextAtPosition)(generatedSource, actualLocation);
   const newBreakpoint = {
     id,
--- a/devtools/client/debugger/new/src/actions/breakpoints/index.js
+++ b/devtools/client/debugger/new/src/actions/breakpoints/index.js
@@ -12,16 +12,18 @@ exports.removeAllBreakpoints = removeAll
 exports.removeBreakpoints = removeBreakpoints;
 exports.remapBreakpoints = remapBreakpoints;
 exports.setBreakpointCondition = setBreakpointCondition;
 exports.toggleBreakpoint = toggleBreakpoint;
 exports.toggleBreakpointsAtLine = toggleBreakpointsAtLine;
 exports.addOrToggleDisabledBreakpoint = addOrToggleDisabledBreakpoint;
 exports.toggleDisabledBreakpoint = toggleDisabledBreakpoint;
 
+var _devtoolsSourceMap = require("devtools/client/shared/source-map/index.js");
+
 var _promise = require("../utils/middleware/promise");
 
 var _selectors = require("../../selectors/index");
 
 var _breakpoint = require("../../utils/breakpoint/index");
 
 var _addBreakpoint = require("./addBreakpoint");
 
@@ -258,17 +260,17 @@ function setBreakpointCondition(location
       return;
     }
 
     if (bp.disabled) {
       await dispatch((0, _addBreakpoint.enableBreakpoint)(location));
       bp.disabled = !bp.disabled;
     }
 
-    await client.setBreakpointCondition(bp.id, location, condition, sourceMaps.isOriginalId(bp.location.sourceId));
+    await client.setBreakpointCondition(bp.id, location, condition, (0, _devtoolsSourceMap.isOriginalId)(bp.location.sourceId));
     const newBreakpoint = { ...bp,
       condition
     };
     (0, _breakpoint.assertBreakpoint)(newBreakpoint);
     return dispatch({
       type: "SET_BREAKPOINT_CONDITION",
       breakpoint: newBreakpoint
     });
--- a/devtools/client/debugger/new/src/actions/breakpoints/syncBreakpoint.js
+++ b/devtools/client/debugger/new/src/actions/breakpoints/syncBreakpoint.js
@@ -51,17 +51,17 @@ function createSyncData(id, pendingBreak
   };
 } // we have three forms of syncing: disabled syncing, existing server syncing
 // and adding a new breakpoint
 
 
 async function syncBreakpointPromise(getState, client, sourceMaps, sourceId, pendingBreakpoint) {
   (0, _breakpoint.assertPendingBreakpoint)(pendingBreakpoint);
   const source = (0, _selectors.getSource)(getState(), sourceId);
-  const generatedSourceId = sourceMaps.isOriginalId(sourceId) ? (0, _devtoolsSourceMap.originalToGeneratedId)(sourceId) : sourceId;
+  const generatedSourceId = (0, _devtoolsSourceMap.isOriginalId)(sourceId) ? (0, _devtoolsSourceMap.originalToGeneratedId)(sourceId) : sourceId;
   const generatedSource = (0, _selectors.getSource)(getState(), generatedSourceId);
 
   if (!source) {
     return null;
   }
 
   const {
     location,
@@ -104,17 +104,17 @@ async function syncBreakpointPromise(get
       previousLocation,
       breakpoint: null
     };
   }
 
   const {
     id,
     actualLocation
-  } = await client.setBreakpoint(scopedGeneratedLocation, pendingBreakpoint.condition, sourceMaps.isOriginalId(sourceId)); // the breakpoint might have slid server side, so we want to get the location
+  } = await client.setBreakpoint(scopedGeneratedLocation, pendingBreakpoint.condition, (0, _devtoolsSourceMap.isOriginalId)(sourceId)); // the breakpoint might have slid server side, so we want to get the location
   // based on the server's return value
 
   const newGeneratedLocation = actualLocation;
   const newLocation = await sourceMaps.getOriginalLocation(newGeneratedLocation);
   const originalText = (0, _source.getTextAtPosition)(source, newLocation);
   const text = (0, _source.getTextAtPosition)(generatedSource, newGeneratedLocation);
   return createSyncData(id, pendingBreakpoint, newLocation, newGeneratedLocation, previousLocation, text, originalText);
 }
--- a/devtools/client/debugger/new/src/actions/pause/mapFrames.js
+++ b/devtools/client/debugger/new/src/actions/pause/mapFrames.js
@@ -4,38 +4,54 @@ Object.defineProperty(exports, "__esModu
   value: true
 });
 exports.updateFrameLocation = updateFrameLocation;
 exports.mapDisplayNames = mapDisplayNames;
 exports.mapFrames = mapFrames;
 
 var _selectors = require("../../selectors/index");
 
+var _assert = require("../../utils/assert");
+
+var _assert2 = _interopRequireDefault(_assert);
+
 var _ast = require("../../utils/ast");
 
+var _devtoolsSourceMap = require("devtools/client/shared/source-map/index.js");
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
 /* 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/>. */
 function updateFrameLocation(frame, sourceMaps) {
+  if (frame.isOriginal) {
+    return Promise.resolve(frame);
+  }
+
   return sourceMaps.getOriginalLocation(frame.location).then(loc => ({ ...frame,
     location: loc,
     generatedLocation: frame.generatedLocation || frame.location
   }));
 }
 
 function updateFrameLocations(frames, sourceMaps) {
   if (!frames || frames.length == 0) {
     return Promise.resolve(frames);
   }
 
   return Promise.all(frames.map(frame => updateFrameLocation(frame, sourceMaps)));
 }
 
 function mapDisplayNames(frames, getState) {
   return frames.map(frame => {
+    if (frame.isOriginal) {
+      return frame;
+    }
+
     const source = (0, _selectors.getSourceFromId)(getState(), frame.location.sourceId);
     const symbols = (0, _selectors.getSymbols)(getState(), source);
 
     if (!symbols || !symbols.functions) {
       return frame;
     }
 
     const originalFunction = (0, _ast.findClosestFunction)(symbols, frame.location);
@@ -45,16 +61,70 @@ function mapDisplayNames(frames, getStat
     }
 
     const originalDisplayName = originalFunction.name;
     return { ...frame,
       originalDisplayName
     };
   });
 }
+
+function isWasmOriginalSourceFrame(frame, getState) {
+  if ((0, _devtoolsSourceMap.isGeneratedId)(frame.location.sourceId)) {
+    return false;
+  }
+
+  const generatedSource = (0, _selectors.getSourceFromId)(getState(), frame.generatedLocation.sourceId);
+  return generatedSource.isWasm;
+}
+
+async function expandFrames(frames, sourceMaps, getState) {
+  const result = [];
+
+  for (let i = 0; i < frames.length; ++i) {
+    const frame = frames[i];
+
+    if (frame.isOriginal || !isWasmOriginalSourceFrame(frame, getState)) {
+      result.push(frame);
+      continue;
+    }
+
+    const originalFrames = await sourceMaps.getOriginalStackFrames(frame.generatedLocation);
+
+    if (!originalFrames) {
+      result.push(frame);
+      continue;
+    }
+
+    (0, _assert2.default)(originalFrames.length > 0, "Expected at least one original frame"); // First entry has not specific location -- use one from original frame.
+
+    originalFrames[0] = { ...originalFrames[0],
+      location: frame.location
+    };
+    originalFrames.forEach((originalFrame, j) => {
+      // Keep outer most frame with true actor ID, and generate uniquie
+      // one for the nested frames.
+      const id = j == 0 ? frame.id : `${frame.id}-originalFrame${j}`;
+      result.push({
+        id,
+        displayName: originalFrame.displayName,
+        location: originalFrame.location,
+        scope: frame.scope,
+        this: frame.this,
+        isOriginal: true,
+        // More fields that will be added by the mapDisplayNames and
+        // updateFrameLocation.
+        generatedLocation: frame.generatedLocation,
+        originalDisplayName: originalFrame.displayName
+      });
+    });
+  }
+
+  return result;
+}
 /**
  * Map call stack frame locations and display names to originals.
  * e.g.
  * 1. When the debuggee pauses
  * 2. When a source is pretty printed
  * 3. When symbols are loaded
  * @memberof actions/pause
  * @static
@@ -69,15 +139,16 @@ function mapFrames() {
   }) {
     const frames = (0, _selectors.getFrames)(getState());
 
     if (!frames) {
       return;
     }
 
     let mappedFrames = await updateFrameLocations(frames, sourceMaps);
+    mappedFrames = await expandFrames(mappedFrames, sourceMaps, getState);
     mappedFrames = mapDisplayNames(mappedFrames, getState);
     dispatch({
       type: "MAP_FRAMES",
       frames: mappedFrames
     });
   };
 }
\ No newline at end of file
--- a/devtools/client/debugger/new/src/actions/pause/paused.js
+++ b/devtools/client/debugger/new/src/actions/pause/paused.js
@@ -1,17 +1,15 @@
 "use strict";
 
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
 exports.paused = paused;
 
-var _devtoolsSourceMap = require("devtools/client/shared/source-map/index.js");
-
 var _selectors = require("../../selectors/index");
 
 var _ = require("./index");
 
 var _breakpoints = require("../breakpoints/index");
 
 var _expressions = require("../expressions");
 
@@ -82,19 +80,17 @@ function paused(pauseInfo) {
     if (hiddenBreakpointLocation) {
       dispatch((0, _breakpoints.removeBreakpoint)(hiddenBreakpointLocation));
     }
 
     await dispatch((0, _.mapFrames)());
     const selectedFrame = (0, _selectors.getSelectedFrame)(getState());
 
     if (selectedFrame) {
-      const visibleFrame = (0, _selectors.getVisibleSelectedFrame)(getState());
-      const location = visibleFrame && (0, _devtoolsSourceMap.isGeneratedId)(visibleFrame.location.sourceId) ? selectedFrame.generatedLocation : selectedFrame.location;
-      await dispatch((0, _sources.selectLocation)(location));
+      await dispatch((0, _sources.selectLocation)(selectedFrame.location));
     }
 
     dispatch((0, _ui.togglePaneCollapse)("end", false));
     await dispatch((0, _fetchScopes.fetchScopes)()); // Run after fetching scoping data so that it may make use of the sourcemap
     // expression mappings for local variables.
 
     const atException = why.type == "exception";
 
--- a/devtools/client/debugger/new/src/actions/sources/newSources.js
+++ b/devtools/client/debugger/new/src/actions/sources/newSources.js
@@ -31,17 +31,17 @@ var _selectors = require("../../selector
 /**
  * Redux actions for the sources state
  * @module actions/sources
  */
 function createOriginalSource(originalUrl, generatedSource, sourceMaps) {
   return {
     url: originalUrl,
     relativeUrl: originalUrl,
-    id: sourceMaps.generatedToOriginalId(generatedSource.id, originalUrl),
+    id: (0, _devtoolsSourceMap.generatedToOriginalId)(generatedSource.id, originalUrl),
     isPrettyPrinted: false,
     isWasm: false,
     isBlackBoxed: false,
     loadedState: "unloaded"
   };
 }
 
 function loadSourceMaps(sources) {
@@ -137,17 +137,17 @@ function checkSelectedSource(sourceId) {
 
 function checkPendingBreakpoints(sourceId) {
   return async ({
     dispatch,
     getState
   }) => {
     // source may have been modified by selectLocation
     const source = (0, _selectors.getSourceFromId)(getState(), sourceId);
-    const pendingBreakpoints = (0, _selectors.getPendingBreakpointsForSource)(getState(), source.url);
+    const pendingBreakpoints = (0, _selectors.getPendingBreakpointsForSource)(getState(), source);
 
     if (pendingBreakpoints.length === 0) {
       return;
     } // load the source text if there is a pending breakpoint for it
 
 
     await dispatch((0, _loadSourceText.loadSourceText)(source));
     await Promise.all(pendingBreakpoints.map(bp => dispatch((0, _breakpoints.syncBreakpoint)(sourceId, bp))));
@@ -196,20 +196,20 @@ function newSources(sources) {
     if (sources.length == 0) {
       return;
     }
 
     dispatch({
       type: "ADD_SOURCES",
       sources: sources
     });
+    await dispatch(loadSourceMaps(sources));
 
     for (const source of sources) {
       dispatch(checkSelectedSource(source.id));
       dispatch(checkPendingBreakpoints(source.id));
-    }
+    } // We would like to restore the blackboxed state
+    // after loading all states to make sure the correctness.
 
-    await dispatch(loadSourceMaps(sources)); // We would like to restore the blackboxed state
-    // after loading all states to make sure the correctness.
 
     await dispatch(restoreBlackBoxedSources(sources));
   };
 }
\ No newline at end of file
--- a/devtools/client/debugger/new/src/actions/sources/prettyPrint.js
+++ b/devtools/client/debugger/new/src/actions/sources/prettyPrint.js
@@ -19,19 +19,19 @@ var _ast = require("../ast");
 var _prettyPrint = require("../../workers/pretty-print/index");
 
 var _parser = require("../../workers/parser/index");
 
 var _source = require("../../utils/source");
 
 var _loadSourceText = require("./loadSourceText");
 
-var _sources = require("../sources/index");
+var _pause = require("../pause/index");
 
-var _pause = require("../pause/index");
+var _sources = require("../sources/index");
 
 var _selectors = require("../../selectors/index");
 
 function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
 
 /* 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/>. */
@@ -120,24 +120,24 @@ function togglePrettyPrint(sourceId) {
     const options = {};
 
     if (selectedLocation) {
       options.location = await sourceMaps.getOriginalLocation(selectedLocation);
     }
 
     if (prettySource) {
       const _sourceId = prettySource.id;
-      return dispatch((0, _sources.selectLocation)({ ...options.location,
+      return dispatch((0, _sources.selectSpecificLocation)({ ...options.location,
         sourceId: _sourceId
       }));
     }
 
     const newPrettySource = await dispatch(createPrettySource(sourceId));
     await dispatch((0, _breakpoints.remapBreakpoints)(sourceId));
     await dispatch((0, _pause.mapFrames)());
     await dispatch((0, _ast.setPausePoints)(newPrettySource.id));
     await dispatch((0, _ast.setSymbols)(newPrettySource.id));
-    dispatch((0, _sources.selectLocation)({ ...options.location,
+    dispatch((0, _sources.selectSpecificLocation)({ ...options.location,
       sourceId: newPrettySource.id
     }));
     return newPrettySource;
   };
 }
\ No newline at end of file
--- a/devtools/client/debugger/new/src/actions/sources/select.js
+++ b/devtools/client/debugger/new/src/actions/sources/select.js
@@ -9,16 +9,18 @@ exports.selectSource = selectSource;
 exports.selectLocation = selectLocation;
 exports.selectSpecificLocation = selectSpecificLocation;
 exports.selectSpecificSource = selectSpecificSource;
 exports.jumpToMappedLocation = jumpToMappedLocation;
 exports.jumpToMappedSelectedLocation = jumpToMappedSelectedLocation;
 
 var _devtoolsSourceMap = require("devtools/client/shared/source-map/index.js");
 
+var _sources = require("../../reducers/sources");
+
 var _ast = require("../ast");
 
 var _ui = require("../ui");
 
 var _prettyPrint = require("./prettyPrint");
 
 var _tabs = require("../tabs");
 
@@ -55,40 +57,45 @@ const setPendingSelectedLocation = expor
 });
 
 const clearSelectedLocation = exports.clearSelectedLocation = () => ({
   type: "CLEAR_SELECTED_LOCATION"
 });
 /**
  * Deterministically select a source that has a given URL. This will
  * work regardless of the connection status or if the source exists
- * yet. This exists mostly for external things to interact with the
+ * yet.
+ *
+ * This exists mostly for external things to interact with the
  * debugger.
  *
  * @memberof actions/sources
  * @static
  */
 
 
-function selectSourceURL(url, options = {}) {
+function selectSourceURL(url, options = {
+  line: 1
+}) {
   return async ({
     dispatch,
-    getState
+    getState,
+    sourceMaps
   }) => {
     const source = (0, _selectors.getSourceByURL)(getState(), url);
 
-    if (source) {
-      const sourceId = source.id;
-      const location = (0, _location.createLocation)({ ...options.location,
-        sourceId
-      });
-      await dispatch(selectLocation(location));
-    } else {
-      dispatch(setPendingSelectedLocation(url, options));
+    if (!source) {
+      return dispatch(setPendingSelectedLocation(url, options));
     }
+
+    const sourceId = source.id;
+    const location = (0, _location.createLocation)({ ...options,
+      sourceId
+    });
+    return dispatch(selectLocation(location));
   };
 }
 /**
  * @memberof actions/sources
  * @static
  */
 
 
@@ -104,55 +111,65 @@ function selectSource(sourceId) {
 }
 /**
  * @memberof actions/sources
  * @static
  */
 
 
 function selectLocation(location, {
-  checkPrettyPrint = true
+  keepContext = true
 } = {}) {
   return async ({
     dispatch,
     getState,
+    sourceMaps,
     client
   }) => {
     const currentSource = (0, _selectors.getSelectedSource)(getState());
 
     if (!client) {
       // No connection, do nothing. This happens when the debugger is
       // shut down too fast and it tries to display a default source.
       return;
     }
 
-    const source = (0, _selectors.getSource)(getState(), location.sourceId);
+    let source = (0, _selectors.getSource)(getState(), location.sourceId);
 
     if (!source) {
       // If there is no source we deselect the current selected source
       return dispatch(clearSelectedLocation());
     }
 
     const activeSearch = (0, _selectors.getActiveSearch)(getState());
 
     if (activeSearch !== "file") {
       dispatch((0, _ui.closeActiveSearch)());
+    } // Preserve the current source map context (original / generated)
+    // when navigting to a new location.
+
+
+    const selectedSource = (0, _selectors.getSelectedSource)(getState());
+
+    if (keepContext && selectedSource && (0, _devtoolsSourceMap.isOriginalId)(selectedSource.id) != (0, _devtoolsSourceMap.isOriginalId)(location.sourceId)) {
+      location = await (0, _sourceMaps.getMappedLocation)(getState(), sourceMaps, location);
+      source = (0, _sources.getSourceFromId)(getState(), location.sourceId);
     }
 
     dispatch((0, _tabs.addTab)(source.url, 0));
     dispatch(setSelectedLocation(source, location));
     await dispatch((0, _loadSourceText.loadSourceText)(source));
     const loadedSource = (0, _selectors.getSource)(getState(), source.id);
 
     if (!loadedSource) {
       // If there was a navigation while we were loading the loadedSource
       return;
     }
 
-    if (checkPrettyPrint && _prefs.prefs.autoPrettyPrint && !(0, _selectors.getPrettySource)(getState(), loadedSource.id) && (0, _source.shouldPrettyPrint)(loadedSource) && (0, _source.isMinified)(loadedSource)) {
+    if (keepContext && _prefs.prefs.autoPrettyPrint && !(0, _selectors.getPrettySource)(getState(), loadedSource.id) && (0, _source.shouldPrettyPrint)(loadedSource) && (0, _source.isMinified)(loadedSource)) {
       await dispatch((0, _prettyPrint.togglePrettyPrint)(loadedSource.id));
       dispatch((0, _tabs.closeTab)(loadedSource.url));
     }
 
     dispatch((0, _ast.setSymbols)(loadedSource.id));
     dispatch((0, _ast.setOutOfScopeLocations)()); // If a new source is selected update the file search results
 
     const newSource = (0, _selectors.getSelectedSource)(getState());
@@ -165,17 +182,17 @@ function selectLocation(location, {
 /**
  * @memberof actions/sources
  * @static
  */
 
 
 function selectSpecificLocation(location) {
   return selectLocation(location, {
-    checkPrettyPrint: false
+    keepContext: false
   });
 }
 /**
  * @memberof actions/sources
  * @static
  */
 
 
@@ -201,26 +218,18 @@ function jumpToMappedLocation(location) 
     getState,
     client,
     sourceMaps
   }) {
     if (!client) {
       return;
     }
 
-    const source = (0, _selectors.getSource)(getState(), location.sourceId);
-    let pairedLocation;
-
-    if ((0, _devtoolsSourceMap.isOriginalId)(location.sourceId)) {
-      pairedLocation = await (0, _sourceMaps.getGeneratedLocation)(getState(), source, location, sourceMaps);
-    } else {
-      pairedLocation = await sourceMaps.getOriginalLocation(location, source);
-    }
-
-    return dispatch(selectLocation({ ...pairedLocation
+    const pairedLocation = await (0, _sourceMaps.getMappedLocation)(getState(), sourceMaps, location);
+    return dispatch(selectSpecificLocation({ ...pairedLocation
     }));
   };
 }
 
 function jumpToMappedSelectedLocation() {
   return async function ({
     dispatch,
     getState
--- a/devtools/client/debugger/new/src/components/Editor/Preview/index.js
+++ b/devtools/client/debugger/new/src/components/Editor/Preview/index.js
@@ -31,17 +31,17 @@ function inPopup(e) {
   const {
     relatedTarget
   } = e;
 
   if (!relatedTarget) {
     return true;
   }
 
-  const pop = relatedTarget.closest(".popover") || relatedTarget.classList.contains("debug-expression");
+  const pop = relatedTarget.closest(".tooltip") || relatedTarget.closest(".popover") || relatedTarget.classList.contains("debug-expression");
   return pop;
 }
 
 function getElementFromPos(pos) {
   // $FlowIgnore
   return document.elementFromPoint(pos.x + pos.width / 2, pos.y + pos.height / 2);
 }
 
--- a/devtools/client/debugger/new/src/components/Editor/Tab.js
+++ b/devtools/client/debugger/new/src/components/Editor/Tab.js
@@ -143,34 +143,31 @@ class Tab extends _react.PureComponent {
 
     function onClickClose(e) {
       e.stopPropagation();
       closeTab(source.url);
     }
 
     function handleTabClick(e) {
       e.preventDefault();
-      e.stopPropagation(); // Accommodate middle click to close tab
-
-      if (e.button === 1) {
-        return closeTab(source.url);
-      }
-
+      e.stopPropagation();
       return selectSpecificSource(sourceId);
     }
 
     const className = (0, _classnames2.default)("source-tab", {
       active,
       pretty: isPrettyCode
     });
     const path = (0, _source.getDisplayPath)(source, tabSources);
     return _react2.default.createElement("div", {
       className: className,
       key: sourceId,
-      onClick: handleTabClick,
+      onClick: handleTabClick // Accommodate middle click to close tab
+      ,
+      onMouseUp: e => e.button === 1 && closeTab(source.url),
       onContextMenu: e => this.onTabContextMenu(e, sourceId),
       title: (0, _source.getFileURL)(source)
     }, _react2.default.createElement(_SourceIcon2.default, {
       source: source,
       shouldHide: icon => ["file", "javascript"].includes(icon)
     }), _react2.default.createElement("div", {
       className: "filename"
     }, (0, _source.getTruncatedFileName)(source), path && _react2.default.createElement("span", null, `../${path}/..`)), _react2.default.createElement(_Button.CloseButton, {
--- a/devtools/client/debugger/new/src/components/PrimaryPanes/SourcesTree.js
+++ b/devtools/client/debugger/new/src/components/PrimaryPanes/SourcesTree.js
@@ -307,24 +307,22 @@ var _initialiseProps = function () {
       debuggeeUrl,
       projectRoot
     } = this.props;
     return _react2.default.createElement(_SourcesTreeItem2.default, {
       item: item,
       depth: depth,
       focused: focused,
       expanded: expanded,
-      setExpanded: setExpanded,
       focusItem: this.focusItem,
       selectItem: this.selectItem,
       source: this.getSource(item),
       debuggeeUrl: debuggeeUrl,
       projectRoot: projectRoot,
-      clearProjectDirectoryRoot: this.props.clearProjectDirectoryRoot,
-      setProjectDirectoryRoot: this.props.setProjectDirectoryRoot
+      setExpanded: setExpanded
     });
   };
 };
 
 function getSourceForTree(state, source) {
   if (!source || !source.isPrettyPrinted) {
     return source;
   }
@@ -344,11 +342,10 @@ const mapStateToProps = state => {
     sources: (0, _selectors.getRelativeSources)(state),
     sourceCount: (0, _selectors.getSourceCount)(state)
   };
 };
 
 exports.default = (0, _reactRedux.connect)(mapStateToProps, {
   selectSource: _actions2.default.selectSource,
   setExpandedState: _actions2.default.setExpandedState,
-  setProjectDirectoryRoot: _actions2.default.setProjectDirectoryRoot,
   clearProjectDirectoryRoot: _actions2.default.clearProjectDirectoryRoot
 })(SourcesTree);
\ No newline at end of file
--- a/devtools/client/debugger/new/src/components/PrimaryPanes/SourcesTreeItem.js
+++ b/devtools/client/debugger/new/src/components/PrimaryPanes/SourcesTreeItem.js
@@ -1,32 +1,42 @@
 "use strict";
 
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
 
+var _devtoolsSourceMap = require("devtools/client/shared/source-map/index.js");
+
 var _react = require("devtools/client/shared/vendor/react");
 
 var _react2 = _interopRequireDefault(_react);
 
+var _reactRedux = require("devtools/client/shared/vendor/react-redux");
+
 var _classnames = require("devtools/client/debugger/new/dist/vendors").vendored["classnames"];
 
 var _classnames2 = _interopRequireDefault(_classnames);
 
 var _devtoolsContextmenu = require("devtools/client/debugger/new/dist/vendors").vendored["devtools-contextmenu"];
 
 var _SourceIcon = require("../shared/SourceIcon");
 
 var _SourceIcon2 = _interopRequireDefault(_SourceIcon);
 
 var _Svg = require("devtools/client/debugger/new/dist/vendors").vendored["Svg"];
 
 var _Svg2 = _interopRequireDefault(_Svg);
 
+var _selectors = require("../../selectors/index");
+
+var _actions = require("../../actions/index");
+
+var _actions2 = _interopRequireDefault(_actions);
+
 var _sourcesTree = require("../../utils/sources-tree/index");
 
 var _clipboard = require("../../utils/clipboard");
 
 var _prefs = require("../../utils/prefs");
 
 function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
 
@@ -163,42 +173,71 @@ class SourceTreeItem extends _react.Comp
       className: (0, _classnames2.default)("arrow", {
         expanded
       })
     }) : _react2.default.createElement("i", {
       className: "no-arrow"
     });
   }
 
-  renderItemName(name) {
-    switch (name) {
+  renderItemName() {
+    const {
+      item
+    } = this.props;
+
+    switch (item.name) {
       case "ng://":
         return "Angular";
 
       case "webpack://":
         return "Webpack";
 
       default:
-        return name;
+        return `${item.name}`;
     }
   }
 
   render() {
     const {
       item,
       depth,
-      focused
+      focused,
+      hasMatchingGeneratedSource
     } = this.props;
+    const suffix = hasMatchingGeneratedSource ? _react2.default.createElement("span", {
+      className: "suffix"
+    }, "[sm]") : null;
     return _react2.default.createElement("div", {
       className: (0, _classnames2.default)("node", {
         focused
       }),
       key: item.path,
       onClick: this.onClick,
       onContextMenu: e => this.onContextMenu(e, item)
     }, this.renderItemArrow(), this.getIcon(item, depth), _react2.default.createElement("span", {
       className: "label"
-    }, " ", this.renderItemName(item.name), " "));
+    }, " ", this.renderItemName(), " ", suffix));
   }
 
 }
 
-exports.default = SourceTreeItem;
\ No newline at end of file
+function getHasMatchingGeneratedSource(state, source) {
+  if (!source) {
+    return false;
+  }
+
+  const sources = (0, _selectors.getSourcesByURL)(state, source.url);
+  return (0, _devtoolsSourceMap.isOriginalId)(source.id) && sources.length > 1;
+}
+
+const mapStateToProps = (state, props) => {
+  const {
+    source
+  } = props;
+  return {
+    hasMatchingGeneratedSource: getHasMatchingGeneratedSource(state, source)
+  };
+};
+
+exports.default = (0, _reactRedux.connect)(mapStateToProps, {
+  setProjectDirectoryRoot: _actions2.default.setProjectDirectoryRoot,
+  clearProjectDirectoryRoot: _actions2.default.clearProjectDirectoryRoot
+})(SourceTreeItem);
\ No newline at end of file
--- a/devtools/client/debugger/new/src/components/PrimaryPanes/index.js
+++ b/devtools/client/debugger/new/src/components/PrimaryPanes/index.js
@@ -115,12 +115,11 @@ class PrimaryPanes extends _react.Compon
 const mapStateToProps = state => ({
   selectedTab: (0, _selectors.getSelectedPrimaryPaneTab)(state),
   sources: (0, _selectors.getSources)(state),
   sourceSearchOn: (0, _selectors.getActiveSearch)(state) === "source"
 });
 
 exports.default = (0, _reactRedux.connect)(mapStateToProps, {
   setPrimaryPaneTab: _actions2.default.setPrimaryPaneTab,
-  selectLocation: _actions2.default.selectLocation,
   setActiveSearch: _actions2.default.setActiveSearch,
   closeActiveSearch: _actions2.default.closeActiveSearch
 })(PrimaryPanes);
\ No newline at end of file
--- a/devtools/client/debugger/new/src/components/ProjectSearch.js
+++ b/devtools/client/debugger/new/src/components/ProjectSearch.js
@@ -78,17 +78,17 @@ class ProjectSearch extends _react.Compo
       }
 
       return setActiveSearch("project");
     };
 
     this.isProjectSearchEnabled = () => this.props.activeSearch === "project";
 
     this.selectMatchItem = matchItem => {
-      this.props.selectLocation({ ...matchItem
+      this.props.selectSpecificLocation({ ...matchItem
       });
       this.props.doSearchForHighlight(this.state.inputValue, (0, _editor.getEditor)(), matchItem.line, matchItem.column);
     };
 
     this.getResults = () => {
       const {
         results
       } = this.props;
@@ -330,12 +330,12 @@ const mapStateToProps = state => ({
   query: (0, _selectors.getTextSearchQuery)(state),
   status: (0, _selectors.getTextSearchStatus)(state)
 });
 
 exports.default = (0, _reactRedux.connect)(mapStateToProps, {
   closeProjectSearch: _actions2.default.closeProjectSearch,
   searchSources: _actions2.default.searchSources,
   clearSearch: _actions2.default.clearSearch,
-  selectLocation: _actions2.default.selectLocation,
+  selectSpecificLocation: _actions2.default.selectSpecificLocation,
   setActiveSearch: _actions2.default.setActiveSearch,
   doSearchForHighlight: _actions2.default.doSearchForHighlight
 })(ProjectSearch);
\ No newline at end of file
--- a/devtools/client/debugger/new/src/components/QuickOpenModal.js
+++ b/devtools/client/debugger/new/src/components/QuickOpenModal.js
@@ -180,28 +180,28 @@ class QuickOpenModal extends _react.Comp
       this.gotoLocation({
         sourceId: item.id,
         line: 0
       });
     };
 
     this.onSelectResultItem = item => {
       const {
-        selectLocation,
+        selectSpecificLocation,
         selectedSource,
         highlightLineRange
       } = this.props;
 
       if (!this.isSymbolSearch() || selectedSource == null) {
         return;
       }
 
       if (this.isVariableQuery()) {
         const line = item.location && item.location.start ? item.location.start.line : 0;
-        return selectLocation({
+        return selectSpecificLocation({
           sourceId: selectedSource.id,
           line
         });
       }
 
       if (this.isFunctionQuery()) {
         return highlightLineRange({ ...(item.location != null ? {
             start: item.location.start.line,
@@ -227,24 +227,24 @@ class QuickOpenModal extends _react.Comp
 
       if (results != null) {
         this.onSelectResultItem(results[nextIndex]);
       }
     };
 
     this.gotoLocation = location => {
       const {
-        selectLocation,
+        selectSpecificLocation,
         selectedSource
       } = this.props;
       const selectedSourceId = selectedSource ? selectedSource.id : "";
 
       if (location != null) {
         const sourceId = location.sourceId ? location.sourceId : selectedSourceId;
-        selectLocation({
+        selectSpecificLocation({
           sourceId,
           line: location.line,
           column: location.column
         });
         this.closeModal();
       }
     };
 
@@ -477,14 +477,14 @@ function mapStateToProps(state) {
     tabs: (0, _selectors.getTabs)(state)
   };
 }
 /* istanbul ignore next: ignoring testing of redux connection stuff */
 
 
 exports.default = (0, _reactRedux.connect)(mapStateToProps, {
   shortcutsModalEnabled: _actions2.default.shortcutsModalEnabled,
-  selectLocation: _actions2.default.selectLocation,
+  selectSpecificLocation: _actions2.default.selectSpecificLocation,
   setQuickOpenQuery: _actions2.default.setQuickOpenQuery,
   highlightLineRange: _actions2.default.highlightLineRange,
   closeQuickOpen: _actions2.default.closeQuickOpen,
   toggleShortcutsModal: _actions2.default.toggleShortcutsModal
 })(QuickOpenModal);
\ No newline at end of file
--- a/devtools/client/debugger/new/src/reducers/pause.js
+++ b/devtools/client/debugger/new/src/reducers/pause.js
@@ -350,31 +350,40 @@ function getCanRewind(state) {
 function getExtra(state) {
   return state.pause.extra;
 }
 
 function getFrames(state) {
   return state.pause.frames;
 }
 
+function getGeneratedFrameId(frameId) {
+  if (frameId.includes("-originalFrame")) {
+    // The mapFrames can add original stack frames -- get generated frameId.
+    return frameId.substr(0, frameId.lastIndexOf("-originalFrame"));
+  }
+
+  return frameId;
+}
+
 function getGeneratedFrameScope(state, frameId) {
   if (!frameId) {
     return null;
   }
 
-  return getFrameScopes(state).generated[frameId];
+  return getFrameScopes(state).generated[getGeneratedFrameId(frameId)];
 }
 
 function getOriginalFrameScope(state, sourceId, frameId) {
   if (!frameId || !sourceId) {
     return null;
   }
 
   const isGenerated = (0, _devtoolsSourceMap.isGeneratedId)(sourceId);
-  const original = getFrameScopes(state).original[frameId];
+  const original = getFrameScopes(state).original[getGeneratedFrameId(frameId)];
 
   if (!isGenerated && original && (original.pending || original.scope)) {
     return original;
   }
 
   return null;
 }
 
--- a/devtools/client/debugger/new/src/reducers/pending-breakpoints.js
+++ b/devtools/client/debugger/new/src/reducers/pending-breakpoints.js
@@ -2,16 +2,20 @@
 
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
 exports.getPendingBreakpoints = getPendingBreakpoints;
 exports.getPendingBreakpointList = getPendingBreakpointList;
 exports.getPendingBreakpointsForSource = getPendingBreakpointsForSource;
 
+var _devtoolsSourceMap = require("devtools/client/shared/source-map/index.js");
+
+var _sources = require("./sources");
+
 var _breakpoint = require("../utils/breakpoint/index");
 
 /* 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/>. */
 
 /**
  * Pending breakpoints reducer
@@ -163,13 +167,20 @@ function deleteBreakpoint(state, locatio
 function getPendingBreakpoints(state) {
   return state.pendingBreakpoints;
 }
 
 function getPendingBreakpointList(state) {
   return Object.values(getPendingBreakpoints(state));
 }
 
-function getPendingBreakpointsForSource(state, sourceUrl) {
-  return getPendingBreakpointList(state).filter(pendingBreakpoint => pendingBreakpoint.location.sourceUrl === sourceUrl);
+function getPendingBreakpointsForSource(state, source) {
+  const sources = (0, _sources.getSourcesByURL)(state, source.url);
+
+  if (sources.length > 1 && (0, _devtoolsSourceMap.isGeneratedId)(source.id)) {
+    // Don't return pending breakpoints for duplicated generated sources
+    return [];
+  }
+
+  return getPendingBreakpointList(state).filter(pendingBreakpoint => pendingBreakpoint.location.sourceUrl === source.url);
 }
 
 exports.default = update;
\ No newline at end of file
--- a/devtools/client/debugger/new/src/reducers/sources.js
+++ b/devtools/client/debugger/new/src/reducers/sources.js
@@ -5,16 +5,17 @@ Object.defineProperty(exports, "__esModu
 });
 exports.getSelectedSource = exports.getSelectedLocation = exports.getSourceCount = undefined;
 exports.initialSourcesState = initialSourcesState;
 exports.createSource = createSource;
 exports.getBlackBoxList = getBlackBoxList;
 exports.getSource = getSource;
 exports.getSourceFromId = getSourceFromId;
 exports.getSourceByURL = getSourceByURL;
+exports.getSourcesByURL = getSourcesByURL;
 exports.getGeneratedSource = getGeneratedSource;
 exports.getPendingSelectedLocation = getPendingSelectedLocation;
 exports.getPrettySource = getPrettySource;
 exports.hasPrettySource = hasPrettySource;
 exports.getSourceByUrlInSources = getSourceByUrlInSources;
 exports.getSourceInSources = getSourceInSources;
 exports.getSources = getSources;
 exports.getUrls = getUrls;
@@ -244,16 +245,20 @@ function getSource(state, id) {
 function getSourceFromId(state, id) {
   return getSourcesState(state).sources[id];
 }
 
 function getSourceByURL(state, url) {
   return getSourceByUrlInSources(getSources(state), getUrls(state), url);
 }
 
+function getSourcesByURL(state, url) {
+  return getSourcesByUrlInSources(getSources(state), getUrls(state), url);
+}
+
 function getGeneratedSource(state, source) {
   if (!(0, _devtoolsSourceMap.isOriginalId)(source.id)) {
     return source;
   }
 
   return getSourceFromId(state, (0, _devtoolsSourceMap.originalToGeneratedId)(source.id));
 }
 
--- a/devtools/client/debugger/new/src/utils/pause/frames/getLibraryFromUrl.js
+++ b/devtools/client/debugger/new/src/utils/pause/frames/getLibraryFromUrl.js
@@ -52,17 +52,17 @@ const libraryMap = [{
 }, {
   label: "Ember",
   pattern: /ember/i
 }, {
   label: "Choo",
   pattern: /choo/i
 }, {
   label: "VueJS",
-  pattern: /vue\.js/i
+  pattern: /vue(?:\.[a-z]+)*\.js/i
 }, {
   label: "RxJS",
   pattern: /rxjs/i
 }, {
   label: "Angular",
   pattern: /angular/i
 }, {
   label: "Redux",
--- a/devtools/client/debugger/new/src/utils/source-maps.js
+++ b/devtools/client/debugger/new/src/utils/source-maps.js
@@ -1,22 +1,25 @@
 "use strict";
 
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
 exports.getGeneratedLocation = getGeneratedLocation;
+exports.getMappedLocation = getMappedLocation;
+
+var _devtoolsSourceMap = require("devtools/client/shared/source-map/index.js");
 
 var _selectors = require("../selectors/index");
 
 /* 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/>. */
 async function getGeneratedLocation(state, source, location, sourceMaps) {
-  if (!sourceMaps.isOriginalId(location.sourceId)) {
+  if (!(0, _devtoolsSourceMap.isOriginalId)(location.sourceId)) {
     return location;
   }
 
   const {
     line,
     sourceId,
     column
   } = await sourceMaps.getGeneratedLocation(location, source);
@@ -27,9 +30,19 @@ async function getGeneratedLocation(stat
   }
 
   return {
     line,
     sourceId,
     column: column === 0 ? undefined : column,
     sourceUrl: generatedSource.url
   };
+}
+
+async function getMappedLocation(state, sourceMaps, location) {
+  const source = (0, _selectors.getSource)(state, location.sourceId);
+
+  if ((0, _devtoolsSourceMap.isOriginalId)(location.sourceId)) {
+    return getGeneratedLocation(state, source, location, sourceMaps);
+  }
+
+  return sourceMaps.getOriginalLocation(location, source);
 }
\ No newline at end of file
--- a/devtools/client/debugger/new/src/utils/source.js
+++ b/devtools/client/debugger/new/src/utils/source.js
@@ -46,17 +46,18 @@ var _url = require("../utils/url");
 var _sourcesTree = require("./sources-tree/index");
 
 var _prefs = require("./prefs");
 
 const sourceTypes = exports.sourceTypes = {
   coffee: "coffeescript",
   js: "javascript",
   jsx: "react",
-  ts: "typescript"
+  ts: "typescript",
+  vue: "vue"
 };
 /**
  * Trims the query part or reference identifier of a url string, if necessary.
  *
  * @memberof utils/source
  * @static
  */
 
--- a/devtools/client/debugger/new/src/utils/sources-tree/getURL.js
+++ b/devtools/client/debugger/new/src/utils/sources-tree/getURL.js
@@ -43,38 +43,37 @@ function _getURL(source, defaultDomain) 
 
   if (!url) {
     return def;
   }
 
   const {
     pathname,
     protocol,
-    host,
-    path
+    host
   } = (0, _url.parse)(url);
   const filename = (0, _devtoolsModules.getUnicodeUrlPath)(getFilenameFromPath(pathname));
 
   switch (protocol) {
     case "javascript:":
       // Ignore `javascript:` URLs for now
       return def;
 
     case "moz-extension:":
     case "resource:":
       return { ...def,
-        path,
+        path: pathname,
         filename,
         group: `${protocol}//${host || ""}`
       };
 
     case "webpack:":
     case "ng:":
       return { ...def,
-        path: path,
+        path: pathname,
         filename,
         group: `${protocol}//`
       };
 
     case "about:":
       // An about page is a special case
       return { ...def,
         path: "/",
@@ -88,17 +87,17 @@ function _getURL(source, defaultDomain) 
         group: NoDomain,
         filename: url
       };
 
     case "":
       if (pathname && pathname.startsWith("/")) {
         // use file protocol for a URL like "/foo/bar.js"
         return { ...def,
-          path: path,
+          path: pathname,
           filename,
           group: "file://"
         };
       } else if (!host) {
         return { ...def,
           path: url,
           group: defaultDomain || "",
           filename
@@ -112,17 +111,17 @@ function _getURL(source, defaultDomain) 
       return { ...def,
         path: pathname,
         filename,
         group: (0, _devtoolsModules.getUnicodeHostname)(host)
       };
   }
 
   return { ...def,
-    path: path,
+    path: pathname,
     group: protocol ? `${protocol}//` : "",
     filename
   };
 }
 
 function getURL(source, debuggeeUrl) {
   if (urlMap.has(source)) {
     return urlMap.get(source) || def;
--- a/devtools/client/debugger/new/test/mochitest/browser_dbg-pause-points.js
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-pause-points.js
@@ -48,12 +48,11 @@ add_task(async function test() {
     name: "sequences",
     count: 4,
     steps: [[23,2], [25,8], [31,4], [34,2], [37,0]]
   });
 
   await testCase(dbg, {
     name: "flow",
     count: 8,
-    steps:
-      [[16,2], [17,12], [18,6], [19,8], [19,17], [19,8], [19,17], [19,8], [20,0]]
+    steps: [[16,2], [17,12], [18,6], [19,2], [19,8], [19,17], [19,8], [19,17], [19,8]]
   });
 });
--- a/devtools/client/debugger/new/test/mochitest/browser_dbg-preview-source-maps.js
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-preview-source-maps.js
@@ -55,12 +55,12 @@ add_task(async function() {
   info(`Test previewing in the generated location`);
   await dbg.actions.jumpToMappedSelectedLocation();
   await waitForSelectedSource(dbg, "bundle.js");
   await assertPreviews(dbg, [
     { line: 70, column: 11, result: 4, expression: "x" }
   ]);
 
   info(`Test that you can not preview in another original file`);
-  await selectSource(dbg, "output");
+  await selectSpecificSource(dbg, "output");
   await hoverAtPos(dbg, { line: 2, ch: 16 });
   await assertNoTooltip(dbg);
 });
--- a/devtools/client/debugger/new/test/mochitest/browser_dbg-sourcemaps.js
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-sourcemaps.js
@@ -44,29 +44,29 @@ add_task(async function() {
     selectors: { getBreakpoint, getBreakpoints },
     getState
   } = dbg;
 
   await waitForSources(dbg, "entry.js", "output.js", "times2.js", "opts.js");
   ok(true, "Original sources exist");
   const bundleSrc = findSource(dbg, "bundle.js");
 
-  await selectSource(dbg, bundleSrc);
+  await selectSpecificSource(dbg, bundleSrc);
 
   await clickGutter(dbg, 13);
   await waitForDispatch(dbg, "ADD_BREAKPOINT");
   assertEditorBreakpoint(dbg, 13, true);
 
   await clickGutter(dbg, 13);
   await waitForDispatch(dbg, "REMOVE_BREAKPOINT");
   is(getBreakpoints(getState()).size, 0, "No breakpoints exists");
 
   const entrySrc = findSource(dbg, "entry.js");
 
-  await selectSource(dbg, entrySrc);
+  await selectSpecificSource(dbg, entrySrc);
   ok(
     getCM(dbg)
       .getValue()
       .includes("window.keepMeAlive"),
     "Original source text loaded correctly"
   );
 
   // Test breaking on a breakpoint
--- a/devtools/client/debugger/new/test/mochitest/browser_dbg-wasm-sourcemaps.js
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-wasm-sourcemaps.js
@@ -19,17 +19,17 @@ add_task(async function() {
   await waitForLoadedSource(dbg, "doc-wasm-sourcemaps");
   assertPausedLocation(dbg);
 
   await waitForSource(dbg, "fib.c");
 
   ok(true, "Original sources exist");
   const mainSrc = findSource(dbg, "fib.c");
 
-  await selectSource(dbg, mainSrc);
+  await selectSpecificSource(dbg, mainSrc);
   await addBreakpoint(dbg, "fib.c", 10);
 
   resume(dbg);
 
   await waitForPaused(dbg, "fib.c");
 
   const frames = findAllElements(dbg, "frames");
   const firstFrameTitle = frames[0].querySelector(".title").textContent;
--- a/devtools/client/debugger/new/test/mochitest/browser_dbg_rr_stepping-04.js
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg_rr_stepping-04.js
@@ -22,20 +22,17 @@ async function test() {
 
   // After reverse-stepping out of the topmost frame we should rewind to the
   // last breakpoint hit.
   await reverseStepOverToLine(client, 21);
   await checkEvaluateInTopFrame(client, "number", 9);
 
   await stepOverToLine(client, 22);
   await stepOverToLine(client, 23);
-  // Line 13 seems like it should be the next stepping point, but the column
-  // numbers reported by the JS engine and required by the pause points do not
-  // match, and we don't stop here.
-  //await stepOverToLine(client, 13);
+  await stepOverToLine(client, 13);
   await stepOverToLine(client, 17);
   await stepOverToLine(client, 18);
 
   // After forward-stepping out of the topmost frame we should run forward to
   // the next breakpoint hit.
   await stepOverToLine(client, 21);
   await checkEvaluateInTopFrame(client, "number", 10);
 
--- a/devtools/client/debugger/new/test/mochitest/helpers.js
+++ b/devtools/client/debugger/new/test/mochitest/helpers.js
@@ -597,16 +597,23 @@ function waitForLoadedSources(dbg) {
  * @static
  */
 async function selectSource(dbg, url, line) {
   const source = findSource(dbg, url);
   await dbg.actions.selectLocation({ sourceId: source.id, line });
   return waitForSelectedSource(dbg, url);
 }
 
+async function selectSpecificSource(dbg, url, line) {
+  const source = findSource(dbg, url);
+  await dbg.actions.selectLocation({ sourceId: source.id, line }, {keepContext: false});
+  return waitForSelectedSource(dbg, url);
+}
+
+
 function closeTab(dbg, url) {
   const source = findSource(dbg, url);
   return dbg.actions.closeTab(source.url);
 }
 
 /**
  * Steps over.
  *
--- a/devtools/client/inspector/shared/three-pane-onboarding-tooltip.js
+++ b/devtools/client/inspector/shared/three-pane-onboarding-tooltip.js
@@ -29,45 +29,45 @@ class ThreePaneOnboardingTooltip {
       type: "arrow",
       useXulWrapper: true,
     });
 
     this.onCloseButtonClick = this.onCloseButtonClick.bind(this);
     this.onLearnMoreLinkClick = this.onLearnMoreLinkClick.bind(this);
 
     const container = doc.createElementNS(XHTML_NS, "div");
-    container.className = "three-pane-onboarding-container";
+    container.className = "onboarding-container";
 
     const icon = doc.createElementNS(XHTML_NS, "span");
-    icon.className = "three-pane-onboarding-icon";
+    icon.className = "onboarding-icon";
     container.appendChild(icon);
 
     const content = doc.createElementNS(XHTML_NS, "div");
-    content.className = "three-pane-onboarding-content";
+    content.className = "onboarding-content";
     container.appendChild(content);
 
     const message = doc.createElementNS(XHTML_NS, "div");
     const learnMoreString = L10N.getStr("inspector.threePaneOnboarding.learnMoreLink");
     const messageString = L10N.getFormatStr("inspector.threePaneOnboarding.content",
       learnMoreString);
     const learnMoreStartIndex = messageString.indexOf(learnMoreString);
 
     message.append(messageString.substring(0, learnMoreStartIndex));
 
     this.learnMoreLink = doc.createElementNS(XHTML_NS, "a");
-    this.learnMoreLink.className = "three-pane-onboarding-link";
+    this.learnMoreLink.className = "onboarding-link";
     this.learnMoreLink.href = "#";
     this.learnMoreLink.textContent = learnMoreString;
 
     message.append(this.learnMoreLink);
     message.append(messageString.substring(learnMoreStartIndex + learnMoreString.length));
     content.append(message);
 
     this.closeButton = doc.createElementNS(XHTML_NS, "button");
-    this.closeButton.className = "three-pane-onboarding-close-button devtools-button";
+    this.closeButton.className = "onboarding-close-button devtools-button";
     container.appendChild(this.closeButton);
 
     this.closeButton.addEventListener("click", this.onCloseButtonClick);
     this.learnMoreLink.addEventListener("click", this.onLearnMoreLinkClick);
 
     this.tooltip.setContent(container, { width: CONTAINER_WIDTH });
     this.tooltip.show(this.doc.querySelector("#inspector-sidebar .sidebar-toggle"), {
       position: "top",
--- a/devtools/client/jar.mn
+++ b/devtools/client/jar.mn
@@ -260,16 +260,17 @@ devtools.jar:
     skin/images/diff.svg (themes/images/diff.svg)
     skin/images/import.svg (themes/images/import.svg)
     skin/images/pane-collapse.svg (themes/images/pane-collapse.svg)
     skin/images/pane-expand.svg (themes/images/pane-expand.svg)
     skin/images/help.svg (themes/images/help.svg)
     skin/images/read-only.svg (themes/images/read-only.svg)
     skin/images/reveal.svg (themes/images/reveal.svg)
     skin/images/select-arrow.svg (themes/images/select-arrow.svg)
+    skin/images/settings.svg (themes/images/settings.svg)
 
     # Debugger
     skin/images/debugger/angular.svg (themes/images/debugger/angular.svg)
     skin/images/debugger/arrow.svg (themes/images/debugger/arrow.svg)
     skin/images/debugger/back.svg (themes/images/debugger/back.svg)
     skin/images/debugger/blackBox.svg (themes/images/debugger/blackBox.svg)
     skin/images/debugger/breakpoint.svg (themes/images/debugger/breakpoint.svg)
     skin/images/debugger/close.svg (themes/images/debugger/close.svg)
--- a/devtools/client/locales/en-US/network-throttling.properties
+++ b/devtools/client/locales/en-US/network-throttling.properties
@@ -1,13 +1,13 @@
 # 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/.
 
-# LOCALIZATION NOTE These strings are used inside the NetworkThrottlingSelector
+# LOCALIZATION NOTE These strings are used inside the NetworkThrottlingMenu
 # component used to throttle network bandwidth.
 #
 # The correct localization of this file might be to keep it in
 # English, or another language commonly spoken among web developers.
 # You want to make that choice consistent across the developer tools.
 # A good criteria is the language in which you'd find the best
 # documentation on web development on the web.
 
--- a/devtools/client/locales/en-US/responsive.properties
+++ b/devtools/client/locales/en-US/responsive.properties
@@ -6,72 +6,60 @@
 # available from the Web Developer sub-menu -> 'Responsive Design Mode'.
 #
 # The correct localization of this file might be to keep it in
 # English, or another language commonly spoken among web developers.
 # You want to make that choice consistent across the developer tools.
 # A good criteria is the language in which you'd find the best
 # documentation on web development on the web.
 
-# LOCALIZATION NOTE (responsive.editDeviceList): option displayed in the device
-# selector
-responsive.editDeviceList=Edit list…
+# LOCALIZATION NOTE (responsive.editDeviceList2): Context menu item displayed in the
+# device selector.
+responsive.editDeviceList2=Edit List…
 
-# LOCALIZATION NOTE (responsive.exit): tooltip text of the exit button.
+# LOCALIZATION NOTE (responsive.exit): Tooltip text of the exit button.
 responsive.exit=Close Responsive Design Mode
 
-# LOCALIZATION NOTE (responsive.rotate): tooltip text of the rotate button.
+# LOCALIZATION NOTE (responsive.rotate): Tooltip text of the rotate button.
 responsive.rotate=Rotate viewport
 
-# LOCALIZATION NOTE (responsive.deviceListLoading): placeholder text for
-# device selector when it's still fetching devices
-responsive.deviceListLoading=Loading…
-
-# LOCALIZATION NOTE (responsive.deviceListError): placeholder text for
-# device selector when an error occurred
-responsive.deviceListError=No list available
-
-# LOCALIZATION NOTE (responsive.done): button text in the device list modal
+# LOCALIZATION NOTE (responsive.done): Button text in the device list modal
 responsive.done=Done
 
-# LOCALIZATION NOTE (responsive.noDeviceSelected): placeholder text for the
-# device selector
-responsive.noDeviceSelected=no device selected
+# LOCALIZATION NOTE (responsive.responsiveMode): Placeholder text for the
+# device selector.
+responsive.responsiveMode=Responsive
 
-# LOCALIZATION NOTE  (responsive.title): the title displayed in the global
-# toolbar
-responsive.title=Responsive Design Mode
-
-# LOCALIZATION NOTE (responsive.enableTouch): tooltip text for the touch
-# simulation button when it's disabled
+# LOCALIZATION NOTE (responsive.enableTouch): Tooltip text for the touch
+# simulation button when it's disabled.
 responsive.enableTouch=Enable touch simulation
 
-# LOCALIZATION NOTE (responsive.disableTouch): tooltip text for the touch
-# simulation button when it's enabled
+# LOCALIZATION NOTE (responsive.disableTouch): Tooltip text for the touch
+# simulation button when it's enabled.
 responsive.disableTouch=Disable touch simulation
 
-# LOCALIZATION NOTE  (responsive.screenshot): tooltip of the screenshot button.
+# LOCALIZATION NOTE  (responsive.screenshot): Tooltip of the screenshot button.
 responsive.screenshot=Take a screenshot of the viewport
 
 # LOCALIZATION NOTE (responsive.screenshotGeneratedFilename): The auto generated
 # filename.
 # The first argument (%1$S) is the date string in yyyy-mm-dd format and the
 # second argument (%2$S) is the time string in HH.MM.SS format.
 responsive.screenshotGeneratedFilename=Screen Shot %1$S at %2$S
 
 # LOCALIZATION NOTE (responsive.remoteOnly): Message displayed in the tab's
 # notification box if a user tries to open Responsive Design Mode in a
 # non-remote tab.
 responsive.remoteOnly=Responsive Design Mode is only available for remote browser tabs, such as those used for web content in multi-process Firefox.
 
-# LOCALIZATION NOTE (responsive.changeDevicePixelRatio): tooltip for the
+# LOCALIZATION NOTE (responsive.changeDevicePixelRatio): Tooltip for the
 # device pixel ratio dropdown when is enabled.
 responsive.changeDevicePixelRatio=Change device pixel ratio of the viewport
 
-# LOCALIZATION NOTE (responsive.devicePixelRatio.auto): tooltip for the device pixel ratio
+# LOCALIZATION NOTE (responsive.devicePixelRatio.auto): Tooltip for the device pixel ratio
 # dropdown when it is disabled because a device is selected.
 # The argument (%1$S) is the selected device (e.g. iPhone 6) that set
 # automatically the device pixel ratio value.
 responsive.devicePixelRatio.auto=Device pixel ratio automatically set by %1$S
 
 # LOCALIZATION NOTE (responsive.customDeviceName): Default value in a form to
 # add a custom device based on an arbitrary size (no association to an existing
 # device).
@@ -123,28 +111,29 @@ responsive.deviceAdderSave=Save
 # device.  %4$S is the user agent of the device.  %5$S is a boolean value
 # noting whether touch input is supported.
 responsive.deviceDetails=Size: %1$S x %2$S\nDPR: %3$S\nUA: %4$S\nTouch: %5$S
 
 # LOCALIZATION NOTE (responsive.devicePixelRatioOption): UI option in a menu to configure
 # the device pixel ratio. %1$S is the devicePixelRatio value of the device.
 responsive.devicePixelRatioOption=DPR: %1$S
 
-# LOCALIZATION NOTE (responsive.reloadConditions.label): Label on button to open a menu
-# used to choose whether to reload the page automatically when certain actions occur.
-responsive.reloadConditions.label=Reload when…
-
-# LOCALIZATION NOTE (responsive.reloadConditions.title): Title on button to open a menu
-# used to choose whether to reload the page automatically when certain actions occur.
-responsive.reloadConditions.title=Choose whether to reload the page automatically when certain actions occur
-
 # LOCALIZATION NOTE (responsive.reloadConditions.touchSimulation): Label on checkbox used
 # to select whether to reload when touch simulation is toggled.
 responsive.reloadConditions.touchSimulation=Reload when touch simulation is toggled
 
 # LOCALIZATION NOTE (responsive.reloadConditions.userAgent): Label on checkbox used
 # to select whether to reload when user agent is changed.
 responsive.reloadConditions.userAgent=Reload when user agent is changed
 
 # LOCALIZATION NOTE (responsive.reloadNotification.description): Text in notification bar
 # shown on first open to clarify that some features need a reload to apply.  %1$S is the
 # label on the reload conditions menu (responsive.reloadConditions.label).
 responsive.reloadNotification.description=Device simulation changes require a reload to fully apply.  Automatic reloads are disabled by default to avoid losing any changes in DevTools.  You can enable reloading via the “%1$S” menu.
+
+# LOCALIZATION NOTE (responsive.leftAlignViewport): Label on checkbox used in the settings
+# menu.
+responsive.leftAlignViewport = Left-align Viewport
+
+# LOCALIZATION NOTE (responsive.settingOnboarding.content): This is the content shown in
+# the setting onboarding tooltip that is displayed below the settings menu button in
+# Responsive Design Mode.
+responsive.settingOnboarding.content=New: Change to left-alignment or edit reload behavior here.
--- a/devtools/client/netmonitor/src/assets/styles/RequestList.css
+++ b/devtools/client/netmonitor/src/assets/styles/RequestList.css
@@ -43,16 +43,17 @@
 /* Requests list table */
 
 .request-list-container {
   display: flex;
   flex-direction: column;
   width: 100%;
   height: 100%;
   overflow: hidden;
+  color: var(--table-text-color);
 }
 
 .requests-list-wrapper {
   width: 100%;
   height: 100%;
 }
 
 .requests-list-table {
@@ -585,18 +586,18 @@
 .request-list-item:not(.selected).odd {
   background-color: var(--table-zebra-background);
 }
 
 .request-list-item:not(.selected):hover {
   background-color: var(--theme-selection-background-hover);
 }
 
-.request-list-item.fromCache > .requests-list-column:not(.requests-list-waterfall) {
-  opacity: 0.6;
+.request-list-item:not(.selected).fromCache > .requests-list-column:not(.requests-list-waterfall) {
+  opacity: 0.7;
 }
 
 /* Responsive web design support */
 
 @media (max-width: 700px) {
   .requests-list-header-button {
     padding-inline-start: 8px;
   }
--- a/devtools/client/netmonitor/src/assets/styles/Toolbar.css
+++ b/devtools/client/netmonitor/src/assets/styles/Toolbar.css
@@ -68,53 +68,35 @@
 .devtools-button.devtools-pause-icon::before {
   background-image: var(--pause-icon-url);
 }
 
 .devtools-button.devtools-play-icon::before {
   background-image: var(--play-icon-url);
 }
 
-.devtools-button.devtools-har-button {
-  margin: 0 0 0 2px;
-  padding: 0;
-}
-
-/* style for dropdown button */
-.devtools-drop-down-button {
-  background-image: var(--select-arrow-image)  !important;
-  background-repeat: no-repeat !important;
-  margin-inline-start: 6px;
-  fill: var(--theme-toolbar-photon-icon-color);
-  -moz-context-properties: fill;
-}
-
-/* style for title holder inside a dropdown button */
-.devtools-drop-down-button .title {
-  padding-top: 0.15em;
-  text-align: center;
-  overflow: hidden;
-  display: inline-block;
-}
-
 /* HAR button */
 
 #devtools-har-button {
   width: 35px;
-  padding-right: 12px;
+  margin-inline-start: 2px;
+  padding-inline-start: 0;
+  padding-inline-end: 12px;
   background-position: right center;
 }
 
 /* Make sure spacing between text and icon is uniform */
 #devtools-har-button .title {
   width: 24px;
 }
 
-#devtools-har-button:not(:hover) {
-  background-color: transparent;
+/* Throttling Button */
+
+#network-throttling-menu {
+  margin-inline-start: 6px;
 }
 
 .devtools-checkbox {
   position: relative;
   vertical-align: middle;
   bottom: 1px;
 }
 
@@ -127,34 +109,16 @@
 .devtools-checkbox-label.devtools-persistlog-checkbox {
   margin-inline-start: 4px;
 }
 
 .devtools-checkbox-label.devtools-cache-checkbox {
   margin-inline-end: 7px;
 }
 
-/* Throttling Button */
-
-#global-network-throttling-selector:not(:hover) {
-  background-color: transparent;
-}
-
-#global-network-throttling-selector {
-  width: 92px;
-  background-position: right 4px center;
-  padding-left: 0;
-  overflow: hidden;
-}
-
-/* Make sure spacing between text and icon is uniform*/
-#global-network-throttling-selector .title{
-  width: 85%;
-}
-
 /* Filter input within the Toolbar */
 
 .devtools-toolbar-group .devtools-filterinput {
   border: none;
   box-shadow: none;
   background-color: var(--theme-body-background);
 }
 
--- a/devtools/client/netmonitor/src/assets/styles/variables.css
+++ b/devtools/client/netmonitor/src/assets/styles/variables.css
@@ -1,13 +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/. */
 
 :root.theme-dark {
+  --table-text-color: var(--grey-40);
   --table-splitter-color: rgba(255,255,255,0.15);
   --table-zebra-background: rgba(255,255,255,0.05);
 
   --timing-blocked-color: rgba(235, 83, 104, 0.8);
   --timing-dns-color: rgba(223, 128, 255, 0.8); /* pink */
   --timing-ssl-color: rgba(217, 102, 41, 0.8); /* orange */
   --timing-connect-color: rgba(217, 102, 41, 0.8); /* orange */
   --timing-send-color: rgba(70, 175, 227, 0.8); /* light blue */
@@ -16,16 +17,17 @@
 
   --sort-ascending-image: url(chrome://devtools/skin/images/sort-ascending-arrow.svg);
   --sort-descending-image: url(chrome://devtools/skin/images/sort-descending-arrow.svg);
 }
 
 :root.theme-light {
   --theme-body-color: var(--grey-70);
 
+  --table-text-color: var(--grey-70);
   --table-splitter-color: rgba(0,0,0,0.15);
   --table-zebra-background: rgba(0,0,0,0.05);
 
   --timing-blocked-color: rgba(235, 83, 104, 0.8);
   --timing-dns-color: rgba(223, 128, 255, 0.8); /* pink */
   --timing-ssl-color: rgba(217, 102, 41, 0.8); /* orange */
   --timing-connect-color: rgba(217, 102, 41, 0.8); /* orange */
   --timing-send-color: rgba(0, 136, 204, 0.8); /* blue */
--- a/devtools/client/netmonitor/src/components/Toolbar.js
+++ b/devtools/client/netmonitor/src/components/Toolbar.js
@@ -12,27 +12,27 @@ const { connect } = require("devtools/cl
 const Actions = require("../actions/index");
 const { FILTER_SEARCH_DELAY, FILTER_TAGS } = require("../constants");
 const {
   getDisplayedRequests,
   getRecordingState,
   getTypeFilteredRequests,
 } = require("../selectors/index");
 const { autocompleteProvider } = require("../utils/filter-autocomplete-provider");
-const { LocalizationHelper } = require("devtools/shared/l10n");
 const { L10N } = require("../utils/l10n");
 const { fetchNetworkUpdatePacket } = require("../utils/request-utils");
 
 // MDN
 const {
   getFilterBoxURL,
 } = require("../utils/mdn-utils");
 const LEARN_MORE_URL = getFilterBoxURL();
 
 // Components
+const NetworkThrottlingMenu = createFactory(require("devtools/client/shared/components/throttling/NetworkThrottlingMenu"));
 const SearchBox = createFactory(require("devtools/client/shared/components/SearchBox"));
 
 const { button, div, input, label, span } = dom;
 
 // Localization
 const SEARCH_KEY_SHORTCUT = L10N.getStr("netmonitor.toolbar.filterFreetext.key");
 const SEARCH_PLACE_HOLDER = L10N.getStr("netmonitor.toolbar.filterFreetext.label");
 const TOOLBAR_CLEAR = L10N.getStr("netmonitor.toolbar.clear");
@@ -46,27 +46,23 @@ const DEVTOOLS_ENABLE_PERSISTENT_LOG_PRE
 const TOOLBAR_FILTER_LABELS = FILTER_TAGS.concat("all").reduce((o, tag) =>
   Object.assign(o, { [tag]: L10N.getStr(`netmonitor.toolbar.filter.${tag}`) }), {});
 const ENABLE_PERSISTENT_LOGS_TOOLTIP =
   L10N.getStr("netmonitor.toolbar.enablePersistentLogs.tooltip");
 const ENABLE_PERSISTENT_LOGS_LABEL =
   L10N.getStr("netmonitor.toolbar.enablePersistentLogs.label");
 const DISABLE_CACHE_TOOLTIP = L10N.getStr("netmonitor.toolbar.disableCache.tooltip");
 const DISABLE_CACHE_LABEL = L10N.getStr("netmonitor.toolbar.disableCache.label");
-const NO_THROTTLING_LABEL = new LocalizationHelper(
-  "devtools/client/locales/network-throttling.properties"
-  ).getStr("responsive.noThrottling");
 
 // Menu
-loader.lazyRequireGetter(this, "showMenu", "devtools/client/netmonitor/src/utils/menu", true);
+loader.lazyRequireGetter(this, "showMenu", "devtools/client/shared/components/menu/utils", true);
 loader.lazyRequireGetter(this, "HarMenuUtils", "devtools/client/netmonitor/src/har/har-menu-utils", true);
 
 // Throttling
 const Types = require("devtools/client/shared/components/throttling/types");
-const throttlingProfiles = require("devtools/client/shared/components/throttling/profiles");
 const { changeNetworkThrottling } = require("devtools/client/shared/components/throttling/actions");
 
 /**
  * Network monitor toolbar component.
  *
  * Toolbar contains a set of useful tools to control network requests
  * as well as set of filters for filtering the content.
  */
@@ -277,74 +273,38 @@ class Toolbar extends Component {
           onChange: toggleBrowserCache,
         }),
         DISABLE_CACHE_LABEL,
       )
     );
   }
 
   /**
-   * Render network throttling selector button.
+   * Render network throttling menu button.
    */
-  renderThrottlingSelector() {
-    const {
-      networkThrottling,
-    } = this.props;
-
-    const selectedProfile = networkThrottling.enabled ?
-      networkThrottling.profile : NO_THROTTLING_LABEL;
-    return button({
-      id: "global-network-throttling-selector",
-      title: selectedProfile,
-      className: "devtools-button devtools-drop-down-button",
-      onClick: evt => {
-        this.showThrottlingSelector(evt.target);
-      },
-    },
-    dom.span({className: "title"},
-      selectedProfile)
-    );
-  }
-
-  showThrottlingSelector(menuButton) {
+  renderThrottlingMenu() {
     const {
       networkThrottling,
       onChangeNetworkThrottling,
     } = this.props;
 
-    const menuItems = throttlingProfiles.map(profile => {
-      return {
-        label: profile.id,
-        type: "checkbox",
-        checked: networkThrottling.enabled &&
-          (profile.id == networkThrottling.profile),
-        click: () => onChangeNetworkThrottling(true, profile.id),
-      };
+    return NetworkThrottlingMenu({
+      networkThrottling,
+      onChangeNetworkThrottling,
     });
-
-    menuItems.unshift("-");
-
-    menuItems.unshift({
-      label: NO_THROTTLING_LABEL,
-      type: "checkbox",
-      checked: !networkThrottling.enabled,
-      click: () => onChangeNetworkThrottling(false, ""),
-    });
-
-    showMenu(menuItems, { button: menuButton });
   }
 
   /**
    * Render drop down button with HAR related actions.
    */
   renderHarButton() {
     return button({
       id: "devtools-har-button",
       title: TOOLBAR_HAR_BUTTON,
-      className: "devtools-button devtools-har-button devtools-drop-down-button",
+      className: "devtools-button devtools-dropdown-button",
       onClick: evt => {
         this.showHarMenu(evt.target);
       },
     },
     dom.span({className: "title"},
       "HAR")
     );
   }
@@ -433,33 +393,33 @@ class Toolbar extends Component {
           this.renderSeparator(),
           this.renderToggleRecordingButton(recording, toggleRecording),
           this.renderSeparator(),
           this.renderFilterButtons(requestFilterTypes),
           this.renderSeparator(),
           this.renderPersistlogCheckbox(persistentLogsEnabled, togglePersistentLogs),
           this.renderCacheCheckbox(browserCacheDisabled, toggleBrowserCache),
           this.renderSeparator(),
-          this.renderThrottlingSelector(),
+          this.renderThrottlingMenu(),
           this.renderHarButton(),
         )
       )
     ) : (
       span({ className: "devtools-toolbar devtools-toolbar-container" },
         span({ className: "devtools-toolbar-group devtools-toolbar-two-rows-1" },
           this.renderClearButton(clearRequests),
           this.renderSeparator(),
           this.renderFilterBox(setRequestFilterText),
           this.renderSeparator(),
           this.renderToggleRecordingButton(recording, toggleRecording),
           this.renderSeparator(),
           this.renderPersistlogCheckbox(persistentLogsEnabled, togglePersistentLogs),
           this.renderCacheCheckbox(browserCacheDisabled, toggleBrowserCache),
           this.renderSeparator(),
-          this.renderThrottlingSelector(),
+          this.renderThrottlingMenu(),
           this.renderHarButton(),
         ),
         span({ className: "devtools-toolbar-group devtools-toolbar-two-rows-2" },
           this.renderFilterButtons(requestFilterTypes)
         )
       )
     );
   }
--- a/devtools/client/netmonitor/src/utils/moz.build
+++ b/devtools/client/netmonitor/src/utils/moz.build
@@ -10,15 +10,14 @@ DIRS += [
 DevToolsModules(
     'filter-autocomplete-provider.js',
     'filter-predicates.js',
     'filter-text-utils.js',
     'format-utils.js',
     'headers-provider.js',
     'l10n.js',
     'mdn-utils.js',
-    'menu.js',
     'open-request-in-tab.js',
     'prefs.js',
     'request-utils.js',
     'sort-predicates.js',
     'sort-utils.js'
 )
--- a/devtools/client/netmonitor/src/widgets/RequestListContextMenu.js
+++ b/devtools/client/netmonitor/src/widgets/RequestListContextMenu.js
@@ -12,17 +12,17 @@ const {
   getUrlQuery,
   getUrlBaseName,
   parseQueryString,
 } = require("../utils/request-utils");
 
 loader.lazyRequireGetter(this, "Curl", "devtools/client/shared/curl", true);
 loader.lazyRequireGetter(this, "saveAs", "devtools/client/shared/file-saver", true);
 loader.lazyRequireGetter(this, "copyString", "devtools/shared/platform/clipboard", true);
-loader.lazyRequireGetter(this, "showMenu", "devtools/client/netmonitor/src/utils/menu", true);
+loader.lazyRequireGetter(this, "showMenu", "devtools/client/shared/components/menu/utils", true);
 loader.lazyRequireGetter(this, "openRequestInTab", "devtools/client/netmonitor/src/utils/firefox/open-request-in-tab", true);
 loader.lazyRequireGetter(this, "HarMenuUtils", "devtools/client/netmonitor/src/har/har-menu-utils", true);
 
 class RequestListContextMenu {
   constructor(props) {
     this.props = props;
   }
 
--- a/devtools/client/netmonitor/src/widgets/RequestListHeaderContextMenu.js
+++ b/devtools/client/netmonitor/src/widgets/RequestListHeaderContextMenu.js
@@ -1,15 +1,15 @@
 /* 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 { showMenu } = require("devtools/client/netmonitor/src/utils/menu");
+const { showMenu } = require("devtools/client/shared/components/menu/utils");
 const { HEADERS } = require("../constants");
 const { L10N } = require("../utils/l10n");
 
 const stringMap = HEADERS
   .filter((header) => header.hasOwnProperty("label"))
   .reduce((acc, { name, label }) => Object.assign(acc, { [name]: label }), {});
 
 const subMenuMap = HEADERS
--- a/devtools/client/netmonitor/test/browser_net_telemetry_throttle_changed.js
+++ b/devtools/client/netmonitor/test/browser_net_telemetry_throttle_changed.js
@@ -20,17 +20,17 @@ add_task(async function() {
 
   // Remove all telemetry events.
   Services.telemetry.clearEvents();
 
   // Ensure no events have been logged
   const snapshot = Services.telemetry.snapshotEvents(OPTOUT, true);
   ok(!snapshot.parent, "No events have been logged for the main process");
 
-  document.querySelector("#global-network-throttling-selector").click();
+  document.getElementById("network-throttling-menu").click();
   monitor.panelWin.parent.document.querySelector("menuitem[label='GPRS']").click();
   await waitFor(monitor.panelWin.api, EVENTS.THROTTLING_CHANGED);
 
   // Verify existence of the telemetry event.
   checkTelemetryEvent({
     mode: "GPRS",
   }, {
     method: "throttle_changed",
--- a/devtools/client/netmonitor/webpack.config.js
+++ b/devtools/client/netmonitor/webpack.config.js
@@ -68,17 +68,18 @@ const webpackConfig = {
       "node_modules",
     ],
     alias: {
       "Services": "devtools-modules/src/Services",
       "react": path.join(__dirname, "node_modules/react"),
 
       "devtools/client/framework/devtools": path.join(__dirname, "../../client/shared/webpack/shims/framework-devtools-shim"),
       "devtools/client/framework/menu": "devtools-modules/src/menu",
-      "devtools/client/netmonitor/src/utils/menu": "devtools-contextmenu",
+
+      "devtools/client/shared/components/menu/utils": "devtools-contextmenu",
 
       "devtools/client/shared/vendor/react": "react",
       "devtools/client/shared/vendor/react-dom": "react-dom",
       "devtools/client/shared/vendor/react-redux": "react-redux",
       "devtools/client/shared/vendor/redux": "redux",
       "devtools/client/shared/vendor/reselect": "reselect",
       "devtools/client/shared/vendor/jszip": "jszip",
 
--- a/devtools/client/preferences/devtools-client.js
+++ b/devtools/client/preferences/devtools-client.js
@@ -305,22 +305,30 @@ pref("devtools.hud.loglimit", 10000);
 pref("devtools.editor.tabsize", 2);
 pref("devtools.editor.expandtab", true);
 pref("devtools.editor.keymap", "default");
 pref("devtools.editor.autoclosebrackets", true);
 pref("devtools.editor.detectindentation", true);
 pref("devtools.editor.enableCodeFolding", true);
 pref("devtools.editor.autocomplete", true);
 
+// Whether or not the viewports are left aligned.
+pref("devtools.responsive.leftAlignViewport.enabled", false);
 // Whether to reload when touch simulation is toggled
 pref("devtools.responsive.reloadConditions.touchSimulation", false);
 // Whether to reload when user agent is changed
 pref("devtools.responsive.reloadConditions.userAgent", false);
 // Whether to show the notification about reloading to apply emulation
 pref("devtools.responsive.reloadNotification.enabled", true);
+// Whether to show the settings onboarding tooltip only in release or beta builds.
+#if defined(RELEASE_OR_BETA)
+pref("devtools.responsive.show-setting-tooltip", true);
+#else
+pref("devtools.responsive.show-setting-tooltip", false);
+#endif
 
 // Enable new about:debugging.
 pref("devtools.aboutdebugging.new-enabled", false);
 pref("devtools.aboutdebugging.network-locations", "[]");
 
 // about:debugging: only show system add-ons in local builds by default.
 #ifdef MOZILLA_OFFICIAL
   pref("devtools.aboutdebugging.showSystemAddons", false);
--- a/devtools/client/responsive.html/actions/index.js
+++ b/devtools/client/responsive.html/actions/index.js
@@ -76,15 +76,18 @@ createEnum([
   "ROTATE_VIEWPORT",
 
   // Take a screenshot of the viewport.
   "TAKE_SCREENSHOT_START",
 
   // Indicates when the screenshot action ends.
   "TAKE_SCREENSHOT_END",
 
+  // Toggles the left alignment of the viewports.
+  "TOGGLE_LEFT_ALIGNMENT",
+
   // Update the device display state in the device selector.
   "UPDATE_DEVICE_DISPLAYED",
 
   // Update the device modal state.
   "UPDATE_DEVICE_MODAL",
 
 ], module.exports);
--- a/devtools/client/responsive.html/actions/moz.build
+++ b/devtools/client/responsive.html/actions/moz.build
@@ -7,10 +7,11 @@
 DevToolsModules(
     'devices.js',
     'display-pixel-ratio.js',
     'index.js',
     'location.js',
     'reload-conditions.js',
     'screenshot.js',
     'touch-simulation.js',
+    'ui.js',
     'viewports.js',
 )
--- a/devtools/client/responsive.html/actions/screenshot.js
+++ b/devtools/client/responsive.html/actions/screenshot.js
@@ -7,17 +7,17 @@
 "use strict";
 
 const {
   TAKE_SCREENSHOT_START,
   TAKE_SCREENSHOT_END,
 } = require("./index");
 
 const { getFormatStr } = require("../utils/l10n");
-const { getToplevelWindow } = require("../utils/window");
+const { getTopLevelWindow } = require("../utils/window");
 const e10s = require("../utils/e10s");
 const Services = require("Services");
 
 const CAMERA_AUDIO_URL = "resource://devtools/client/themes/audio/shutter.wav";
 
 const animationFrame = () => new Promise(resolve => {
   window.requestAnimationFrame(resolve);
 });
@@ -35,17 +35,17 @@ function getFileName() {
 
 function createScreenshotFor(node) {
   const mm = node.frameLoader.messageManager;
 
   return e10s.request(mm, "RequestScreenshot");
 }
 
 function saveToFile(data, filename) {
-  const chromeWindow = getToplevelWindow(window);
+  const chromeWindow = getTopLevelWindow(window);
   const chromeDocument = chromeWindow.document;
 
   // append .png extension to filename if it doesn't exist
   filename = filename.replace(/\.png$|$/i, ".png");
 
   chromeWindow.saveURL(data, filename, null,
                         true, true,
                         chromeDocument.documentURIObject, chromeDocument);
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/actions/ui.js
@@ -0,0 +1,20 @@
+/* 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 {
+  TOGGLE_LEFT_ALIGNMENT,
+} = require("./index");
+
+module.exports = {
+
+  toggleLeftAlignment(enabled) {
+    return {
+      type: TOGGLE_LEFT_ALIGNMENT,
+      enabled,
+    };
+  },
+
+};
rename from devtools/client/responsive.html/app.js
rename to devtools/client/responsive.html/components/App.js
--- a/devtools/client/responsive.html/app.js
+++ b/devtools/client/responsive.html/components/App.js
@@ -6,38 +6,41 @@
 
 "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");
+} = require("../actions/devices");
 const { changeNetworkThrottling } = require("devtools/client/shared/components/throttling/actions");
-const { changeReloadCondition } = require("./actions/reload-conditions");
-const { takeScreenshot } = require("./actions/screenshot");
-const { changeTouchSimulation } = require("./actions/touch-simulation");
+const { changeReloadCondition } = require("../actions/reload-conditions");
+const { takeScreenshot } = require("../actions/screenshot");
+const { changeTouchSimulation } = require("../actions/touch-simulation");
+const { toggleLeftAlignment } = require("../actions/ui");
 const {
   changeDevice,
   changePixelRatio,
   removeDeviceAssociation,
   resizeViewport,
   rotateViewport,
-} = require("./actions/viewports");
-const DeviceModal = createFactory(require("./components/DeviceModal"));
-const GlobalToolbar = createFactory(require("./components/GlobalToolbar"));
-const Viewports = createFactory(require("./components/Viewports"));
-const Types = require("./types");
+} = require("../actions/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,
       networkThrottling: PropTypes.shape(Types.networkThrottling).isRequired,
@@ -45,31 +48,33 @@ 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);
     this.onDeviceListUpdate = this.onDeviceListUpdate.bind(this);
     this.onExit = this.onExit.bind(this);
     this.onRemoveCustomDevice = this.onRemoveCustomDevice.bind(this);
     this.onRemoveDeviceAssociation = this.onRemoveDeviceAssociation.bind(this);
     this.onResizeViewport = this.onResizeViewport.bind(this);
     this.onRotateViewport = this.onRotateViewport.bind(this);
     this.onScreenshot = this.onScreenshot.bind(this);
+    this.onToggleLeftAlignment = this.onToggleLeftAlignment.bind(this);
     this.onUpdateDeviceDisplayed = this.onUpdateDeviceDisplayed.bind(this);
     this.onUpdateDeviceModal = this.onUpdateDeviceModal.bind(this);
   }
 
   onAddCustomDevice(device) {
     this.props.dispatch(addCustomDevice(device));
   }
 
@@ -154,16 +159,20 @@ class App extends Component {
   onRotateViewport(id) {
     this.props.dispatch(rotateViewport(id));
   }
 
   onScreenshot() {
     this.props.dispatch(takeScreenshot());
   }
 
+  onToggleLeftAlignment() {
+    this.props.dispatch(toggleLeftAlignment());
+  }
+
   onUpdateDeviceDisplayed(device, deviceType, displayed) {
     this.props.dispatch(updateDeviceDisplayed(device, deviceType, displayed));
   }
 
   onUpdateDeviceModal(isOpen, modalOpenedFromViewport) {
     this.props.dispatch(updateDeviceModal(isOpen, modalOpenedFromViewport));
   }
 
@@ -189,64 +198,67 @@ class App extends Component {
       onContentResize,
       onDeviceListUpdate,
       onExit,
       onRemoveCustomDevice,
       onRemoveDeviceAssociation,
       onResizeViewport,
       onRotateViewport,
       onScreenshot,
+      onToggleLeftAlignment,
       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(
       {
         id: "app",
       },
-      GlobalToolbar({
+      Toolbar({
         devices,
         displayPixelRatio,
         networkThrottling,
         reloadConditions,
         screenshot,
         selectedDevice,
         selectedPixelRatio,
         touchSimulation,
+        viewport: viewports[0],
+        onChangeDevice,
         onChangeNetworkThrottling,
         onChangePixelRatio,
         onChangeReloadCondition,
         onChangeTouchSimulation,
         onExit,
+        onRemoveDeviceAssociation,
+        onResizeViewport,
+        onRotateViewport,
         onScreenshot,
+        onToggleLeftAlignment,
+        onUpdateDeviceModal,
       }),
       Viewports({
-        devices,
         screenshot,
         viewports,
         onBrowserMounted,
-        onChangeDevice,
         onContentResize,
         onRemoveDeviceAssociation,
-        onRotateViewport,
         onResizeViewport,
-        onUpdateDeviceModal,
       }),
       DeviceModal({
         deviceAdderViewportTemplate,
         devices,
         onAddCustomDevice,
         onDeviceListUpdate,
         onRemoveCustomDevice,
         onUpdateDeviceDisplayed,
--- a/devtools/client/responsive.html/components/Browser.js
+++ b/devtools/client/responsive.html/components/Browser.js
@@ -4,22 +4,22 @@
 
 /* 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 { getTopLevelWindow } = require("../utils/window");
 
 const FRAME_SCRIPT = "resource://devtools/client/responsive.html/browser/content.js";
 
 class Browser extends PureComponent {
   /**
    * This component is not allowed to depend directly on frequently changing data (width,
    * height). Any changes in props would cause the <iframe> to be removed and added again,
    * throwing away the current state of the page.
@@ -109,17 +109,17 @@ class Browser extends PureComponent {
     // since it still needs to do async work before the content is actually
     // resized to match.
     e10s.on(mm, "OnContentResize", onContentResize);
 
     const ready = e10s.once(mm, "ChildScriptReady");
     mm.loadFrameScript(FRAME_SCRIPT, true);
     await ready;
 
-    const browserWindow = getToplevelWindow(window);
+    const browserWindow = getTopLevelWindow(window);
     const requiresFloatingScrollbars =
       !browserWindow.matchMedia("(-moz-overlay-scrollbars)").matches;
 
     await e10s.request(mm, "Start", {
       requiresFloatingScrollbars,
       // Tests expect events on resize to wait for various size changes
       notifyOnResize: flags.testing,
     });
--- 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);
@@ -147,21 +150,21 @@ class DeviceModal extends PureComponent 
 
     return dom.div(
       {
         id: "device-modal-wrapper",
         className: this.props.devices.isModalOpen ? "opened" : "closed",
       },
       dom.div(
         {
-          className: "device-modal container",
+          className: "device-modal",
         },
         dom.button({
           id: "device-close-button",
-          className: "toolbar-button devtools-button",
+          className: "devtools-button",
           onClick: () => onUpdateDeviceModal(false),
         }),
         dom.div(
           {
             className: "device-modal-content",
           },
           devices.types.map(type => {
             return dom.div(
@@ -179,17 +182,17 @@ class DeviceModal extends PureComponent 
                 const details = getFormatStr(
                   "responsive.deviceDetails", device.width, device.height,
                   device.pixelRatio, device.userAgent, device.touch
                 );
 
                 let removeDeviceButton;
                 if (type == "custom") {
                   removeDeviceButton = dom.button({
-                    className: "device-remove-button toolbar-button devtools-button",
+                    className: "device-remove-button devtools-button",
                     onClick: () => onRemoveCustomDevice(device),
                   });
                 }
 
                 return dom.label(
                   {
                     className: "device-label",
                     key: device.name,
rename from devtools/client/responsive.html/components/DevicePixelRatioSelector.js
rename to devtools/client/responsive.html/components/DevicePixelRatioMenu.js
--- a/devtools/client/responsive.html/components/DevicePixelRatioSelector.js
+++ b/devtools/client/responsive.html/components/DevicePixelRatioMenu.js
@@ -1,136 +1,93 @@
 /* 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);
+
+loader.lazyRequireGetter(this, "showMenu", "devtools/client/shared/components/menu/utils", true);
 
 const PIXEL_RATIO_PRESET = [1, 2, 3];
 
-const createVisibleOption = value => {
-  const label = labelForOption(value);
-  return dom.option({
-    value,
-    title: label,
-    key: value,
-  }, label);
-};
-
-const createHiddenOption = value => {
-  const label = labelForOption(value);
-  return dom.option({
-    value,
-    title: label,
-    hidden: true,
-    disabled: true,
-  }, label);
-};
-
-class DevicePixelRatioSelector extends PureComponent {
+class DevicePixelRatioMenu extends PureComponent {
   static get propTypes() {
     return {
       devices: PropTypes.shape(Types.devices).isRequired,
       displayPixelRatio: Types.pixelRatio.value.isRequired,
       selectedDevice: PropTypes.string.isRequired,
       selectedPixelRatio: PropTypes.shape(Types.pixelRatio).isRequired,
       onChangePixelRatio: PropTypes.func.isRequired,
     };
   }
 
   constructor(props) {
     super(props);
-
-    this.state = {
-      isFocused: false
-    };
-
-    this.onFocusChange = this.onFocusChange.bind(this);
-    this.onSelectChange = this.onSelectChange.bind(this);
+    this.onShowDevicePixelMenu = this.onShowDevicePixelMenu.bind(this);
   }
 
-  onFocusChange({type}) {
-    this.setState({
-      isFocused: type === "focus"
+  onShowDevicePixelMenu(event) {
+    const {
+      displayPixelRatio,
+      onChangePixelRatio,
+    } = this.props;
+
+    const menuItems = PIXEL_RATIO_PRESET.map(value => {
+      return {
+        label: getFormatStr("responsive.devicePixelRatioOption", value),
+        type: "checkbox",
+        checked: displayPixelRatio === value,
+        click: () => onChangePixelRatio(+value),
+      };
     });
-  }
 
-  onSelectChange({ target }) {
-    this.props.onChangePixelRatio(+target.value);
+    showMenu(menuItems, {
+      button: event.target,
+      useTopLevelWindow: true,
+    });
   }
 
   render() {
     const {
       devices,
       displayPixelRatio,
       selectedDevice,
       selectedPixelRatio,
     } = this.props;
 
-    const hiddenOptions = [];
-
-    for (const type of devices.types) {
-      for (const device of devices[type]) {
-        if (device.displayed &&
-            !hiddenOptions.includes(device.pixelRatio) &&
-            !PIXEL_RATIO_PRESET.includes(device.pixelRatio)) {
-          hiddenOptions.push(device.pixelRatio);
-        }
-      }
-    }
+    const isDisabled = devices.listState !== Types.loadableState.LOADED ||
+      selectedDevice !== "";
 
-    if (!PIXEL_RATIO_PRESET.includes(displayPixelRatio)) {
-      hiddenOptions.push(displayPixelRatio);
-    }
-
-    const state = devices.listState;
-    const isDisabled = (state !== Types.loadableState.LOADED) || (selectedDevice !== "");
-    let selectorClass = "toolbar-dropdown";
     let title;
-
     if (isDisabled) {
-      selectorClass += " disabled";
       title = getFormatStr("responsive.devicePixelRatio.auto", selectedDevice);
     } else {
       title = getStr("responsive.changeDevicePixelRatio");
-
-      if (selectedPixelRatio.value) {
-        selectorClass += " selected";
-      }
-    }
-
-    if (this.state.isFocused) {
-      selectorClass += " focused";
     }
 
-    let listContent = PIXEL_RATIO_PRESET.map(createVisibleOption);
-
-    if (state == Types.loadableState.LOADED) {
-      listContent = listContent.concat(hiddenOptions.map(createHiddenOption));
-    }
-
-    return dom.select(
-      {
-        id: "global-device-pixel-ratio-selector",
-        value: selectedPixelRatio.value || displayPixelRatio,
-        disabled: isDisabled,
-        onChange: this.onSelectChange,
-        onFocus: this.onFocusChange,
-        onBlur: this.onFocusChange,
-        className: selectorClass,
-        title: title
-      },
-      ...listContent
+    return (
+      dom.button(
+        {
+          id: "device-pixel-ratio-menu",
+          className: "devtools-button devtools-dropdown-button",
+          disabled: isDisabled,
+          title,
+          onClick: this.onShowDevicePixelMenu,
+        },
+        dom.span({ className: "title" },
+          getFormatStr("responsive.devicePixelRatioOption",
+            selectedPixelRatio.value || displayPixelRatio)
+        )
+      )
     );
   }
 }
 
-module.exports = DevicePixelRatioSelector;
+module.exports = DevicePixelRatioMenu;
--- a/devtools/client/responsive.html/components/DeviceSelector.js
+++ b/devtools/client/responsive.html/components/DeviceSelector.js
@@ -1,131 +1,103 @@
 /* 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 PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 
+const { getStr } = require("../utils/l10n");
 const Types = require("../types");
-const OPEN_DEVICE_MODAL_VALUE = "OPEN_DEVICE_MODAL";
+
+loader.lazyRequireGetter(this, "showMenu", "devtools/client/shared/components/menu/utils", true);
 
 class DeviceSelector extends PureComponent {
   static get propTypes() {
     return {
       devices: PropTypes.shape(Types.devices).isRequired,
       selectedDevice: PropTypes.string.isRequired,
       viewportId: PropTypes.number.isRequired,
       onChangeDevice: PropTypes.func.isRequired,
       onResizeViewport: PropTypes.func.isRequired,
       onUpdateDeviceModal: PropTypes.func.isRequired,
     };
   }
 
   constructor(props) {
     super(props);
-    this.onSelectChange = this.onSelectChange.bind(this);
+    this.onShowDeviceMenu = this.onShowDeviceMenu.bind(this);
   }
 
-  onSelectChange({ target }) {
+  onShowDeviceMenu(event) {
     const {
       devices,
+      selectedDevice,
       viewportId,
       onChangeDevice,
       onResizeViewport,
       onUpdateDeviceModal,
     } = this.props;
 
-    if (target.value === OPEN_DEVICE_MODAL_VALUE) {
-      onUpdateDeviceModal(true, viewportId);
-      return;
-    }
+    const menuItems = [];
+
     for (const type of devices.types) {
       for (const device of devices[type]) {
-        if (device.name === target.value) {
-          onResizeViewport(device.width, device.height);
-          onChangeDevice(device, type);
-          return;
+        if (device.displayed) {
+          menuItems.push({
+            label: device.name,
+            type: "checkbox",
+            checked: selectedDevice === device.name,
+            click: () => {
+              onResizeViewport(viewportId, device.width, device.height);
+              onChangeDevice(viewportId, device, type);
+            },
+          });
         }
       }
     }
+
+    menuItems.sort(function(a, b) {
+      return a.label.localeCompare(b.label);
+    });
+
+    if (menuItems.length > 0) {
+      menuItems.push("-");
+    }
+
+    menuItems.push({
+      label: getStr("responsive.editDeviceList2"),
+      click: () => onUpdateDeviceModal(true, viewportId),
+    });
+
+    showMenu(menuItems, {
+      button: event.target,
+      useTopLevelWindow: true,
+    });
   }
 
   render() {
     const {
       devices,
       selectedDevice,
     } = this.props;
 
-    const options = [];
-    for (const type of devices.types) {
-      for (const device of devices[type]) {
-        if (device.displayed) {
-          options.push(device);
-        }
-      }
-    }
-
-    options.sort(function(a, b) {
-      return a.name.localeCompare(b.name);
-    });
-
-    let selectClass = "viewport-device-selector toolbar-dropdown";
-    if (selectedDevice) {
-      selectClass += " selected";
-    }
-
-    const state = devices.listState;
-    let listContent;
-
-    if (state == Types.loadableState.LOADED) {
-      listContent = [
-        dom.option({
-          value: "",
-          title: "",
-          disabled: true,
-          hidden: true,
+    return (
+      dom.button(
+        {
+          id: "device-selector",
+          className: "devtools-button devtools-dropdown-button",
+          disabled: devices.listState !== Types.loadableState.LOADED,
+          title: selectedDevice,
+          onClick: this.onShowDeviceMenu,
         },
-        getStr("responsive.noDeviceSelected")),
-        options.map(device => {
-          return dom.option({
-            key: device.name,
-            value: device.name,
-            title: "",
-          }, device.name);
-        }),
-        dom.option({
-          value: OPEN_DEVICE_MODAL_VALUE,
-          title: "",
-        }, getStr("responsive.editDeviceList"))];
-    } else if (state == Types.loadableState.LOADING
-      || state == Types.loadableState.INITIALIZED) {
-      listContent = [dom.option({
-        value: "",
-        title: "",
-        disabled: true,
-      }, getStr("responsive.deviceListLoading"))];
-    } else if (state == Types.loadableState.ERROR) {
-      listContent = [dom.option({
-        value: "",
-        title: "",
-        disabled: true,
-      }, getStr("responsive.deviceListError"))];
-    }
-
-    return dom.select(
-      {
-        className: selectClass,
-        value: selectedDevice,
-        title: selectedDevice,
-        onChange: this.onSelectChange,
-        disabled: (state !== Types.loadableState.LOADED),
-      },
-      ...listContent
+        dom.span({ className: "title" },
+          selectedDevice || getStr("responsive.responsiveMode")
+        )
+      )
     );
   }
 }
 
 module.exports = DeviceSelector;
--- a/devtools/client/responsive.html/components/ResizableViewport.js
+++ b/devtools/client/responsive.html/components/ResizableViewport.js
@@ -2,58 +2,121 @@
  * 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 ViewportToolbar = createFactory(require("./ViewportToolbar"));
 
 const VIEWPORT_MIN_WIDTH = Constants.MIN_VIEWPORT_DIMENSION;
 const VIEWPORT_MIN_HEIGHT = Constants.MIN_VIEWPORT_DIMENSION;
 
 class ResizableViewport extends Component {
   static get propTypes() {
     return {
-      devices: PropTypes.shape(Types.devices).isRequired,
+      leftAlignmentEnabled: PropTypes.bool.isRequired,
       screenshot: PropTypes.shape(Types.screenshot).isRequired,
       swapAfterMount: PropTypes.bool.isRequired,
       viewport: PropTypes.shape(Types.viewport).isRequired,
       onBrowserMounted: PropTypes.func.isRequired,
-      onChangeDevice: PropTypes.func.isRequired,
       onContentResize: PropTypes.func.isRequired,
       onRemoveDeviceAssociation: PropTypes.func.isRequired,
       onResizeViewport: PropTypes.func.isRequired,
-      onRotateViewport: PropTypes.func.isRequired,
-      onUpdateDeviceModal: PropTypes.func.isRequired,
     };
   }
 
   constructor(props) {
     super(props);
 
     this.state = {
       isResizing: false,
       lastClientX: 0,
       lastClientY: 0,
       ignoreX: false,
       ignoreY: false,
     };
 
+    this.onRemoveDeviceAssociation = this.onRemoveDeviceAssociation.bind(this);
+    this.onResizeDrag = this.onResizeDrag.bind(this);
     this.onResizeStart = this.onResizeStart.bind(this);
     this.onResizeStop = this.onResizeStop.bind(this);
-    this.onResizeDrag = this.onResizeDrag.bind(this);
+    this.onResizeViewport = this.onResizeViewport.bind(this);
+  }
+
+  onRemoveDeviceAssociation() {
+    const {
+      viewport,
+      onRemoveDeviceAssociation,
+    } = this.props;
+
+    onRemoveDeviceAssociation(viewport.id);
+  }
+
+  onResizeDrag({ clientX, clientY }) {
+    if (!this.state.isResizing) {
+      return;
+    }
+
+    let { lastClientX, lastClientY, ignoreX, ignoreY } = this.state;
+    let deltaX = clientX - lastClientX;
+    let deltaY = clientY - lastClientY;
+
+    if (!this.props.leftAlignmentEnabled) {
+      // The viewport is centered horizontally, so horizontal resize resizes
+      // by twice the distance the mouse was dragged - on left and right side.
+      deltaX = deltaX * 2;
+    }
+
+    if (ignoreX) {
+      deltaX = 0;
+    }
+    if (ignoreY) {
+      deltaY = 0;
+    }
+
+    let width = this.props.viewport.width + deltaX;
+    let height = this.props.viewport.height + deltaY;
+
+    if (width < VIEWPORT_MIN_WIDTH) {
+      width = VIEWPORT_MIN_WIDTH;
+    } else {
+      lastClientX = clientX;
+    }
+
+    if (height < VIEWPORT_MIN_HEIGHT) {
+      height = VIEWPORT_MIN_HEIGHT;
+    } else {
+      lastClientY = clientY;
+    }
+
+    // Update the viewport store with the new width and height.
+    this.onResizeViewport(width, height);
+    // 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) {
+      // In bug 1329843 and others, we may eventually stop this approach of removing the
+      // the properties of the device on resize.  However, at the moment, there is no
+      // way to edit dPR when a device is selected, and there is no UI at all for editing
+      // UA, so it's important to keep doing this for now.
+      this.onRemoveDeviceAssociation();
+    }
+
+    this.setState({
+      lastClientX,
+      lastClientY
+    });
   }
 
   onResizeStart({ target, clientX, clientY }) {
     window.addEventListener("mousemove", this.onResizeDrag, true);
     window.addEventListener("mouseup", this.onResizeStop, true);
 
     this.setState({
       isResizing: true,
@@ -72,129 +135,75 @@ class ResizableViewport extends Componen
       isResizing: false,
       lastClientX: 0,
       lastClientY: 0,
       ignoreX: false,
       ignoreY: false,
     });
   }
 
-  onResizeDrag({ clientX, clientY }) {
-    if (!this.state.isResizing) {
-      return;
-    }
-
-    let { lastClientX, lastClientY, ignoreX, ignoreY } = this.state;
-    // the viewport is centered horizontally, so horizontal resize resizes
-    // by twice the distance the mouse was dragged - on left and right side.
-    let deltaX = 2 * (clientX - lastClientX);
-    let deltaY = (clientY - lastClientY);
-
-    if (ignoreX) {
-      deltaX = 0;
-    }
-    if (ignoreY) {
-      deltaY = 0;
-    }
-
-    let width = this.props.viewport.width + deltaX;
-    let height = this.props.viewport.height + deltaY;
+  onResizeViewport(width, height) {
+    const {
+      viewport,
+      onResizeViewport,
+    } = this.props;
 
-    if (width < VIEWPORT_MIN_WIDTH) {
-      width = VIEWPORT_MIN_WIDTH;
-    } else {
-      lastClientX = clientX;
-    }
-
-    if (height < VIEWPORT_MIN_HEIGHT) {
-      height = VIEWPORT_MIN_HEIGHT;
-    } else {
-      lastClientY = clientY;
-    }
-
-    // Update the viewport store with the new width and height.
-    this.props.onResizeViewport(width, height);
-    // 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) {
-      // In bug 1329843 and others, we may eventually stop this approach of removing the
-      // the properties of the device on resize.  However, at the moment, there is no
-      // way to edit dPR when a device is selected, and there is no UI at all for editing
-      // UA, so it's important to keep doing this for now.
-      this.props.onRemoveDeviceAssociation();
-    }
-
-    this.setState({
-      lastClientX,
-      lastClientY
-    });
+    onResizeViewport(viewport.id, width, height);
   }
 
   render() {
     const {
-      devices,
       screenshot,
       swapAfterMount,
       viewport,
       onBrowserMounted,
-      onChangeDevice,
       onContentResize,
-      onResizeViewport,
-      onRotateViewport,
-      onUpdateDeviceModal,
     } = this.props;
 
     let resizeHandleClass = "viewport-resize-handle";
     if (screenshot.isCapturing) {
       resizeHandleClass += " hidden";
     }
 
     let contentClass = "viewport-content";
     if (this.state.isResizing) {
       contentClass += " resizing";
     }
 
-    return dom.div(
-      {
-        className: "resizable-viewport",
-      },
-      ViewportToolbar({
-        devices,
-        viewport,
-        onChangeDevice,
-        onResizeViewport,
-        onRotateViewport,
-        onUpdateDeviceModal,
-      }),
-      dom.div(
-        {
-          className: contentClass,
-          style: {
-            width: viewport.width + "px",
-            height: viewport.height + "px",
-          },
-        },
-        Browser({
-          swapAfterMount,
-          userContextId: viewport.userContextId,
-          onBrowserMounted,
-          onContentResize,
-        })
-      ),
-      dom.div({
-        className: resizeHandleClass,
-        onMouseDown: this.onResizeStart,
-      }),
-      dom.div({
-        ref: "resizeBarX",
-        className: "viewport-horizontal-resize-handle",
-        onMouseDown: this.onResizeStart,
-      }),
-      dom.div({
-        ref: "resizeBarY",
-        className: "viewport-vertical-resize-handle",
-        onMouseDown: this.onResizeStart,
-      })
+    return (
+      dom.div({ className: "viewport" },
+        dom.div({ className: "resizable-viewport" },
+          dom.div(
+            {
+              className: contentClass,
+              style: {
+                width: viewport.width + "px",
+                height: viewport.height + "px",
+              },
+            },
+            Browser({
+              swapAfterMount,
+              userContextId: viewport.userContextId,
+              onBrowserMounted,
+              onContentResize,
+            })
+          ),
+          dom.div({
+            className: resizeHandleClass,
+            onMouseDown: this.onResizeStart,
+          }),
+          dom.div({
+            ref: "resizeBarX",
+            className: "viewport-horizontal-resize-handle",
+            onMouseDown: this.onResizeStart,
+          }),
+          dom.div({
+            ref: "resizeBarY",
+            className: "viewport-vertical-resize-handle",
+            onMouseDown: this.onResizeStart,
+          })
+        )
+      )
     );
   }
 }
 
 module.exports = ResizableViewport;
rename from devtools/client/responsive.html/components/ReloadConditions.js
rename to devtools/client/responsive.html/components/SettingsMenu.js
--- a/devtools/client/responsive.html/components/ReloadConditions.js
+++ b/devtools/client/responsive.html/components/SettingsMenu.js
@@ -1,49 +1,94 @@
 /* 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 { connect } = require("devtools/client/shared/vendor/react-redux");
+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 } = require("../utils/l10n");
 const Types = require("../types");
-const { getStr } = require("../utils/l10n");
-const ToggleMenu = createFactory(require("./ToggleMenu"));
 
-class ReloadConditions extends PureComponent {
+loader.lazyRequireGetter(this, "showMenu", "devtools/client/shared/components/menu/utils", true);
+
+class SettingsMenu extends PureComponent {
   static get propTypes() {
     return {
+      leftAlignmentEnabled: PropTypes.bool.isRequired,
       reloadConditions: PropTypes.shape(Types.reloadConditions).isRequired,
       onChangeReloadCondition: PropTypes.func.isRequired,
+      onToggleLeftAlignment: PropTypes.func.isRequired,
     };
   }
 
+  constructor(props) {
+    super(props);
+    this.onToggleSettingMenu = this.onToggleSettingMenu.bind(this);
+  }
+
+  onToggleSettingMenu(event) {
+    const {
+      leftAlignmentEnabled,
+      reloadConditions,
+      onChangeReloadCondition,
+      onToggleLeftAlignment,
+    } = this.props;
+
+    const menuItems = [
+      {
+        id: "toggleLeftAlignment",
+        checked: leftAlignmentEnabled,
+        label: getStr("responsive.leftAlignViewport"),
+        type: "checkbox",
+        click: () => {
+          onToggleLeftAlignment();
+        },
+      },
+      "-",
+      {
+        id: "touchSimulation",
+        checked: reloadConditions.touchSimulation,
+        label: getStr("responsive.reloadConditions.touchSimulation"),
+        type: "checkbox",
+        click: () => {
+          onChangeReloadCondition("touchSimulation", !reloadConditions.touchSimulation);
+        },
+      },
+      {
+        id: "userAgent",
+        checked: reloadConditions.userAgent,
+        label: getStr("responsive.reloadConditions.userAgent"),
+        type: "checkbox",
+        click: () => {
+          onChangeReloadCondition("userAgent", !reloadConditions.userAgent);
+        },
+      },
+    ];
+
+    showMenu(menuItems, {
+      button: event.target,
+      useTopLevelWindow: true,
+    });
+  }
+
   render() {
-    const {
-      reloadConditions,
-      onChangeReloadCondition,
-    } = this.props;
-
-    return ToggleMenu({
-      id: "global-reload-conditions-menu",
-      items: [
-        {
-          id: "touchSimulation",
-          label: getStr("responsive.reloadConditions.touchSimulation"),
-          checked: reloadConditions.touchSimulation,
-        },
-        {
-          id: "userAgent",
-          label: getStr("responsive.reloadConditions.userAgent"),
-          checked: reloadConditions.userAgent,
-        },
-      ],
-      label: getStr("responsive.reloadConditions.label"),
-      title: getStr("responsive.reloadConditions.title"),
-      onChange: onChangeReloadCondition,
-    });
+    return (
+      dom.button({
+        id: "settings-button",
+        className: "devtools-button",
+        onClick: this.onToggleSettingMenu,
+      })
+    );
   }
 }
 
-module.exports = ReloadConditions;
+const mapStateToProps = state => {
+  return {
+    leftAlignmentEnabled: state.ui.leftAlignmentEnabled,
+  };
+};
+
+module.exports = connect(mapStateToProps)(SettingsMenu);
deleted file mode 100644
--- a/devtools/client/responsive.html/components/ToggleMenu.js
+++ /dev/null
@@ -1,130 +0,0 @@
-/* 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 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 {
-  static get propTypes() {
-    return {
-      id: PropTypes.string,
-      items: PropTypes.arrayOf(PropTypes.shape(MenuItem)).isRequired,
-      label: PropTypes.string,
-      title: PropTypes.string,
-      onChange: PropTypes.func.isRequired,
-    };
-  }
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      isOpen: false,
-    };
-
-    this.onItemChange = this.onItemChange.bind(this);
-    this.onToggleOpen = this.onToggleOpen.bind(this);
-  }
-
-  onItemChange({ target }) {
-    const {
-      onChange,
-    } = this.props;
-
-    // Close menu after changing an item
-    this.setState({
-      isOpen: false,
-    });
-
-    const id = target.name;
-    onChange(id, target.checked);
-  }
-
-  onToggleOpen() {
-    const {
-      isOpen,
-    } = this.state;
-
-    this.setState({
-      isOpen: !isOpen,
-    });
-  }
-
-  render() {
-    const {
-      id: menuID,
-      items,
-      label: toggleLabel,
-      title,
-    } = this.props;
-
-    const {
-      isOpen,
-    } = this.state;
-
-    const {
-      onItemChange,
-      onToggleOpen,
-    } = this;
-
-    const menuItems = items.map(({ id, label, checked }) => {
-      const inputID = `devtools-menu-item-${id}`;
-
-      return dom.div(
-        {
-          className: "devtools-menu-item",
-          key: id,
-        },
-        dom.input({
-          type: "checkbox",
-          id: inputID,
-          name: id,
-          checked,
-          onChange: onItemChange,
-        }),
-        dom.label({
-          htmlFor: inputID,
-        }, label)
-      );
-    });
-
-    let menuClass = "devtools-menu";
-    if (isOpen) {
-      menuClass += " opened";
-    }
-    const menu = dom.div(
-      {
-        className: menuClass,
-      },
-      menuItems
-    );
-
-    let buttonClass = "devtools-toggle-menu";
-    buttonClass += " toolbar-dropdown toolbar-button devtools-button";
-    if (isOpen || items.some(({ checked }) => checked)) {
-      buttonClass += " selected";
-    }
-    return dom.div(
-      {
-        id: menuID,
-        className: buttonClass,
-        title,
-        onClick: onToggleOpen,
-      },
-      toggleLabel,
-      menu
-    );
-  }
-}
-
-module.exports = ToggleMenu;
rename from devtools/client/responsive.html/components/GlobalToolbar.js
rename to devtools/client/responsive.html/components/Toolbar.js
--- a/devtools/client/responsive.html/components/GlobalToolbar.js
+++ b/devtools/client/responsive.html/components/Toolbar.js
@@ -1,110 +1,145 @@
 /* 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 DevicePixelRatioMenu = createFactory(require("./DevicePixelRatioMenu"));
+const DeviceSelector = createFactory(require("./DeviceSelector"));
+const NetworkThrottlingMenu = createFactory(require("devtools/client/shared/components/throttling/NetworkThrottlingMenu"));
+const SettingsMenu = createFactory(require("./SettingsMenu"));
+const ViewportDimension = createFactory(require("./ViewportDimension"));
 
 const { getStr } = require("../utils/l10n");
 const Types = require("../types");
-const DevicePixelRatioSelector = createFactory(require("./DevicePixelRatioSelector"));
-const NetworkThrottlingSelector = createFactory(require("devtools/client/shared/components/throttling/NetworkThrottlingSelector"));
-const ReloadConditions = createFactory(require("./ReloadConditions"));
 
-class GlobalToolbar extends PureComponent {
+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,
+      onRotateViewport: PropTypes.func.isRequired,
       onScreenshot: PropTypes.func.isRequired,
+      onToggleLeftAlignment: 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,
+      onRotateViewport,
       onScreenshot,
+      onToggleLeftAlignment,
+      onUpdateDeviceModal,
     } = this.props;
 
-    let touchButtonClass = "toolbar-button devtools-button";
-    if (touchSimulation.enabled) {
-      touchButtonClass += " checked";
-    }
-
     return dom.header(
-      {
-        id: "global-toolbar",
-        className: "container",
-      },
-      dom.span(
-        {
-          className: "title",
-        },
-        getStr("responsive.title")
-      ),
-      NetworkThrottlingSelector({
-        networkThrottling,
-        onChangeNetworkThrottling,
-      }),
-      DevicePixelRatioSelector({
+      { id: "toolbar" },
+      DeviceSelector({
         devices,
-        displayPixelRatio,
         selectedDevice,
-        selectedPixelRatio,
-        onChangePixelRatio,
+        viewportId: viewport.id,
+        onChangeDevice,
+        onResizeViewport,
+        onUpdateDeviceModal,
       }),
-      ReloadConditions({
-        reloadConditions,
-        onChangeReloadCondition,
-      }),
-      dom.button({
-        id: "global-touch-simulation-button",
-        className: touchButtonClass,
-        title: (touchSimulation.enabled ?
-          getStr("responsive.disableTouch") : getStr("responsive.enableTouch")),
-        onClick: () => onChangeTouchSimulation(!touchSimulation.enabled),
-      }),
-      dom.button({
-        id: "global-screenshot-button",
-        className: "toolbar-button devtools-button",
-        title: getStr("responsive.screenshot"),
-        onClick: onScreenshot,
-        disabled: screenshot.isCapturing,
-      }),
-      dom.button({
-        id: "global-exit-button",
-        className: "toolbar-button devtools-button",
-        title: getStr("responsive.exit"),
-        onClick: onExit,
-      })
+      dom.div(
+        { id: "toolbar-center-controls" },
+        ViewportDimension({
+          viewport,
+          onRemoveDeviceAssociation,
+          onResizeViewport,
+        }),
+        dom.button({
+          id: "rotate-button",
+          className: "devtools-button",
+          onClick: () => onRotateViewport(viewport.id),
+          title: getStr("responsive.rotate"),
+        }),
+        dom.div({ className: "devtools-separator" }),
+        DevicePixelRatioMenu({
+          devices,
+          displayPixelRatio,
+          selectedDevice,
+          selectedPixelRatio,
+          onChangePixelRatio,
+        }),
+        dom.div({ className: "devtools-separator" }),
+        NetworkThrottlingMenu({
+          networkThrottling,
+          onChangeNetworkThrottling,
+          useTopLevelWindow: true,
+        }),
+        dom.div({ className: "devtools-separator" }),
+        dom.button({
+          id: "touch-simulation-button",
+          className: "devtools-button" +
+                     (touchSimulation.enabled ? " checked" : ""),
+          title: (touchSimulation.enabled ?
+            getStr("responsive.disableTouch") : getStr("responsive.enableTouch")),
+          onClick: () => onChangeTouchSimulation(!touchSimulation.enabled),
+        })
+      ),
+      dom.div(
+        { id: "toolbar-end-controls" },
+        dom.button({
+          id: "screenshot-button",
+          className: "devtools-button",
+          title: getStr("responsive.screenshot"),
+          onClick: onScreenshot,
+          disabled: screenshot.isCapturing,
+        }),
+        SettingsMenu({
+          reloadConditions,
+          onChangeReloadCondition,
+          onToggleLeftAlignment,
+        }),
+        dom.div({ className: "devtools-separator" }),
+        dom.button({
+          id: "exit-button",
+          className: "devtools-button",
+          title: getStr("responsive.exit"),
+          onClick: onExit,
+        })
+      )
     );
   }
 }
 
-module.exports = GlobalToolbar;
+module.exports = Toolbar;
deleted file mode 100644
--- a/devtools/client/responsive.html/components/Viewport.js
+++ /dev/null
@@ -1,120 +0,0 @@
-/* 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 PropTypes = require("devtools/client/shared/vendor/react-prop-types");
-const dom = require("devtools/client/shared/vendor/react-dom-factories");
-
-const Types = require("../types");
-const ResizableViewport = createFactory(require("./ResizableViewport"));
-const ViewportDimension = createFactory(require("./ViewportDimension"));
-
-class Viewport extends Component {
-  static get propTypes() {
-    return {
-      devices: PropTypes.shape(Types.devices).isRequired,
-      screenshot: PropTypes.shape(Types.screenshot).isRequired,
-      swapAfterMount: PropTypes.bool.isRequired,
-      viewport: PropTypes.shape(Types.viewport).isRequired,
-      onBrowserMounted: PropTypes.func.isRequired,
-      onChangeDevice: PropTypes.func.isRequired,
-      onContentResize: PropTypes.func.isRequired,
-      onRemoveDeviceAssociation: PropTypes.func.isRequired,
-      onResizeViewport: PropTypes.func.isRequired,
-      onRotateViewport: PropTypes.func.isRequired,
-      onUpdateDeviceModal: PropTypes.func.isRequired,
-    };
-  }
-
-  constructor(props) {
-    super(props);
-    this.onChangeDevice = this.onChangeDevice.bind(this);
-    this.onRemoveDeviceAssociation = this.onRemoveDeviceAssociation.bind(this);
-    this.onResizeViewport = this.onResizeViewport.bind(this);
-    this.onRotateViewport = this.onRotateViewport.bind(this);
-  }
-
-  onChangeDevice(device, deviceType) {
-    const {
-      viewport,
-      onChangeDevice,
-    } = this.props;
-
-    onChangeDevice(viewport.id, device, deviceType);
-  }
-
-  onRemoveDeviceAssociation() {
-    const {
-      viewport,
-      onRemoveDeviceAssociation,
-    } = this.props;
-
-    onRemoveDeviceAssociation(viewport.id);
-  }
-
-  onResizeViewport(width, height) {
-    const {
-      viewport,
-      onResizeViewport,
-    } = this.props;
-
-    onResizeViewport(viewport.id, width, height);
-  }
-
-  onRotateViewport() {
-    const {
-      viewport,
-      onRotateViewport,
-    } = this.props;
-
-    onRotateViewport(viewport.id);
-  }
-
-  render() {
-    const {
-      devices,
-      screenshot,
-      swapAfterMount,
-      viewport,
-      onBrowserMounted,
-      onContentResize,
-      onUpdateDeviceModal,
-    } = this.props;
-
-    const {
-      onChangeDevice,
-      onRemoveDeviceAssociation,
-      onRotateViewport,
-      onResizeViewport,
-    } = this;
-
-    return dom.div(
-      {
-        className: "viewport",
-      },
-      ViewportDimension({
-        viewport,
-        onChangeSize: onResizeViewport,
-        onRemoveDeviceAssociation,
-      }),
-      ResizableViewport({
-        devices,
-        screenshot,
-        swapAfterMount,
-        viewport,
-        onBrowserMounted,
-        onChangeDevice,
-        onContentResize,
-        onRemoveDeviceAssociation,
-        onResizeViewport,
-        onRotateViewport,
-        onUpdateDeviceModal,
-      })
-    );
-  }
-}
-
-module.exports = Viewport;
--- 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{2,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;
deleted file mode 100644
--- a/devtools/client/responsive.html/components/ViewportToolbar.js
+++ /dev/null
@@ -1,58 +0,0 @@
-/* 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 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 DeviceSelector = createFactory(require("./DeviceSelector"));
-
-class ViewportToolbar extends PureComponent {
-  static get propTypes() {
-    return {
-      devices: PropTypes.shape(Types.devices).isRequired,
-      viewport: PropTypes.shape(Types.viewport).isRequired,
-      onChangeDevice: PropTypes.func.isRequired,
-      onResizeViewport: PropTypes.func.isRequired,
-      onRotateViewport: PropTypes.func.isRequired,
-      onUpdateDeviceModal: PropTypes.func.isRequired,
-    };
-  }
-
-  render() {
-    const {
-      devices,
-      viewport,
-      onChangeDevice,
-      onResizeViewport,
-      onRotateViewport,
-      onUpdateDeviceModal,
-    } = this.props;
-
-    return dom.div(
-      {
-        className: "viewport-toolbar container",
-      },
-      DeviceSelector({
-        devices,
-        selectedDevice: viewport.device,
-        viewportId: viewport.id,
-        onChangeDevice,
-        onResizeViewport,
-        onUpdateDeviceModal,
-      }),
-      dom.button({
-        className: "viewport-rotate-button toolbar-button devtools-button",
-        onClick: onRotateViewport,
-        title: getStr("responsive.rotate"),
-      })
-    );
-  }
-}
-
-module.exports = ViewportToolbar;
--- a/devtools/client/responsive.html/components/Viewports.js
+++ b/devtools/client/responsive.html/components/Viewports.js
@@ -1,68 +1,92 @@
 /* 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 { connect } = require("devtools/client/shared/vendor/react-redux");
 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 Viewport = createFactory(require("./Viewport"));
 
 class Viewports extends Component {
   static get propTypes() {
     return {
-      devices: PropTypes.shape(Types.devices).isRequired,
+      leftAlignmentEnabled: PropTypes.bool.isRequired,
       screenshot: PropTypes.shape(Types.screenshot).isRequired,
       viewports: PropTypes.arrayOf(PropTypes.shape(Types.viewport)).isRequired,
       onBrowserMounted: PropTypes.func.isRequired,
-      onChangeDevice: PropTypes.func.isRequired,
       onContentResize: PropTypes.func.isRequired,
       onRemoveDeviceAssociation: PropTypes.func.isRequired,
       onResizeViewport: PropTypes.func.isRequired,
-      onRotateViewport: PropTypes.func.isRequired,
-      onUpdateDeviceModal: PropTypes.func.isRequired,
     };
   }
 
   render() {
     const {
-      devices,
+      leftAlignmentEnabled,
       screenshot,
       viewports,
       onBrowserMounted,
-      onChangeDevice,
       onContentResize,
       onRemoveDeviceAssociation,
       onResizeViewport,
-      onRotateViewport,
-      onUpdateDeviceModal,
     } = this.props;
 
-    return dom.div(
-      {
-        id: "viewports",
-      },
-      viewports.map((viewport, i) => {
-        return Viewport({
-          key: viewport.id,
-          devices,
-          screenshot,
-          swapAfterMount: i == 0,
-          viewport,
-          onBrowserMounted,
-          onChangeDevice,
-          onContentResize,
-          onRemoveDeviceAssociation,
-          onResizeViewport,
-          onRotateViewport,
-          onUpdateDeviceModal,
-        });
-      })
+    const viewportSize = window.getViewportSize();
+    // The viewport may not have been created yet. Default to justify-content: center
+    // for the container.
+    let justifyContent = "center";
+
+    // If the RDM viewport is bigger than the window's inner width, set the container's
+    // justify-content to start so that the left-most viewport is visible when there's
+    // horizontal overflow. That is when the horizontal space become smaller than the
+    // viewports and a scrollbar appears, then the first viewport will still be visible.
+    if (leftAlignmentEnabled ||
+        (viewportSize && viewportSize.width > window.innerWidth)) {
+      justifyContent = "start";
+    }
+
+    return (
+      dom.div(
+        {
+          id: "viewports-container",
+          style: {
+            justifyContent,
+          },
+        },
+        dom.div(
+          {
+            id: "viewports",
+            className: leftAlignmentEnabled ? "left-aligned" : "",
+          },
+          viewports.map((viewport, i) => {
+            return ResizableViewport({
+              key: viewport.id,
+              leftAlignmentEnabled,
+              screenshot,
+              swapAfterMount: i == 0,
+              viewport,
+              onBrowserMounted,
+              onContentResize,
+              onRemoveDeviceAssociation,
+              onResizeViewport,
+            });
+          })
+        )
+      )
     );
   }
 }
 
-module.exports = Viewports;
+const mapStateToProps = state => {
+  return {
+    leftAlignmentEnabled: state.ui.leftAlignmentEnabled,
+  };
+};
+
+module.exports = connect(mapStateToProps)(Viewports);
--- a/devtools/client/responsive.html/components/moz.build
+++ b/devtools/client/responsive.html/components/moz.build
@@ -1,21 +1,19 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # 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(
+    'App.js',
     'Browser.js',
     'DeviceAdder.js',
     'DeviceModal.js',
-    'DevicePixelRatioSelector.js',
+    'DevicePixelRatioMenu.js',
     'DeviceSelector.js',
-    'GlobalToolbar.js',
-    'ReloadConditions.js',
     'ResizableViewport.js',
-    'ToggleMenu.js',
-    'Viewport.js',
+    'SettingsMenu.js',
+    'Toolbar.js',
     'ViewportDimension.js',
     'Viewports.js',
-    'ViewportToolbar.js',
 )
--- a/devtools/client/responsive.html/constants.js
+++ b/devtools/client/responsive.html/constants.js
@@ -1,8 +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;
+exports.MIN_VIEWPORT_DIMENSION = 50;
--- a/devtools/client/responsive.html/index.css
+++ b/devtools/client/responsive.html/index.css
@@ -1,283 +1,159 @@
 /* TODO: May break up into component local CSS.  Pending future discussions by
  * React component group on how to best handle CSS. */
 
 /**
  * CSS Variables specific to the responsive design mode
  */
 
-.theme-light {
+:root {
   --rdm-box-shadow: 0 4px 4px 0 rgba(155, 155, 155, 0.26);
   --submit-button-active-background-color: rgba(0,0,0,0.12);
   --submit-button-active-color: var(--theme-body-color);
-  --viewport-color: #999797;
-  --viewport-hover-color: var(--theme-body-color);
   --viewport-active-color: #3b3b3b;
 }
 
-.theme-dark {
+:root.theme-dark {
   --rdm-box-shadow: 0 4px 4px 0 rgba(105, 105, 105, 0.26);
   --submit-button-active-background-color: var(--theme-toolbar-hover-active);
   --submit-button-active-color: var(--theme-selection-color);
-  --viewport-color: #c6ccd0;
-  --viewport-hover-color: #dde1e4;
   --viewport-active-color: #fcfcfc;
 }
 
 * {
   box-sizing: border-box;
 }
 
 :root,
 input,
-select,
 button {
-  font-size: 11px;
+  font-size: 12px;
 }
 
 html,
 body,
 #root {
   height: 100%;
-  margin: 0;
+  overflow: hidden;
+}
+
+.theme-dark body,
+.theme-dark button,
+.theme-dark input {
+  color: var(--theme-toolbar-color);
 }
 
 #app {
-  /* Center the viewports container */
   display: flex;
-  align-items: center;
   flex-direction: column;
-  padding-top: 15px;
-  padding-bottom: 1%;
-  position: relative;
+  width: 100%;
   height: 100%;
 }
 
 /**
  * Common styles for shared components
  */
 
-.container {
-  background-color: var(--theme-toolbar-background);
-  border: 1px solid var(--theme-splitter-color);
-}
-
-.toolbar-button {
-  margin: 0;
-  padding: 0;
-  border: none;
-  color: var(--viewport-color);
-}
-
-.toolbar-button:empty:hover:not(:disabled),
-.toolbar-button:empty:-moz-any(:hover:active, .checked):not(:disabled),
-.toolbar-button:not(:empty),
-.toolbar-button:hover:not(:empty):not(:disabled):not(.checked) {
-  /* Reset background from .devtools-button */
-  background: none;
-}
-
-.toolbar-button:active::before {
-  filter: var(--theme-icon-checked-filter);
-}
-
-.toolbar-button.selected {
-  color: var(--viewport-active-color);
-}
-
-.toolbar-button:not(:disabled):hover {
-  color: var(--viewport-hover-color);
-}
-
-select {
-  -moz-appearance: none;
-  color: var(--viewport-color);
-  border: none;
+.devtools-separator {
   height: 100%;
-  padding: 0 8px;
-  text-align: center;
-  text-overflow: ellipsis;
-}
-
-select.selected {
-  color: var(--viewport-active-color);
-}
-
-select:not(:disabled):hover {
-  color: var(--viewport-hover-color);
-}
-
-/* This is (believed to be?) separate from the identical select.selected rule
-   set so that it overrides select:hover because of file ordering once the
-   select is focused.  It's unclear whether the visual effect that results here
-   is intentional and desired. */
-select:focus {
-  color: var(--viewport-active-color);
-}
-
-select > option {
-  text-align: left;
-  padding: 5px 10px;
-}
-
-select > option,
-select > option:hover {
-  color: var(--viewport-active-color);
-}
-
-select > option.divider {
-  border-top: 1px solid var(--theme-splitter-color);
-  height: 0px;
-  padding: 0;
-  font-size: 0px;
+  margin: 0 1px;
 }
 
 /**
- * Toggle Menu
+ * Toolbar
  */
 
-.devtools-toggle-menu {
-  position: relative;
+#toolbar {
+  background-color: var(--theme-tab-toolbar-background);
+  border-bottom: 1px solid var(--theme-splitter-color);
+  display: grid;
+  grid-template-columns: min-content auto min-content;
+  width: 100%;
+  min-height: 29px;
+  -moz-user-select: none;
 }
 
-.devtools-toggle-menu .devtools-menu {
-  display: none;
-  flex-direction: column;
-  align-items: start;
-  position: absolute;
-  left: 0;
-  top: 100%;
-  z-index: 1;
-  padding: 5px;
-  border-radius: 2px;
-  background-color: var(--theme-toolbar-background);
-  box-shadow: var(--rdm-box-shadow);
-}
-
-.devtools-toggle-menu .devtools-menu.opened {
-  display: flex;
-}
-
-.devtools-toggle-menu .devtools-menu-item {
+#toolbar-center-controls,
+#toolbar-end-controls {
   display: flex;
   align-items: center;
 }
 
-/**
- * Common background for dropdowns like select and toggle menu
- */
-.toolbar-dropdown,
-.toolbar-dropdown.devtools-button,
-.toolbar-dropdown.devtools-button:hover:not(:empty):not(:disabled):not(.checked) {
-  background-color: var(--theme-toolbar-background);
-  background-image: var(--select-arrow-image);
-  background-position: 100% 50%;
-  background-repeat: no-repeat;
-  background-size: 7px;
-  -moz-context-properties: fill;
-  fill: currentColor;
+#toolbar-center-controls {
+  justify-self: center;
 }
 
-/**
- * Global Toolbar
- */
-
-#global-toolbar {
-  color: var(--theme-body-color-alt);
-  border-radius: 2px;
-  box-shadow: var(--rdm-box-shadow);
-  margin: 0 0 15px 0;
-  padding: 4px 5px;
-  display: inline-flex;
-  align-items: center;
-  -moz-user-select: none;
+#rotate-button::before {
+  background-image: url("./images/rotate-viewport.svg");
 }
 
-#global-toolbar > .title {
-  border-right: 1px solid var(--theme-splitter-color);
-  padding: 1px 6px 0 2px;
-}
-
-#global-toolbar > .toolbar-button:first-of-type {
-  margin-inline-start: 8px;
-}
-
-#global-toolbar > .toolbar-button::before {
-  width: 12px;
-  height: 12px;
-  background-size: cover;
-}
-
-#global-toolbar .toolbar-dropdown {
-  background-position-x: right 5px;
-  border-right: 1px solid var(--theme-splitter-color);
-  padding-right: 15px;
-  /* padding-left: 0; */
-}
-
-#global-touch-simulation-button::before {
+#touch-simulation-button::before {
   background-image: url("./images/touch-events.svg");
 }
 
-#global-screenshot-button::before {
+#screenshot-button::before {
   background-image: url("./images/screenshot.svg");
 }
 
-#global-exit-button::before {
+#settings-button::before {
+  background-image: url("chrome://devtools/skin/images/settings.svg");
+}
+
+#exit-button::before {
   background-image: url("chrome://devtools/skin/images/close.svg");
 }
 
-#global-screenshot-button:disabled {
+#screenshot-button:disabled {
   filter: var(--theme-icon-checked-filter);
   opacity: 1 !important;
 }
 
-#global-network-throttling-selector {
-  height: 15px;
+#device-selector {
+  align-self: center;
+  background-position: right 4px center;
+  margin-inline-start: 4px;
   padding-left: 0;
-  width: 103px;
+  width: 8em;
 }
 
-#global-device-pixel-ratio-selector {
-  -moz-user-select: none;
-  color: var(--viewport-color);
-  height: 15px;
+#device-selector .title {
+  width: 85%;
+}
+
+#device-pixel-ratio-menu {
+  width: 6em;
   /* `max-width` is here to keep the UI compact if the device pixel ratio changes to a
      repeating decimal value.  This can happen if you zoom the UI (Cmd + Plus / Minus on
      macOS for example). */
   max-width: 8em;
-}
-
-#global-device-pixel-ratio-selector.focused,
-#global-device-pixel-ratio-selector:not(.disabled):hover {
-  color: var(--viewport-hover-color);
+  background-position: right 4px center;
+  padding-left: 0;
 }
 
-#global-device-pixel-ratio-selector:focus {
-  color: var(--viewport-active-color);
+#viewports-container {
+  display: flex;
+  overflow: auto;
+  height: 100%;
+  width: 100%;
 }
 
-#global-device-pixel-ratio-selector.selected {
-  color: var(--viewport-active-color);
-}
-
-#global-device-pixel-ratio-selector > option {
-  padding: 5px;
+.theme-light #viewports-container {
+  background-color: #F5F5F6;
 }
 
 #viewports {
-  /* Make sure left-most viewport is visible when there's horizontal overflow.
-     That is, when the horizontal space become smaller than the viewports and a
-     scrollbar appears, then the first viewport will still be visible */
-  position: sticky;
-  left: 0;
   /* Individual viewports are inline elements, make sure they stay on a single
      line */
   white-space: nowrap;
+  margin-top: 16px;
+}
+
+#viewports.left-aligned {
+  margin-left: 16px;
 }
 
 /**
  * Viewport Container
  */
 
 .viewport {
   display: inline-block;
@@ -287,38 +163,16 @@ select > option.divider {
 
 .resizable-viewport {
   border: 1px solid var(--theme-splitter-color);
   box-shadow: var(--rdm-box-shadow);
   position: relative;
 }
 
 /**
- * Viewport Toolbar
- */
-
-.viewport-toolbar {
-  border-width: 0;
-  border-bottom-width: 1px;
-  display: flex;
-  flex-direction: row;
-  justify-content: center;
-  height: 16px;
-}
-
-.viewport-rotate-button {
-  position: absolute;
-  right: 0;
-}
-
-.viewport-rotate-button::before {
-  background-image: url("./images/rotate-viewport.svg");
-}
-
-/**
  * Viewport Content
  */
 
 .viewport-content.resizing {
   pointer-events: none;
 }
 
 /**
@@ -371,58 +225,48 @@ 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;
+}
+
+.theme-dark .viewport-dimension-input {
+  background-color: var(--theme-tab-toolbar-background);
 }
 
 .viewport-dimension-separator {
   -moz-user-select: none;
+  padding: 0 0.3em;
 }
 
 /**
  * Device Modal
  */
 
 @keyframes fade-in-and-up {
   0% {
@@ -443,16 +287,18 @@ select > option.divider {
   100% {
     opacity: 0;
     transform: translateY(5px);
     visibility: hidden;
   }
 }
 
 .device-modal {
+  background-color: var(--theme-toolbar-background);
+  border: 1px solid var(--theme-splitter-color);
   border-radius: 2px;
   box-shadow: var(--rdm-box-shadow);
   position: absolute;
   margin: auto;
   top: 0;
   bottom: 0;
   left: 0;
   right: 0;
@@ -486,67 +332,57 @@ select > option.divider {
   display: flex;
   flex-direction: column;
   flex-wrap: wrap;
   overflow: auto;
   height: 515px;
   margin: 20px 20px 0;
 }
 
-#device-close-button,
-#device-close-button::before {
+#device-close-button {
   position: absolute;
   top: 5px;
   right: 2px;
-  width: 12px;
-  height: 12px;
 }
 
 #device-close-button::before {
   background-image: url("chrome://devtools/skin/images/close.svg");
-  margin: -6px 0 0 -6px;
 }
 
 .device-type {
   display: flex;
   flex-direction: column;
   padding: 10px;
 }
 
 .device-header {
   font-weight: bold;
   text-transform: capitalize;
   padding: 0 0 3px 23px;
 }
 
 .device-label {
+  color: var(--theme-body-color);
   padding-bottom: 3px;
   display: flex;
   align-items: center;
   /* Largest size without horizontal scrollbars */
   max-width: 181px;
 }
 
 .device-input-checkbox {
   margin-right: 5px;
 }
 
 .device-name {
   flex: 1;
 }
 
-.device-remove-button,
-.device-remove-button:empty::before {
-  width: 12px;
-  height: 12px;
-}
-
 .device-remove-button:empty::before {
   background-image: url("chrome://devtools/skin/images/close.svg");
-  margin: -6px 0 0 -6px;
 }
 
 #device-submit-button {
   background-color: var(--theme-tab-toolbar-background);
   border-width: 1px 0 0 0;
   border-top-width: 1px;
   border-top-style: solid;
   border-top-color: var(--theme-splitter-color);
@@ -604,16 +440,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;
 }
 
--- a/devtools/client/responsive.html/index.js
+++ b/devtools/client/responsive.html/index.js
@@ -8,25 +8,24 @@
 
 const { BrowserLoader } =
   ChromeUtils.import("resource://devtools/client/shared/browser-loader.js", {});
 const { require } = BrowserLoader({
   baseURI: "resource://devtools/client/responsive.html/",
   window
 });
 const Telemetry = require("devtools/client/shared/telemetry");
-const { loadAgentSheet } = require("./utils/css");
 
 const { createFactory, createElement } =
   require("devtools/client/shared/vendor/react");
 const ReactDOM = require("devtools/client/shared/vendor/react-dom");
 const { Provider } = require("devtools/client/shared/vendor/react-redux");
 
 const message = require("./utils/message");
-const App = createFactory(require("./app"));
+const App = createFactory(require("./components/App"));
 const Store = require("./store");
 const { loadDevices } = require("./actions/devices");
 const { changeDisplayPixelRatio } = require("./actions/display-pixel-ratio");
 const { changeLocation } = require("./actions/location");
 const { loadReloadConditions } = require("./actions/reload-conditions");
 const { addViewport, resizeViewport } = require("./actions/viewports");
 
 // Exposed for use by tests
@@ -34,23 +33,16 @@ window.require = require;
 
 const bootstrap = {
 
   telemetry: new Telemetry(),
 
   store: null,
 
   async init() {
-    // Load a special UA stylesheet to reset certain styles such as dropdown
-    // lists.
-    loadAgentSheet(
-      window,
-      "resource://devtools/client/responsive.html/responsive-ua.css"
-    );
-
     this.telemetry.toolOpened("responsive");
 
     const store = this.store = Store();
     const provider = createElement(Provider, { store }, App());
     ReactDOM.render(provider, document.querySelector("#root"));
     message.post(window, "init:done");
   },
 
@@ -132,17 +124,22 @@ window.addInitialViewport = ({ uri, user
     console.error(e);
   }
 };
 
 /**
  * Called by manager.js when tests want to check the viewport size.
  */
 window.getViewportSize = () => {
-  const { width, height } = bootstrap.store.getState().viewports[0];
+  const { viewports } = bootstrap.store.getState();
+  if (!viewports.length) {
+    return null;
+  }
+
+  const { width, height } = viewports[0];
   return { width, height };
 };
 
 /**
  * Called by manager.js to set viewport size from tests, GCLI, etc.
  */
 window.setViewportSize = ({ width, height }) => {
   try {
--- a/devtools/client/responsive.html/index.xhtml
+++ b/devtools/client/responsive.html/index.xhtml
@@ -8,12 +8,12 @@
     <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
     <link rel="stylesheet" type="text/css"
           href="resource://devtools/client/responsive.html/index.css"/>
     <script type="application/javascript"
             src="chrome://devtools/content/shared/theme-switching.js"></script>
     <script type="application/javascript"
             src="resource://devtools/client/responsive.html/index.js"></script>
   </head>
-  <body class="theme-body" role="application">
+  <body class="theme-toolbar" role="application">
     <div id="root"/>
   </body>
 </html>
--- a/devtools/client/responsive.html/manager.js
+++ b/devtools/client/responsive.html/manager.js
@@ -9,29 +9,31 @@ const promise = require("promise");
 const Services = require("Services");
 const EventEmitter = require("devtools/shared/event-emitter");
 
 const TOOL_URL = "chrome://devtools/content/responsive.html/index.xhtml";
 
 loader.lazyRequireGetter(this, "DebuggerClient", "devtools/shared/client/debugger-client", true);
 loader.lazyRequireGetter(this, "DebuggerServer", "devtools/server/main", true);
 loader.lazyRequireGetter(this, "throttlingProfiles", "devtools/client/shared/components/throttling/profiles");
+loader.lazyRequireGetter(this, "SettingOnboardingTooltip", "devtools/client/responsive.html/setting-onboarding-tooltip");
 loader.lazyRequireGetter(this, "swapToInnerBrowser", "devtools/client/responsive.html/browser/swap", true);
 loader.lazyRequireGetter(this, "startup", "devtools/client/responsive.html/utils/window", true);
 loader.lazyRequireGetter(this, "message", "devtools/client/responsive.html/utils/message");
 loader.lazyRequireGetter(this, "showNotification", "devtools/client/responsive.html/utils/notification", true);
 loader.lazyRequireGetter(this, "l10n", "devtools/client/responsive.html/utils/l10n");
 loader.lazyRequireGetter(this, "EmulationFront", "devtools/shared/fronts/emulation", true);
 loader.lazyRequireGetter(this, "PriorityLevels", "devtools/client/shared/components/NotificationBox", true);
 loader.lazyRequireGetter(this, "TargetFactory", "devtools/client/framework/target", true);
 loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true);
 loader.lazyRequireGetter(this, "Telemetry", "devtools/client/shared/telemetry");
 
 const RELOAD_CONDITION_PREF_PREFIX = "devtools.responsive.reloadConditions.";
 const RELOAD_NOTIFICATION_PREF = "devtools.responsive.reloadNotification.enabled";
+const SHOW_SETTING_TOOLTIP_PREF = "devtools.responsive.show-setting-tooltip";
 
 function debug(msg) {
   // console.log(`RDM manager: ${msg}`);
 }
 
 /**
  * ResponsiveUIManager is the external API for the browser UI, etc. to use when
  * opening and closing the responsive UI.
@@ -372,16 +374,22 @@ ResponsiveUI.prototype = {
     // Notify the inner browser to start the frame script
     debug("Wait until start frame script");
     await message.request(this.toolWindow, "start-frame-script");
 
     // Get the protocol ready to speak with emulation actor
     debug("Wait until RDP server connect");
     await this.connectToServer();
 
+    // Show the settings onboarding tooltip
+    if (Services.prefs.getBoolPref(SHOW_SETTING_TOOLTIP_PREF)) {
+      this.settingOnboardingTooltip =
+        new SettingOnboardingTooltip(ui.toolWindow.document);
+    }
+
     // Non-blocking message to tool UI to start any delayed init activities
     message.post(this.toolWindow, "post-init");
 
     debug("Init done");
   },
 
   /**
    * Close RDM and restore page content back into a regular tab.
@@ -431,16 +439,21 @@ ResponsiveUI.prototype = {
                       this.reloadOnChange("userAgent");
       reloadNeeded |= await this.updateTouchSimulation() &&
                       this.reloadOnChange("touchSimulation");
       if (reloadNeeded) {
         this.getViewportBrowser().reload();
       }
     }
 
+    if (this.settingOnboardingTooltip) {
+      this.settingOnboardingTooltip.destroy();
+      this.settingOnboardingTooltip = null;
+    }
+
     // Destroy local state
     const swap = this.swap;
     this.browserWindow = null;
     this.tab = null;
     this.inited = null;
     this.toolWindow = null;
     this.swap = null;
 
--- a/devtools/client/responsive.html/moz.build
+++ b/devtools/client/responsive.html/moz.build
@@ -9,24 +9,23 @@ DIRS += [
     'browser',
     'components',
     'images',
     'reducers',
     'utils',
 ]
 
 DevToolsModules(
-    'app.js',
     'commands.js',
     'constants.js',
     'index.css',
     'index.js',
     'manager.js',
     'reducers.js',
-    'responsive-ua.css',
+    'setting-onboarding-tooltip.js',
     'store.js',
     'types.js',
 )
 
 XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
 BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
 
 with Files('**'):
--- a/devtools/client/responsive.html/reducers.js
+++ b/devtools/client/responsive.html/reducers.js
@@ -6,9 +6,10 @@
 
 exports.devices = require("./reducers/devices");
 exports.displayPixelRatio = require("./reducers/display-pixel-ratio");
 exports.location = require("./reducers/location");
 exports.networkThrottling = require("devtools/client/shared/components/throttling/reducer");
 exports.reloadConditions = require("./reducers/reload-conditions");
 exports.screenshot = require("./reducers/screenshot");
 exports.touchSimulation = require("./reducers/touch-simulation");
+exports.ui = require("./reducers/ui");
 exports.viewports = require("./reducers/viewports");
--- a/devtools/client/responsive.html/reducers/moz.build
+++ b/devtools/client/responsive.html/reducers/moz.build
@@ -6,10 +6,11 @@
 
 DevToolsModules(
     'devices.js',
     'display-pixel-ratio.js',
     'location.js',
     'reload-conditions.js',
     'screenshot.js',
     'touch-simulation.js',
+    'ui.js',
     'viewports.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/reducers/ui.js
@@ -0,0 +1,41 @@
+/* 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 Services = require("Services");
+
+const {
+  TOGGLE_LEFT_ALIGNMENT,
+} = require("../actions/index");
+
+const LEFT_ALIGNMENT_ENABLED = "devtools.responsive.leftAlignViewport.enabled";
+
+const INITIAL_UI = {
+  // Whether or not the viewports are left aligned.
+  leftAlignmentEnabled: Services.prefs.getBoolPref(LEFT_ALIGNMENT_ENABLED),
+};
+
+const reducers = {
+
+  [TOGGLE_LEFT_ALIGNMENT](ui, { enabled }) {
+    const leftAlignmentEnabled = enabled !== undefined ?
+      enabled : !ui.leftAlignmentEnabled;
+
+    Services.prefs.setBoolPref(LEFT_ALIGNMENT_ENABLED, leftAlignmentEnabled);
+
+    return Object.assign({}, ui, {
+      leftAlignmentEnabled,
+    });
+  },
+
+};
+
+module.exports = function(ui = INITIAL_UI, action) {
+  const reducer = reducers[action.type];
+  if (!reducer) {
+    return ui;
+  }
+  return reducer(ui, action);
+};
deleted file mode 100644
--- a/devtools/client/responsive.html/responsive-ua.css
+++ /dev/null
@@ -1,6 +0,0 @@
-@namespace url(http://www.w3.org/1999/xhtml);
-
-/* Reset default UA styles for dropdown options */
-*|*::-moz-dropdown-list {
-  border: 0 !important;
-}
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/setting-onboarding-tooltip.js
@@ -0,0 +1,71 @@
+/* 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 Services = require("Services");
+const { HTMLTooltip } = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+
+const { getStr } = require("./utils/l10n");
+
+const SHOW_SETTING_TOOLTIP_PREF = "devtools.responsive.show-setting-tooltip";
+
+const CONTAINER_WIDTH = 270;
+
+/**
+ * Setting onboarding tooltip that is shown on the setting menu button in the RDM toolbar
+ * when the pref is on.
+ */
+class SettingOnboardingTooltip {
+  constructor(doc) {
+    this.doc = doc;
+    this.tooltip = new HTMLTooltip(this.doc, { type: "arrow" });
+
+    this.onCloseButtonClick = this.onCloseButtonClick.bind(this);
+
+    const container = doc.createElement("div");
+    container.className = "onboarding-container";
+
+    const icon = doc.createElement("span");
+    icon.className = "onboarding-icon";
+    container.appendChild(icon);
+
+    const content = doc.createElement("div");
+    content.className = "onboarding-content";
+    content.textContent = getStr("responsive.settingOnboarding.content");
+    container.appendChild(content);
+
+    this.closeButton = doc.createElement("button");
+    this.closeButton.className = "onboarding-close-button devtools-button";
+    container.appendChild(this.closeButton);
+
+    this.closeButton.addEventListener("click", this.onCloseButtonClick);
+
+    this.tooltip.setContent(container, { width: CONTAINER_WIDTH });
+    this.tooltip.show(this.doc.getElementById("settings-button"), {
+      position: "bottom",
+    });
+  }
+
+  destroy() {
+    this.closeButton.removeEventListener("click", this.onCloseButtonClick);
+
+    this.tooltip.destroy();
+
+    this.closeButton = null;
+    this.doc = null;
+    this.tooltip = null;
+  }
+
+  /**
+   * Handler for the "click" event on the close button. Hides the onboarding tooltip
+   * and sets the show three pane onboarding tooltip pref to false.
+   */
+  onCloseButtonClick() {
+    Services.prefs.setBoolPref(SHOW_SETTING_TOOLTIP_PREF, false);
+    this.tooltip.hide();
+  }
+}
+
+module.exports = SettingOnboardingTooltip;
--- a/devtools/client/responsive.html/test/browser/browser_device_change.js
+++ b/devtools/client/responsive.html/test/browser/browser_device_change.js
@@ -38,17 +38,17 @@ addRDMTask(TEST_URL, async function({ ui
     && state.devices.listState == Types.loadableState.LOADED);
 
   // Test defaults
   testViewportDimensions(ui, 320, 480);
   info("Should have default UA at the start of the test");
   await testUserAgent(ui, DEFAULT_UA);
   await testDevicePixelRatio(ui, DEFAULT_DPPX);
   await testTouchEventsOverride(ui, false);
-  testViewportDeviceSelectLabel(ui, "no device selected");
+  testViewportDeviceMenuLabel(ui, "Responsive");
 
   // Test device with custom properties
   let reloaded = waitForViewportLoad(ui);
   await selectDevice(ui, "Fake Phone RDM Test");
   await reloaded;
   await waitForViewportResizeTo(ui, testDevice.width, testDevice.height);
   info("Should have device UA now that device is applied");
   await testUserAgent(ui, testDevice.userAgent);
@@ -60,17 +60,17 @@ addRDMTask(TEST_URL, async function({ ui
   reloaded = waitForViewportLoad(ui);
   await testViewportResize(ui, ".viewport-vertical-resize-handle",
     [-10, -10], [testDevice.width, testDevice.height - 10], [0, -10], ui);
   await Promise.all([ deviceRemoved, reloaded ]);
   info("Should have default UA after resizing viewport");
   await testUserAgent(ui, DEFAULT_UA);
   await testDevicePixelRatio(ui, DEFAULT_DPPX);
   await testTouchEventsOverride(ui, false);
-  testViewportDeviceSelectLabel(ui, "no device selected");
+  testViewportDeviceMenuLabel(ui, "Responsive");
 
   // Test device with generic properties
   await selectDevice(ui, "Laptop (1366 x 768)");
   await waitForViewportResizeTo(ui, 1366, 768);
   info("Should have default UA when using device without specific UA");
   await testUserAgent(ui, DEFAULT_UA);
   await testDevicePixelRatio(ui, 1);
   await testTouchEventsOverride(ui, false);
--- a/devtools/client/responsive.html/test/browser/browser_device_custom.js
+++ b/devtools/client/responsive.html/test/browser/browser_device_custom.js
@@ -29,23 +29,20 @@ const Types = require("devtools/client/r
 addRDMTask(TEST_URL, async function({ ui }) {
   const { toolWindow } = ui;
   const { store, document } = toolWindow;
 
   // Wait until the viewport has been added and the device list has been loaded
   await waitUntilState(store, state => state.