Bug 1506547 - Align user-select behavior more with other UAs. r=mats
authorEmilio Cobos Álvarez <emilio@crisal.io>
Mon, 26 Nov 2018 09:21:37 +0000
changeset 504393 8e88421b280c2afda62f4ba704ce29701c30549f
parent 504392 f17b7ba6d0aa737d4f69a0fc3206da8c539225e5
child 504394 367cea0a5b9f4b31c5ff497f36cfba484c52ebbd
push id10290
push userffxbld-merge
push dateMon, 03 Dec 2018 16:23:23 +0000
treeherdermozilla-beta@700bed2445e6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmats
bugs1506547, 1181130, 1109968, 1132768
milestone65.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1506547 - Align user-select behavior more with other UAs. r=mats There's a few subtle behavior changes here, which I'll try to break down in the commit message. The biggest one is the EditableDescendantCount stuff going away. This was added in bug 1181130, to prevent clicking on the non-editable div from selecting the editable div inside. This is problematic for multiple reasons: * First, I don't think non-editable regions of an editable element should be user-select: all. * Second, it just doesn't work in Shadow DOM (the editable descendant count is not kept up-to-date when not in the uncomposed doc), so nested contenteditables behave differently inside vs. outside a Shadow Tree. * Third, I think it's user hostile to just entirely disable selection if you have a contenteditable descendant as a child of a user-select: all thing. WebKit behaves like this patch in the following test-case (though not Blink): https://crisal.io/tmp/user-select-all-contenteditable-descendant.html Edge doesn't seem to support user-select: all at all (no pun intended). But we don't allow to select anything at all which looks wrong. * Fourth, it's not tested at all (which explains how we broke it in Shadow DOM and not even notice...). In any case I've verified that this doesn't regress the editor from that bug. If this regresses anything we can fix it as outlined in the first bullet point above, which should also make us more compatible with other UAs in that test-case. The other change is `all` not overriding everything else. So, something like: <div style="-webkit-user-select: all">All <div style="-webkit-user-select: none">None</div></div> Totally ignores the -webkit-user-select: none declaration in Firefox before this change. This doesn't match any other UA nor the spec, and this patch aligns us with WebKit / Blink. This in turn makes us not need -moz-text anymore, whose only purpose was to avoid this. This also fixes a variety of bugs uncovered by the previous changes, like the SetIgnoreUserModify(false) call in editor being completely useless, since presShell->SetCaretEnabled ended in nsCaret::SetVisible, which overrode it. This in turn uncovered even more bugs, from bugs in the caret painting code, like not checking -moz-user-modify on the right frame if you're the last frame of a line, to even funnier bits where before this patch you show the caret but can't write at all... In any case, the new setup I came up with is that when you're editing (the selection is focused on an editable node) moving the caret forces it to end up in an editable node, thus jumping over non-editable ones. This has the nice effect of not completely disabling selection of -moz-user-select: all elements that have editable descendants (which was a very ad-hoc hack for bug 1181130, and somewhat broken per the above), and also not needing the -moz-user-select: all for non-editable bits in contenteditable.css at all. This also fixes issues with br-skipping like not being able to insert content in the following test-case: <div contenteditable="true"><span contenteditable="false">xyz </span><br>editable</div> If you start moving to the left from the second line, for example. I think this yields way better behavior in all the relevant test-cases from bug 1181130 / bug 1109968 / bug 1132768, shouldn't cause any regression, and the complexity is significantly reduced in some places. There's still some other broken bits that this patch doesn't fix, but I'll file follow-ups for those. Differential Revision: https://phabricator.services.mozilla.com/D12687
accessible/generic/HyperTextAccessible.cpp
browser/base/content/test/static/browser_parsable_css.js
devtools/shared/css/generated/properties-db.js
dom/base/Element.cpp
dom/base/Selection.cpp
dom/base/Selection.h
dom/base/nsINode.cpp
dom/base/nsINode.h
dom/base/test/test_bug166235.html
dom/html/nsGenericHTMLElement.cpp
editor/libeditor/EditorBase.cpp
editor/libeditor/tests/test_bug430392.html
layout/base/nsCaret.cpp
layout/base/nsCaret.h
layout/base/nsLayoutUtils.cpp
layout/base/nsLayoutUtils.h
layout/base/tests/bug1109968-1.html
layout/base/tests/bug1109968-2.html
layout/base/tests/bug1132768-1.html
layout/base/tests/bug1506547-1.html
layout/base/tests/bug1506547-2.html
layout/base/tests/bug1506547-3.html
layout/base/tests/bug1506547-4-ref.html
layout/base/tests/bug1506547-4.html
layout/base/tests/bug1506547-5-ref.html
layout/base/tests/bug1506547-5.html
layout/base/tests/bug1506547-6.html
layout/base/tests/mochitest.ini
layout/base/tests/test_reftests_with_caret.html
layout/generic/nsFrame.cpp
layout/generic/nsFrameSelection.cpp
layout/generic/nsFrameSelection.h
layout/generic/nsIFrame.h
layout/style/contenteditable.css
servo/components/style/properties/longhands/ui.mako.rs
servo/components/style/values/specified/ui.rs
--- a/accessible/generic/HyperTextAccessible.cpp
+++ b/accessible/generic/HyperTextAccessible.cpp
@@ -542,17 +542,18 @@ HyperTextAccessible::FindOffset(uint32_t
 
   const bool kIsJumpLinesOk = true; // okay to jump lines
   const bool kIsScrollViewAStop = false; // do not stop at scroll views
   const bool kIsKeyboardSelect = true; // is keyboard selection
   const bool kIsVisualBidi = false; // use visual order for bidi text
   nsPeekOffsetStruct pos(aAmount, aDirection, innerContentOffset,
                          nsPoint(0, 0), kIsJumpLinesOk, kIsScrollViewAStop,
                          kIsKeyboardSelect, kIsVisualBidi,
-                         false, aWordMovementType);
+                         false, nsPeekOffsetStruct::ForceEditableRegion::No,
+                         aWordMovementType);
   nsresult rv = frameAtOffset->PeekOffset(&pos);
 
   // PeekOffset fails on last/first lines of the text in certain cases.
   if (NS_FAILED(rv) && aAmount == eSelectLine) {
     pos.mAmount = (aDirection == eDirNext) ? eSelectEndLine : eSelectBeginLine;
     frameAtOffset->PeekOffset(&pos);
   }
   if (!pos.mResultContent) {
--- a/browser/base/content/test/static/browser_parsable_css.js
+++ b/browser/base/content/test/static/browser_parsable_css.js
@@ -43,20 +43,16 @@ let whitelist = [
    errorMessage: /Unknown property.*overflow-clip-box/i,
    isFromDevTools: false},
   // The '-moz-menulist-button' value is only supported in chrome and UA sheets
   // but forms.css is loaded as a document sheet by this test.
   // Maybe bug 1261237 will fix this?
   {sourceName: /(?:res|gre-resources)\/forms\.css$/i,
    errorMessage: /Error in parsing value for \u2018-moz-appearance\u2019/iu,
    isFromDevTools: false},
-  // -moz-user-select: -moz-text is only enabled to user-agent stylesheets.
-  {sourceName: /contenteditable.css$/i,
-   errorMessage: /Error in parsing value for \u2018-moz-user-select\u2019/iu,
-   isFromDevTools: false},
   // These variables are declared somewhere else, and error when we load the
   // files directly. They're all marked intermittent because their appearance
   // in the error console seems to not be consistent.
   {sourceName: /jsonview\/css\/general\.css$/i,
    intermittent: true,
    errorMessage: /Property contained reference to invalid variable.*color/i,
    isFromDevTools: true},
   {sourceName: /webide\/skin\/logs\.css$/i,
--- a/devtools/shared/css/generated/properties-db.js
+++ b/devtools/shared/css/generated/properties-db.js
@@ -1353,17 +1353,16 @@ exports.CSS_PROPERTIES = {
   "-moz-user-select": {
     "isInherited": false,
     "subproperties": [
       "-moz-user-select"
     ],
     "supports": [],
     "values": [
       "-moz-none",
-      "-moz-text",
       "all",
       "auto",
       "inherit",
       "initial",
       "none",
       "text",
       "unset"
     ]
@@ -2745,17 +2744,16 @@ exports.CSS_PROPERTIES = {
   "-webkit-user-select": {
     "isInherited": false,
     "subproperties": [
       "-moz-user-select"
     ],
     "supports": [],
     "values": [
       "-moz-none",
-      "-moz-text",
       "all",
       "auto",
       "inherit",
       "initial",
       "none",
       "text",
       "unset"
     ]
--- a/dom/base/Element.cpp
+++ b/dom/base/Element.cpp
@@ -1634,31 +1634,16 @@ Element::GetElementsWithGrid(nsTArray<Re
     }
 
     // Either this isn't an element, or it has no frame. Continue with the
     // traversal but ignore all the children.
     cur = cur->GetNextNonChildNode(this);
   }
 }
 
-/**
- * Returns the count of descendants (inclusive of aContent) in
- * the uncomposed document that are explicitly set as editable.
- */
-static uint32_t
-EditableInclusiveDescendantCount(nsIContent* aContent)
-{
-  auto htmlElem = nsGenericHTMLElement::FromNode(aContent);
-  if (htmlElem) {
-    return htmlElem->EditableInclusiveDescendantCount();
-  }
-
-  return aContent->EditableDescendantCount();
-}
-
 nsresult
 Element::BindToTree(nsIDocument* aDocument, nsIContent* aParent,
                     nsIContent* aBindingParent)
 {
   MOZ_ASSERT(aParent || aDocument, "Must have document if no parent!");
   MOZ_ASSERT((NODE_FROM(aParent, aDocument)->OwnerDoc() == OwnerDoc()),
              "Must have the same owner document");
   MOZ_ASSERT(!aParent || aDocument == aParent->GetUncomposedDoc(),
@@ -1797,18 +1782,16 @@ Element::BindToTree(nsIDocument* aDocume
 
   // This has to be here, rather than in nsGenericHTMLElement::BindToTree,
   //  because it has to happen after updating the parent pointer, but before
   //  recursively binding the kids.
   if (IsHTMLElement()) {
     SetDirOnBind(this, aParent);
   }
 
-  uint32_t editableDescendantCount = 0;
-
   UpdateEditableState(false);
 
   // If we had a pre-existing XBL binding, we might have anonymous children that
   // also need to be told that they are moving.
   if (HasFlag(NODE_MAY_BE_IN_BINDING_MNGR)) {
     nsXBLBinding* binding =
       OwnerDoc()->BindingManager()->GetBindingWithContent(this);
 
@@ -1821,40 +1804,16 @@ Element::BindToTree(nsIDocument* aDocume
   }
 
   // Now recurse into our kids
   nsresult rv;
   for (nsIContent* child = GetFirstChild(); child;
        child = child->GetNextSibling()) {
     rv = child->BindToTree(aDocument, this, aBindingParent);
     NS_ENSURE_SUCCESS(rv, rv);
-
-    editableDescendantCount += EditableInclusiveDescendantCount(child);
-  }
-
-  if (aDocument) {
-    // Update our editable descendant count because we don't keep track of it
-    // for content that is not in the uncomposed document.
-    MOZ_ASSERT(EditableDescendantCount() == 0);
-    ChangeEditableDescendantCount(editableDescendantCount);
-
-    if (!hadParent) {
-      uint32_t editableDescendantChange = EditableInclusiveDescendantCount(this);
-      if (editableDescendantChange != 0) {
-        // If we are binding a subtree root to the document, we need to update
-        // the editable descendant count of all the ancestors.
-        // But we don't cross Shadow DOM boundary.
-        // (The expected behavior with Shadow DOM is unclear)
-        nsIContent* parent = GetParent();
-        while (parent && parent->IsElement()) {
-          parent->ChangeEditableDescendantCount(editableDescendantChange);
-          parent = parent->GetParent();
-        }
-      }
-    }
   }
 
   nsNodeUtils::ParentChainChanged(this);
   if (!hadParent && IsRootOfNativeAnonymousSubtree()) {
     nsNodeUtils::NativeAnonymousChildListChange(this, false);
   }
 
   // Ensure we only add to the table once, in the case we move the ShadowRoot
@@ -1986,29 +1945,16 @@ Element::UnbindFromTree(bool aDeep, bool
     MOZ_ASSERT(IsInAnonymousSubtree());
   }
 
   if (document) {
     ClearServoData(document);
   }
 
   if (aNullParent) {
-    if (GetParent() && GetParent()->IsInUncomposedDoc()) {
-      // Update the editable descendant count in the ancestors before we
-      // lose the reference to the parent.
-      int32_t editableDescendantChange = -1 * EditableInclusiveDescendantCount(this);
-      if (editableDescendantChange != 0) {
-        nsIContent* parent = GetParent();
-        while (parent) {
-          parent->ChangeEditableDescendantCount(editableDescendantChange);
-          parent = parent->GetParent();
-        }
-      }
-    }
-
     if (IsRootOfNativeAnonymousSubtree()) {
       nsNodeUtils::NativeAnonymousChildListChange(this, true);
     }
 
     if (GetParent()) {
       RefPtr<nsINode> p;
       p.swap(mParent);
     } else {
@@ -2053,20 +1999,16 @@ Element::UnbindFromTree(bool aDeep, bool
         // We have to clear all pending restyle requests for the animations on
         // this element to avoid unnecessary restyles when we re-attached this
         // element.
         presContext->EffectCompositor()->ClearRestyleRequestsFor(this);
       }
     }
   }
 
-  // Editable descendant count only counts descendants that
-  // are in the uncomposed document.
-  ResetEditableDescendantCount();
-
   if (aNullParent || !mParent->IsInShadowTree()) {
     UnsetFlags(NODE_IS_IN_SHADOW_TREE);
 
     // Begin keeping track of our subtree root.
     SetSubtreeRootPointer(aNullParent ? this : mParent->SubtreeRoot());
   }
 
   bool clearBindingParent = true;
--- a/dom/base/Selection.cpp
+++ b/dom/base/Selection.cpp
@@ -483,16 +483,32 @@ Selection::GetInterlinePosition(ErrorRes
 {
   if (!mFrameSelection) {
     aRv.Throw(NS_ERROR_NOT_INITIALIZED); // Can't do selection
     return false;
   }
   return mFrameSelection->GetHint() == CARET_ASSOCIATE_AFTER;
 }
 
+bool
+Selection::IsEditorSelection() const
+{
+  nsINode* focusNode = GetFocusNode();
+  if (!focusNode) {
+    return false;
+  }
+
+  if (focusNode->IsEditable()) {
+    return true;
+  }
+
+  auto* element = Element::FromNode(focusNode);
+  return element && element->State().HasState(NS_EVENT_STATE_MOZ_READWRITE);
+}
+
 Nullable<int16_t>
 Selection::GetCaretBidiLevel(mozilla::ErrorResult& aRv) const
 {
   if (!mFrameSelection) {
     aRv.Throw(NS_ERROR_NOT_INITIALIZED);
     return Nullable<int16_t>();
   }
   nsBidiLevel caretBidiLevel = mFrameSelection->GetCaretBidiLevel();
@@ -762,32 +778,32 @@ NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(
   NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
   NS_INTERFACE_MAP_ENTRY(nsISupports)
 NS_INTERFACE_MAP_END
 
 NS_IMPL_MAIN_THREAD_ONLY_CYCLE_COLLECTING_ADDREF(Selection)
 NS_IMPL_MAIN_THREAD_ONLY_CYCLE_COLLECTING_RELEASE(Selection)
 
 const RangeBoundary&
-Selection::AnchorRef()
+Selection::AnchorRef() const
 {
   if (!mAnchorFocusRange) {
     static RangeBoundary sEmpty;
     return sEmpty;
   }
 
   if (GetDirection() == eDirNext) {
     return mAnchorFocusRange->StartRef();
   }
 
   return mAnchorFocusRange->EndRef();
 }
 
 const RangeBoundary&
-Selection::FocusRef()
+Selection::FocusRef() const
 {
   if (!mAnchorFocusRange) {
     static RangeBoundary sEmpty;
     return sEmpty;
   }
 
   if (GetDirection() == eDirNext){
     return mAnchorFocusRange->EndRef();
@@ -1552,16 +1568,17 @@ Selection::GetPrimaryOrCaretFrameForNode
   CaretAssociationHint hint = mFrameSelection->GetHint();
 
   if (aVisual) {
     nsBidiLevel caretBidiLevel = mFrameSelection->GetCaretBidiLevel();
 
     return nsCaret::GetCaretFrameForNodeOffset(mFrameSelection,
                                                aContent, aOffset, hint,
                                                caretBidiLevel, aReturnFrame,
+                                               /* aReturnUnadjustedFrame = */ nullptr,
                                                aOffsetUsed);
   }
 
   *aReturnFrame =
     mFrameSelection->GetFrameForNodeOffset(aContent, aOffset,
                                            hint, aOffsetUsed);
   if (!*aReturnFrame) {
     return NS_ERROR_FAILURE;
--- a/dom/base/Selection.h
+++ b/dom/base/Selection.h
@@ -201,17 +201,21 @@ public:
   nsRange*      GetRangeAt(int32_t aIndex) const;
 
   // Get the anchor-to-focus range if we don't care which end is
   // anchor and which end is focus.
   const nsRange* GetAnchorFocusRange() const {
     return mAnchorFocusRange;
   }
 
-  nsDirection  GetDirection(){return mDirection;}
+  nsDirection GetDirection() const
+  {
+    return mDirection;
+  }
+
   void         SetDirection(nsDirection aDir){mDirection = aDir;}
   nsresult     SetAnchorFocusToRange(nsRange *aRange);
   void         ReplaceAnchorFocusRange(nsRange *aRange);
   void         AdjustAnchorFocusForMultiRange(nsDirection aDirection);
 
   nsresult GetPrimaryFrameForAnchorNode(nsIFrame** aReturnFrame);
   nsresult GetPrimaryFrameForFocusNode(nsIFrame** aReturnFrame,
                                        int32_t* aOffset, bool aVisual);
@@ -231,50 +235,50 @@ public:
                                     const nsPoint& aPoint,
                                     uint32_t aDelay);
 
   nsresult     StopAutoScrollTimer();
 
   JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
 
   // WebIDL methods
-  nsINode* GetAnchorNode()
+  nsINode* GetAnchorNode() const
   {
     const RangeBoundary& anchor = AnchorRef();
     return anchor.IsSet() ? anchor.Container() : nullptr;
   }
-  uint32_t AnchorOffset()
+  uint32_t AnchorOffset() const
   {
     const RangeBoundary& anchor = AnchorRef();
     return anchor.IsSet() ? anchor.Offset() : 0;
   }
-  nsINode* GetFocusNode()
+  nsINode* GetFocusNode() const
   {
     const RangeBoundary& focus = FocusRef();
     return focus.IsSet() ? focus.Container() : nullptr;
   }
-  uint32_t FocusOffset()
+  uint32_t FocusOffset() const
   {
     const RangeBoundary& focus = FocusRef();
     return focus.IsSet() ? focus.Offset() : 0;
   }
 
   nsIContent* GetChildAtAnchorOffset()
   {
     const RangeBoundary& anchor = AnchorRef();
     return anchor.IsSet() ? anchor.GetChildAtOffset() : nullptr;
   }
   nsIContent* GetChildAtFocusOffset()
   {
     const RangeBoundary& focus = FocusRef();
     return focus.IsSet() ? focus.GetChildAtOffset() : nullptr;
   }
 
-  const RangeBoundary& AnchorRef();
-  const RangeBoundary& FocusRef();
+  const RangeBoundary& AnchorRef() const;
+  const RangeBoundary& FocusRef() const;
 
   /*
    * IsCollapsed -- is the whole selection just one point, or unset?
    */
   bool IsCollapsed() const
   {
     uint32_t cnt = mRanges.Length();
     if (cnt == 0) {
@@ -470,16 +474,19 @@ public:
   void SetBaseAndExtent(nsINode& aAnchorNode, uint32_t aAnchorOffset,
                         nsINode& aFocusNode, uint32_t aFocusOffset,
                         mozilla::ErrorResult& aRv);
 
   void AddSelectionChangeBlocker();
   void RemoveSelectionChangeBlocker();
   bool IsBlockingSelectionChangeEvents() const;
 
+  // Whether this selection is focused in an editable element.
+  bool IsEditorSelection() const;
+
   /**
    * Set the painting style for the range. The range must be a range in
    * the selection. The textRangeStyle will be used by text frame
    * when it is painting the selection.
    */
   nsresult SetTextRangeStyle(nsRange* aRange,
                              const TextRangeStyle& aTextRangeStyle);
 
--- a/dom/base/nsINode.cpp
+++ b/dom/base/nsINode.cpp
@@ -114,18 +114,17 @@
 #ifdef ACCESSIBILITY
 #include "mozilla/dom/AccessibleNode.h"
 #endif
 
 using namespace mozilla;
 using namespace mozilla::dom;
 
 nsINode::nsSlots::nsSlots()
-  : mWeakReference(nullptr),
-    mEditableDescendantCount(0)
+  : mWeakReference(nullptr)
 {
 }
 
 nsINode::nsSlots::~nsSlots()
 {
   if (mChildNodes) {
     mChildNodes->DropReference();
   }
@@ -1175,48 +1174,16 @@ nsINode::GetOwnerGlobalForBindings()
 
 nsIGlobalObject*
 nsINode::GetOwnerGlobal() const
 {
   bool dummy;
   return OwnerDoc()->GetScriptHandlingObject(dummy);
 }
 
-void
-nsINode::ChangeEditableDescendantCount(int32_t aDelta)
-{
-  if (aDelta == 0) {
-    return;
-  }
-
-  nsSlots* s = Slots();
-  MOZ_ASSERT(aDelta > 0 ||
-             s->mEditableDescendantCount >= (uint32_t) (-1 * aDelta));
-  s->mEditableDescendantCount += aDelta;
-}
-
-void
-nsINode::ResetEditableDescendantCount()
-{
-  nsSlots* s = GetExistingSlots();
-  if (s) {
-    s->mEditableDescendantCount = 0;
-  }
-}
-
-uint32_t
-nsINode::EditableDescendantCount()
-{
-  nsSlots* s = GetExistingSlots();
-  if (s) {
-    return s->mEditableDescendantCount;
-  }
-  return 0;
-}
-
 bool
 nsINode::UnoptimizableCCNode() const
 {
   const uintptr_t problematicFlags = (NODE_IS_ANONYMOUS_ROOT |
                                       NODE_IS_IN_NATIVE_ANONYMOUS_SUBTREE |
                                       NODE_IS_NATIVE_ANONYMOUS_ROOT |
                                       NODE_MAY_BE_IN_BINDING_MNGR);
   return HasFlag(problematicFlags) ||
--- a/dom/base/nsINode.h
+++ b/dom/base/nsINode.h
@@ -1136,22 +1136,16 @@ public:
 
     /**
      * A set of ranges which are in the selection and which have this node as
      * their endpoints' common ancestor.  This is a UniquePtr instead of just a
      * LinkedList, because that prevents us from pushing DOMSlots up to the next
      * allocation bucket size, at the cost of some complexity.
      */
     mozilla::UniquePtr<mozilla::LinkedList<nsRange>> mCommonAncestorRanges;
-
-    /**
-     * Number of descendant nodes in the uncomposed document that have been
-     * explicitly set as editable.
-     */
-    uint32_t mEditableDescendantCount;
   };
 
   /**
    * Functions for managing flags and slots
    */
 #ifdef DEBUG
   nsSlots* DebugGetSlots()
   {
@@ -1177,38 +1171,21 @@ public:
     NS_ASSERTION(!(aFlagsToUnset &
                    (NODE_IS_ANONYMOUS_ROOT |
                     NODE_IS_IN_NATIVE_ANONYMOUS_SUBTREE |
                     NODE_IS_NATIVE_ANONYMOUS_ROOT)),
                  "Trying to unset write-only flags");
     nsWrapperCache::UnsetFlags(aFlagsToUnset);
   }
 
-  void ChangeEditableDescendantCount(int32_t aDelta);
-
-  /**
-   * Returns the count of descendant nodes in the uncomposed
-   * document that are explicitly set as editable.
-   */
-  uint32_t EditableDescendantCount();
-
-  /**
-   * Sets the editable descendant count to 0. The editable
-   * descendant count only counts explicitly editable nodes
-   * that are in the uncomposed document so this method
-   * should be called when nodes are are removed from it.
-   */
-  void ResetEditableDescendantCount();
-
   void SetEditableFlag(bool aEditable)
   {
     if (aEditable) {
       SetFlags(NODE_IS_EDITABLE);
-    }
-    else {
+    } else {
       UnsetFlags(NODE_IS_EDITABLE);
     }
   }
 
   inline bool IsEditable() const;
 
   /**
    * Returns true if |this| or any of its ancestors is native anonymous.
--- a/dom/base/test/test_bug166235.html
+++ b/dom/base/test/test_bug166235.html
@@ -11,17 +11,17 @@ https://bugzilla.mozilla.org/show_bug.cg
 </head>
 <body>
 <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=166235">Mozilla Bug 166235 and Bug 816298</a>
 <p id="test0">This text should be copied.</p>
 <p id="test1">This text should<span style="-moz-user-select: none;"> NOT</span> be copied.</p>
 <p id="test2">This<span style="-moz-user-select: none;"><span style="-moz-user-select: text"> text should</span> NOT</span> be copied.</p>
 <p id="test3">This text should<span style="-moz-user-select: -moz-none;"> NOT</span> be copied.</p>
 <p id="test4">This<span style="-moz-user-select: -moz-none;"><span style="-moz-user-select: text"> text should</span> NOT</span> be copied.</p>
-<p id="test5">This<span style="-moz-user-select: all"> text<span style="-moz-user-select: none"> should</span></span> be copied.</p>
+<p id="test5">This<span style="-moz-user-select: all"> text should</span> be copied.</p>
 <div id="content" style="display: none">
   
 </div>
 <textarea id="input"></textarea>
 <pre id="test">
 <script type="application/javascript">
   "use strict";
 
@@ -102,17 +102,17 @@ var originalStrings = [
 
 // expected results for clipboard text/html
 var clipboardHTML = [
   '<p id=\"test0\">This text should be copied.</p>',
   '<p id=\"test1\">This text should be copied.</p>',
   '<p id=\"test2\">This<span style=\"-moz-user-select: text\"> text should</span> be copied.</p>',
   '<p id=\"test3\">This text should be copied.</p>',
   '<p id=\"test4\">This<span style=\"-moz-user-select: text\"> text should</span> be copied.</p>',
-  '<p id=\"test5\">This<span style=\"-moz-user-select: all\"> text<span style=\"-moz-user-select: none\"> should</span></span> be copied.</p>',
+  '<p id=\"test5\">This<span style=\"-moz-user-select: all\"> text should</span> be copied.</p>',
 ];
 
 // expected results for clipboard text/unicode
 var clipboardUnicode = [
   'This text should be copied.',
   'This text should be copied.',
   'This text should be copied.',
   'This text should be copied.',
@@ -122,17 +122,17 @@ var clipboardUnicode = [
 
 // expected results for .innerHTML
 var innerHTMLStrings = [
   'This text should be copied.',
   'This text should<span style=\"-moz-user-select: none;\"> NOT</span> be copied.',
   'This<span style=\"-moz-user-select: none;\"><span style=\"-moz-user-select: text\"> text should</span> NOT</span> be copied.',
   'This text should<span style=\"-moz-user-select: -moz-none;\"> NOT</span> be copied.',
   'This<span style=\"-moz-user-select: -moz-none;\"><span style=\"-moz-user-select: text\"> text should</span> NOT</span> be copied.',
-  'This<span style=\"-moz-user-select: all\"> text<span style=\"-moz-user-select: none\"> should</span></span> be copied.',
+  'This<span style=\"-moz-user-select: all\"> text should</span> be copied.',
 ];
 
 // expected results for pasting into a TEXTAREA
 var textareaStrings = [
   'This text should be copied.',
   'This text should be copied.',
   'This text should be copied.',
   'This text should be copied.',
--- a/dom/html/nsGenericHTMLElement.cpp
+++ b/dom/html/nsGenericHTMLElement.cpp
@@ -402,24 +402,16 @@ nsGenericHTMLElement::IntrinsicState() c
                  "HTML element's directionality must be either RTL or LTR");
     state |= NS_EVENT_STATE_LTR;
     state &= ~NS_EVENT_STATE_RTL;
   }
 
   return state;
 }
 
-uint32_t
-nsGenericHTMLElement::EditableInclusiveDescendantCount()
-{
-  bool isEditable = IsInComposedDoc() && HasFlag(NODE_IS_EDITABLE) &&
-    GetContentEditableValue() == eTrue;
-  return EditableDescendantCount() + isEditable;
-}
-
 nsresult
 nsGenericHTMLElement::BindToTree(nsIDocument* aDocument, nsIContent* aParent,
                                  nsIContent* aBindingParent)
 {
   nsresult rv = nsGenericHTMLElementBase::BindToTree(aDocument, aParent,
                                                      aBindingParent);
   NS_ENSURE_SUCCESS(rv, rv);
 
@@ -2674,17 +2666,17 @@ MakeContentDescendantsEditable(nsIConten
   // internal editable state and don't need to notify anyone about
   // that.  For elements, we need to send a ContentStateChanged
   // notification.
   if (!aContent->IsElement()) {
     aContent->UpdateEditableState(false);
     return;
   }
 
-  Element *element = aContent->AsElement();
+  Element* element = aContent->AsElement();
 
   element->UpdateEditableState(true);
 
   for (nsIContent *child = aContent->GetFirstChild();
        child;
        child = child->GetNextSibling()) {
     if (!child->IsElement() ||
         !child->AsElement()->HasAttr(kNameSpaceID_None,
@@ -2698,28 +2690,19 @@ void
 nsGenericHTMLElement::ChangeEditableState(int32_t aChange)
 {
   nsIDocument* document = GetComposedDoc();
   if (!document) {
     return;
   }
 
   if (aChange != 0) {
-    nsCOMPtr<nsIHTMLDocument> htmlDocument =
-      do_QueryInterface(document);
-    if (htmlDocument) {
+    if (nsCOMPtr<nsIHTMLDocument> htmlDocument = do_QueryInterface(document)) {
       htmlDocument->ChangeContentEditableCount(this, aChange);
     }
-
-    nsIContent* parent = GetParent();
-    // Don't update across Shadow DOM boundary.
-    while (parent && parent->IsElement()) {
-      parent->ChangeEditableDescendantCount(aChange);
-      parent = parent->GetParent();
-    }
   }
 
   if (document->HasFlag(NODE_IS_EDITABLE)) {
     document = nullptr;
   }
 
   // MakeContentDescendantsEditable is going to call ContentStateChanged for
   // this element and all descendants if editable state has changed.
--- a/editor/libeditor/EditorBase.cpp
+++ b/editor/libeditor/EditorBase.cpp
@@ -4676,20 +4676,26 @@ EditorBase::InitializeSelection(EventTar
     return NS_ERROR_FAILURE;
   }
 
   // Init the caret
   RefPtr<nsCaret> caret = presShell->GetCaret();
   if (NS_WARN_IF(!caret)) {
     return NS_ERROR_FAILURE;
   }
-  caret->SetIgnoreUserModify(false);
   caret->SetSelection(SelectionRefPtr());
   selectionController->SetCaretReadOnly(IsReadonly());
   selectionController->SetCaretEnabled(true);
+  // NOTE(emilio): It's important for this call to be after
+  // SetCaretEnabled(true), since that would override mIgnoreUserModify to true.
+  //
+  // Also, make sure to always ignore it for designMode, since that effectively
+  // overrides everything and we allow to edit stuff with
+  // contenteditable="false" subtrees in such a document.
+  caret->SetIgnoreUserModify(targetNode->OwnerDoc()->HasFlag(NODE_IS_EDITABLE));
 
   // Init selection
   selectionController->SetDisplaySelection(
                          nsISelectionController::SELECTION_ON);
   selectionController->SetSelectionFlags(
                          nsISelectionDisplay::DISPLAY_ALL);
   selectionController->RepaintSelection(
                          nsISelectionController::SELECTION_NORMAL);
@@ -4768,16 +4774,20 @@ EditorBase::FinalizeSelection()
     return NS_ERROR_NOT_INITIALIZED;
   }
 
   SelectionRefPtr()->SetAncestorLimiter(nullptr);
 
   nsCOMPtr<nsIPresShell> presShell = GetPresShell();
   NS_ENSURE_TRUE(presShell, NS_ERROR_NOT_INITIALIZED);
 
+  if (RefPtr<nsCaret> caret = presShell->GetCaret()) {
+    caret->SetIgnoreUserModify(true);
+  }
+
   selectionController->SetCaretEnabled(false);
 
   nsFocusManager* fm = nsFocusManager::GetFocusManager();
   NS_ENSURE_TRUE(fm, NS_ERROR_NOT_INITIALIZED);
   fm->UpdateCaretForCaretBrowsingMode();
 
   if (!HasIndependentSelection()) {
     // If this editor doesn't have an independent selection, i.e., it must
--- a/editor/libeditor/tests/test_bug430392.html
+++ b/editor/libeditor/tests/test_bug430392.html
@@ -43,17 +43,17 @@ function test() {
            ? undefined : " A; B ; C "],
   ["adding shift-returns", () => {
     getSelection().collapse(edit.firstChild, 0);
     synthesizeKey("KEY_ArrowRight");
     synthesizeKey("KEY_Enter", {shiftKey: true});
     synthesizeKey("KEY_Enter", {shiftKey: true});
     synthesizeKey("KEY_Backspace");
     synthesizeKey("KEY_Backspace");
-  }, "A ; B ; C "]];
+  }]];
   [
     "insertorderedlist",
     "insertunorderedlist",
     ["formatblock", "p"],
   ]
   .forEach(item => {
       var cmd = Array.isArray(item) ? item[0] : item;
       var param = Array.isArray(item) ? item[1] : "";
--- a/layout/base/nsCaret.cpp
+++ b/layout/base/nsCaret.cpp
@@ -94,22 +94,24 @@ AdjustCaretFrameForLineEnd(nsIFrame** aF
 {
   nsLineBox* line = FindContainingLine(*aFrame);
   if (!line)
     return;
   int32_t count = line->GetChildCount();
   for (nsIFrame* f = line->mFirstChild; count > 0; --count, f = f->GetNextSibling())
   {
     nsIFrame* r = CheckForTrailingTextFrameRecursive(f, *aFrame);
-    if (r == *aFrame)
+    if (r == *aFrame) {
       return;
-    if (r)
-    {
+    }
+    if (r) {
+      // We found our frame, but we may not be able to properly paint the caret if
+      // -moz-user-modify differs from our actual frame.
+      MOZ_ASSERT(r->IsTextFrame(), "Expected text frame");
       *aFrame = r;
-      NS_ASSERTION(r->IsTextFrame(), "Expected text frame");
       *aOffset = (static_cast<nsTextFrame*>(r))->GetContentEnd();
       return;
     }
   }
 }
 
 static bool
 IsBidiUI()
@@ -375,18 +377,23 @@ nsCaret::GetGeometryForFrame(nsIFrame* a
     *aBidiIndicatorSize = caretMetrics.mBidiIndicatorSize;
   }
   return rect;
 }
 
 nsIFrame*
 nsCaret::GetFrameAndOffset(Selection* aSelection,
                            nsINode* aOverrideNode, int32_t aOverrideOffset,
-                           int32_t* aFrameOffset)
+                           int32_t* aFrameOffset,
+                           nsIFrame** aUnadjustedFrame)
 {
+  if (aUnadjustedFrame) {
+    *aUnadjustedFrame = nullptr;
+  }
+
   nsINode* focusNode;
   int32_t focusOffset;
 
   if (aOverrideNode) {
     focusNode = aOverrideNode;
     focusOffset = aOverrideOffset;
   } else if (aSelection) {
     focusNode = aSelection->GetFocusNode();
@@ -400,21 +407,21 @@ nsCaret::GetFrameAndOffset(Selection* aS
   }
 
   nsIContent* contentNode = focusNode->AsContent();
   nsFrameSelection* frameSelection = aSelection->GetFrameSelection();
   nsBidiLevel bidiLevel = frameSelection->GetCaretBidiLevel();
   nsIFrame* frame;
   nsresult rv = nsCaret::GetCaretFrameForNodeOffset(
       frameSelection, contentNode, focusOffset,
-      frameSelection->GetHint(), bidiLevel, &frame, aFrameOffset);
+      frameSelection->GetHint(), bidiLevel, &frame, aUnadjustedFrame,
+      aFrameOffset);
   if (NS_FAILED(rv) || !frame) {
     return nullptr;
   }
-
   return frame;
 }
 
 /* static */ nsIFrame*
 nsCaret::GetGeometry(Selection* aSelection, nsRect* aRect)
 {
   int32_t frameOffset;
   nsIFrame* frame = GetFrameAndOffset(aSelection, nullptr, 0, &frameOffset);
@@ -488,26 +495,36 @@ nsCaret::GetPaintGeometry(nsRect* aRect)
     return nullptr;
   }
 
   // Update selection language direction now so the new direction will be
   // taken into account when computing the caret position below.
   CheckSelectionLanguageChange();
 
   int32_t frameOffset;
+  nsIFrame* unadjustedFrame = nullptr;
   nsIFrame* frame = GetFrameAndOffset(GetSelection(),
-      mOverrideContent, mOverrideOffset, &frameOffset);
+      mOverrideContent, mOverrideOffset, &frameOffset, &unadjustedFrame);
+  MOZ_ASSERT(!!frame == !!unadjustedFrame);
   if (!frame) {
     return nullptr;
   }
 
-  // now we have a frame, check whether it's appropriate to show the caret here
-  const nsStyleUI* ui = frame->StyleUI();
+  // Now we have a frame, check whether it's appropriate to show the caret here.
+  // Note we need to check the unadjusted frame, otherwise consider the
+  // following case:
+  //
+  //   <div contenteditable><span contenteditable=false>Text   </span><br>
+  //
+  // Where the selection is targeting the <br>. We want to display the caret,
+  // since the <br> we're focused at is editable, but we do want to paint it at
+  // the adjusted frame offset, so that we can see the collapsed whitespace.
+  const nsStyleUI* ui = unadjustedFrame->StyleUI();
   if ((!mIgnoreUserModify && ui->mUserModify == StyleUserModify::ReadOnly) ||
-      frame->IsContentDisabled()) {
+      unadjustedFrame->IsContentDisabled()) {
     return nullptr;
   }
 
   // If the offset falls outside of the frame, then don't paint the caret.
   int32_t startOffset, endOffset;
   if (frame->IsTextFrame() &&
       (NS_FAILED(frame->GetOffsets(startOffset, endOffset)) ||
        startOffset > frameOffset || endOffset < frameOffset)) {
@@ -640,16 +657,17 @@ void nsCaret::StopBlinking()
 
 nsresult
 nsCaret::GetCaretFrameForNodeOffset(nsFrameSelection*    aFrameSelection,
                                     nsIContent*          aContentNode,
                                     int32_t              aOffset,
                                     CaretAssociationHint aFrameHint,
                                     nsBidiLevel          aBidiLevel,
                                     nsIFrame**           aReturnFrame,
+                                    nsIFrame**           aReturnUnadjustedFrame,
                                     int32_t*             aReturnOffset)
 {
   if (!aFrameSelection)
     return NS_ERROR_FAILURE;
   nsIPresShell* presShell = aFrameSelection->GetShell();
   if (!presShell)
     return NS_ERROR_FAILURE;
 
@@ -660,30 +678,33 @@ nsCaret::GetCaretFrameForNodeOffset(nsFr
   nsIFrame* theFrame = nullptr;
   int32_t   theFrameOffset = 0;
 
   theFrame = aFrameSelection->GetFrameForNodeOffset(
       aContentNode, aOffset, aFrameHint, &theFrameOffset);
   if (!theFrame)
     return NS_ERROR_FAILURE;
 
+  if (aReturnUnadjustedFrame) {
+    *aReturnUnadjustedFrame = theFrame;
+  }
+
   // if theFrame is after a text frame that's logically at the end of the line
   // (e.g. if theFrame is a <br> frame), then put the caret at the end of
   // that text frame instead. This way, the caret will be positioned as if
   // trailing whitespace was not trimmed.
   AdjustCaretFrameForLineEnd(&theFrame, &theFrameOffset);
 
   // Mamdouh : modification of the caret to work at rtl and ltr with Bidi
   //
   // Direction Style from visibility->mDirection
   // ------------------
   // NS_STYLE_DIRECTION_LTR : LTR or Default
   // NS_STYLE_DIRECTION_RTL
-  if (theFrame->PresContext()->BidiEnabled())
-  {
+  if (theFrame->PresContext()->BidiEnabled()) {
     // If there has been a reflow, take the caret Bidi level to be the level of the current frame
     if (aBidiLevel & BIDI_LEVEL_UNDEFINED) {
       aBidiLevel = theFrame->GetEmbeddingLevel();
     }
 
     int32_t start;
     int32_t end;
     nsIFrame* frameBefore;
--- a/layout/base/nsCaret.h
+++ b/layout/base/nsCaret.h
@@ -177,30 +177,34 @@ class nsCaret final : public nsISelectio
     static nsIFrame* GetGeometry(mozilla::dom::Selection* aSelection,
                                  nsRect* aRect);
     static nsresult GetCaretFrameForNodeOffset(nsFrameSelection* aFrameSelection,
                                                nsIContent* aContentNode,
                                                int32_t aOffset,
                                                CaretAssociationHint aFrameHint,
                                                uint8_t aBidiLevel,
                                                nsIFrame** aReturnFrame,
+                                               nsIFrame** aReturnUnadjustedFrame,
                                                int32_t* aReturnOffset);
     static nsRect GetGeometryForFrame(nsIFrame* aFrame,
                                       int32_t   aFrameOffset,
                                       nscoord*  aBidiIndicatorSize);
 
     // Get the frame and frame offset based on the focus node and focus offset
     // of aSelection. If aOverrideNode and aOverride are provided, use them
     // instead.
     // @param aFrameOffset return the frame offset if non-null.
+    // @param aUnadjustedFrame return the original frame that the selection is
+    // targeting, without any adjustment for painting.
     // @return the frame of the focus node.
     static nsIFrame* GetFrameAndOffset(mozilla::dom::Selection* aSelection,
                                        nsINode* aOverrideNode,
                                        int32_t aOverrideOffset,
-                                       int32_t* aFrameOffset);
+                                       int32_t* aFrameOffset,
+                                       nsIFrame** aUnadjustedFrame = nullptr);
 
     size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
 
     nsIFrame*     GetFrame(int32_t* aContentOffset);
     void          ComputeCaretRects(nsIFrame* aFrame, int32_t aFrameOffset,
                                     nsRect* aCaretRect, nsRect* aHookRect);
 
 protected:
--- a/layout/base/nsLayoutUtils.cpp
+++ b/layout/base/nsLayoutUtils.cpp
@@ -4773,17 +4773,17 @@ nsLayoutUtils::GetNonGeneratedAncestor(n
   nsIFrame* f = aFrame;
   do {
     f = GetParentOrPlaceholderFor(f);
   } while (f->GetStateBits() & NS_FRAME_GENERATED_CONTENT);
   return f;
 }
 
 nsIFrame*
-nsLayoutUtils::GetParentOrPlaceholderFor(nsIFrame* aFrame)
+nsLayoutUtils::GetParentOrPlaceholderFor(const nsIFrame* aFrame)
 {
   if ((aFrame->GetStateBits() & NS_FRAME_OUT_OF_FLOW)
       && !aFrame->GetPrevInFlow()) {
     return aFrame->GetProperty(nsIFrame::PlaceholderFrameProperty());
   }
   return aFrame->GetParent();
 }
 
--- a/layout/base/nsLayoutUtils.h
+++ b/layout/base/nsLayoutUtils.h
@@ -1384,17 +1384,17 @@ public:
    * Whether the frame is an nsBlockFrame which is not a wrapper block.
    */
   static bool IsNonWrapperBlock(nsIFrame* aFrame);
 
   /**
    * If aFrame is an out of flow frame, return its placeholder, otherwise
    * return its parent.
    */
-  static nsIFrame* GetParentOrPlaceholderFor(nsIFrame* aFrame);
+  static nsIFrame* GetParentOrPlaceholderFor(const nsIFrame* aFrame);
 
   /**
    * If aFrame is an out of flow frame, return its placeholder, otherwise
    * return its (possibly cross-doc) parent.
    */
   static nsIFrame* GetParentOrPlaceholderForCrossDoc(nsIFrame* aFrame);
 
   /**
--- a/layout/base/tests/bug1109968-1.html
+++ b/layout/base/tests/bug1109968-1.html
@@ -8,16 +8,16 @@
       var div = document.querySelector("div");
       function start() {
         div.focus();
       }
       function done() {
         var sel = getSelection();
         sel.collapse(div, 0);
         // Press Right four times to set the caret right before "baz"
-        for (var i = 0; i < 5; ++i) {
+        for (var i = 0; i < 4; ++i) {
           synthesizeKey("KEY_ArrowRight");
         }
         document.documentElement.removeAttribute("class");
       }
     </script>
   </body>
 </html>
--- a/layout/base/tests/bug1109968-2.html
+++ b/layout/base/tests/bug1109968-2.html
@@ -8,16 +8,16 @@
       var div = document.querySelector("div");
       function start() {
         div.focus();
       }
       function done() {
         var sel = getSelection();
         sel.collapse(div, 0);
         // Press Right four times to set the caret right before "bar"
-        for (var i = 0; i < 6; ++i) {
+        for (var i = 0; i < 4; ++i) {
           synthesizeKey("KEY_ArrowRight");
         }
         document.documentElement.removeAttribute("class");
       }
     </script>
   </body>
 </html>
--- a/layout/base/tests/bug1132768-1.html
+++ b/layout/base/tests/bug1132768-1.html
@@ -1,17 +1,17 @@
 <!DOCTYPE html>
 <html class="reftest-wait">
+  <title>Can drag-select non-editable content inside editable content</title>
+  <script src="selection-utils.js"></script>
+  <script src="/tests/SimpleTest/SimpleTest.js"></script>
   <script src="/tests/SimpleTest/EventUtils.js"></script>
+  <div contenteditable spellcheck="false"
+       style="outline: none">foo<span contenteditable=false>bar</span>baz</div>
   <script>
-    function test() {
-      focus();
-      synthesizeMouseAtCenter(document.querySelector("span"), {});
-    }
-    function focused() {
-      document.documentElement.removeAttribute("class");
-    }
+  SimpleTest.waitForFocus(function() {
+    const span = document.querySelector("span");
+    const rect = span.getBoundingClientRect();
+    dragSelectPoints(span, 0, rect.height / 2, rect.width, rect.height / 2);
+    setTimeout(() => document.documentElement.removeAttribute("class"));
+  });
   </script>
-  <body onload="setTimeout(test, 0)">
-    <div contenteditable spellcheck="false" onfocus="focused()"
-         style="outline: none">foo<span contenteditable=false>bar</span>baz</div>
-  </body>
 </html>
new file mode 100644
--- /dev/null
+++ b/layout/base/tests/bug1506547-1.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<html class="reftest-wait">
+<title>Moving the caret in an editor jumps over non-editable nodes.</title>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<style>
+ * { outline: none }
+
+ div {
+  border: 1px solid red;
+  margin: 5px;
+  padding: 2px;
+ }
+</style>
+<div contenteditable="true">
+  I am div number one
+  <div contenteditable="false">X X X</div>
+  However I am editable
+</div>
+<script>
+SimpleTest.waitForFocus(function() {
+  const editable = document.querySelector('div[contenteditable="true"]');
+  const noneditable = document.querySelector('div[contenteditable="false"]');
+  editable.focus();
+  synthesizeKey("KEY_ArrowDown");
+  setTimeout(() => document.documentElement.removeAttribute("class"), 0);
+});
+</script>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/base/tests/bug1506547-2.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<html class="reftest-wait">
+<title>Moving the caret in an editor jumps over non-editable nodes.</title>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<style>
+ * { outline: none }
+
+ div {
+  border: 1px solid red;
+  margin: 5px;
+  padding: 2px;
+ }
+</style>
+<div contenteditable="true">
+  I am div number one
+  <div contenteditable="false">X X X</div>
+  However I am editable
+</div>
+<script>
+SimpleTest.waitForFocus(function() {
+  const editable = document.querySelector('div[contenteditable="true"]');
+  editable.focus();
+  // 5 words in the first line, plus the non-editable node.
+  for (let i = 0; i < "I am div number one".length + 2; ++i)
+    synthesizeKey("KEY_ArrowRight");
+  setTimeout(() => document.documentElement.removeAttribute("class"), 0);
+});
+</script>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/base/tests/bug1506547-3.html
@@ -0,0 +1,32 @@
+<!doctype html>
+<html class="reftest-wait">
+<title>Moving the caret in an editor jumps over non-editable nodes.</title>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<style>
+ * { outline: none }
+
+ div {
+  border: 1px solid red;
+  margin: 5px;
+  padding: 2px;
+ }
+</style>
+<div contenteditable="true">
+  I am div number one
+  <div contenteditable="false">X X X</div>
+  However I am editable
+</div>
+<script>
+SimpleTest.waitForFocus(function() {
+  const editable = document.querySelector('div[contenteditable="true"]');
+  const noneditable = document.querySelector('div[contenteditable="false"]');
+  editable.focus();
+  synthesizeKey("KEY_End");
+  synthesizeKey("KEY_ArrowDown");
+  for (let i = 0; i < 4; ++i)
+    synthesizeKey("KEY_ArrowLeft", { ctrlKey: true });
+  setTimeout(() => document.documentElement.removeAttribute("class"), 0);
+});
+</script>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/base/tests/bug1506547-4-ref.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<title>Caret on editable line with non-editable content and whitespace.</title>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<style>
+ * { outline: none }
+
+ div {
+  border: 1px solid red;
+  margin: 5px;
+  padding: 2px;
+ }
+</style>
+<div contenteditable="true"><span>xyz </span><br>editable</div>
+<script>
+SimpleTest.waitForFocus(function() {
+  const editable = document.querySelector('div[contenteditable="true"]');
+  editable.focus();
+  synthesizeMouse(editable, 100, 10, {});
+});
+</script>
new file mode 100644
--- /dev/null
+++ b/layout/base/tests/bug1506547-4.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<title>Caret on editable line with non-editable content and whitespace.</title>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<style>
+ * { outline: none }
+
+ div {
+  border: 1px solid red;
+  margin: 5px;
+  padding: 2px;
+ }
+</style>
+<div contenteditable="true"><span contenteditable="false">xyz </span><br>editable</div>
+<script>
+SimpleTest.waitForFocus(function() {
+  const editable = document.querySelector('div[contenteditable="true"]');
+  editable.focus();
+  synthesizeMouse(editable, 100, 10, {});
+});
+</script>
new file mode 100644
--- /dev/null
+++ b/layout/base/tests/bug1506547-5-ref.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<html class="reftest-wait">
+<title>Caret on editable line with non-editable content and whitespace.</title>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<style>
+ * { outline: none }
+
+ div {
+  border: 1px solid red;
+  margin: 5px;
+  padding: 2px;
+ }
+</style>
+<div contenteditable="true"><span>xyz </span><br>editable</div>
+<script>
+SimpleTest.waitForFocus(function() {
+  const editable = document.querySelector('div[contenteditable="true"]');
+  editable.focus();
+  synthesizeMouse(editable, 100, 10, {});
+  setTimeout(() => {
+    sendString("xxx");
+    setTimeout(() => document.documentElement.className = "");
+  });
+});
+</script>
new file mode 100644
--- /dev/null
+++ b/layout/base/tests/bug1506547-5.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<html class="reftest-wait">
+<title>Caret on editable line with non-editable content and whitespace.</title>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<style>
+ * { outline: none }
+
+ div {
+  border: 1px solid red;
+  margin: 5px;
+  padding: 2px;
+ }
+</style>
+<div contenteditable="true"><span contenteditable="false">xyz </span><br>editable</div>
+<script>
+SimpleTest.waitForFocus(function() {
+  const editable = document.querySelector('div[contenteditable="true"]');
+  editable.focus();
+  synthesizeMouse(editable, 100, 10, {});
+  setTimeout(() => {
+    sendString("xxx");
+    setTimeout(() => document.documentElement.className = "");
+  });
+});
+</script>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/base/tests/bug1506547-6.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<html class="reftest-wait">
+<title>Caret on editable line with non-editable content and whitespace.</title>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<style>
+ * { outline: none }
+
+ div {
+  border: 1px solid red;
+  margin: 5px;
+  padding: 2px;
+ }
+</style>
+<div contenteditable="true"><span contenteditable="false">xyz </span><br>editable</div>
+<script>
+SimpleTest.waitForFocus(function() {
+  const editable = document.querySelector('div[contenteditable="true"]');
+  editable.focus();
+  synthesizeKey("KEY_ArrowDown");
+  synthesizeKey("KEY_ArrowLeft");
+  setTimeout(() => {
+    sendString("xxx");
+    setTimeout(() => document.documentElement.className = "");
+  });
+});
+</script>
--- a/layout/base/tests/mochitest.ini
+++ b/layout/base/tests/mochitest.ini
@@ -347,16 +347,25 @@ support-files =
   textarea-maxlength-valid-before-change.html
   textarea-maxlength-valid-change.html
   textarea-minlength-invalid-change.html
   textarea-minlength-ui-invalid-change.html
   textarea-minlength-ui-valid-change.html
   textarea-minlength-valid-before-change.html
   textarea-minlength-valid-change.html
   textarea-valid-ref.html
+  bug1506547-1.html
+  bug1506547-2.html
+  bug1506547-3.html
+  bug1506547-4.html
+  bug1506547-5.html
+  bug1506547-6.html
+  bug1506547-4-ref.html
+  bug1506547-5-ref.html
+
 [test_remote_frame.html]
 [test_resize_flush.html]
 support-files = resize_flush_iframe.html
 [test_scroll_event_ordering.html]
 [test_scroll_selection_into_view.html]
 skip-if = toolkit == 'android' # Bug 1355844
 support-files =
   scroll_selection_into_view_window.html
--- a/layout/base/tests/test_reftests_with_caret.html
+++ b/layout/base/tests/test_reftests_with_caret.html
@@ -201,16 +201,21 @@ var tests = [
     [ 'bug1415416.html'   , 'bug1415416-ref.html' ] ,
     [ 'bug1423331-1.html' , 'bug1423331-1-ref.html' ] ,
     [ 'bug1423331-2.html' , 'bug1423331-2-ref.html' ] ,
     // FIXME(bug 1434949): These two fail in some platforms.
     // [ 'bug1423331-3.html' , 'bug1423331-1-ref.html' ] ,
     // [ 'bug1423331-4.html' , 'bug1423331-2-ref.html' ] ,
     [ 'bug1484094-1.html' , 'bug1484094-1-ref.html' ] ,
     [ 'bug1484094-2.html' , 'bug1484094-2-ref.html' ] ,
+    [ 'bug1506547-1.html' , 'bug1506547-2.html' ] ,
+    [ 'bug1506547-2.html' , 'bug1506547-3.html' ] ,
+    [ 'bug1506547-4.html' , 'bug1506547-4-ref.html' ] ,
+    [ 'bug1506547-5.html' , 'bug1506547-5-ref.html' ] ,
+    [ 'bug1506547-6.html' , 'bug1506547-5-ref.html' ] ,
     function() {SpecialPowers.pushPrefEnv({'clear': [['layout.accessiblecaret.enabled']]}, nextTest);} ,
 ];
 
 if (!navigator.appVersion.includes("Android")) {
   tests.push([ 'bug512295-1.html' , 'bug512295-1-ref.html' ]);
   tests.push([ 'bug512295-2.html' , 'bug512295-2-ref.html' ]);
   tests.push([ 'bug923376.html'   , 'bug923376-ref.html'   ]);
   tests.push(function() {SpecialPowers.pushPrefEnv({'set': [['layout.css.overflow-clip-box.enabled', true]]}, nextTest);});
--- a/layout/generic/nsFrame.cpp
+++ b/layout/generic/nsFrame.cpp
@@ -4124,90 +4124,40 @@ nsFrame::GetDataForTableSelection(const 
   if (foundCell)
     *aTarget = TableSelection::Cell;
   else if (foundTable)
     *aTarget = TableSelection::Table;
 
   return NS_OK;
 }
 
+static StyleUserSelect
+UsedUserSelect(const nsIFrame* aFrame)
+{
+  if (aFrame->HasAnyStateBits(NS_FRAME_GENERATED_CONTENT)) {
+    return StyleUserSelect::None;
+  }
+
+  auto style = aFrame->StyleUIReset()->mUserSelect;
+  if (style != StyleUserSelect::Auto) {
+    return style;
+  }
+
+  auto* parent = nsLayoutUtils::GetParentOrPlaceholderFor(aFrame);
+  return parent ? UsedUserSelect(parent) : StyleUserSelect::Text;
+}
+
 bool
 nsIFrame::IsSelectable(StyleUserSelect* aSelectStyle) const
 {
-  // it's ok if aSelectStyle is null
-
-  // Like 'visibility', we must check all the parents: if a parent
-  // is not selectable, none of its children is selectable.
-  //
-  // The -moz-all value acts similarly: if a frame has 'user-select:-moz-all',
-  // all its children are selectable, even those with 'user-select:none'.
-  //
-  // As a result, if 'none' and '-moz-all' are not present in the frame hierarchy,
-  // aSelectStyle returns the first style that is not AUTO. If these values
-  // are present in the frame hierarchy, aSelectStyle returns the style of the
-  // topmost parent that has either 'none' or '-moz-all'.
-  //
-  // The -moz-text value acts as a way to override an ancestor's all/-moz-all value.
-  //
-  // For instance, if the frame hierarchy is:
-  //    AUTO     -> _MOZ_ALL  -> NONE -> TEXT,      the returned value is ALL
-  //    AUTO     -> _MOZ_ALL  -> NONE -> _MOZ_TEXT, the returned value is TEXT.
-  //    TEXT     -> NONE      -> AUTO -> _MOZ_ALL,  the returned value is TEXT
-  //    _MOZ_ALL -> TEXT      -> AUTO -> AUTO,      the returned value is ALL
-  //    _MOZ_ALL -> _MOZ_TEXT -> AUTO -> AUTO,      the returned value is TEXT.
-  //    AUTO     -> CELL      -> TEXT -> AUTO,      the returned value is TEXT
-  //
-  StyleUserSelect selectStyle  = StyleUserSelect::Auto;
-  nsIFrame* frame              = const_cast<nsIFrame*>(this);
-  bool containsEditable        = false;
-
-  while (frame) {
-    const nsStyleUIReset* userinterface = frame->StyleUIReset();
-    switch (userinterface->mUserSelect) {
-      case StyleUserSelect::All: {
-        // override the previous values
-        if (selectStyle != StyleUserSelect::MozText) {
-          selectStyle = userinterface->mUserSelect;
-        }
-        nsIContent* frameContent = frame->GetContent();
-        containsEditable = frameContent &&
-          frameContent->EditableDescendantCount() > 0;
-        break;
-      }
-      default:
-        // otherwise return the first value which is not 'auto'
-        if (selectStyle == StyleUserSelect::Auto) {
-          selectStyle = userinterface->mUserSelect;
-        }
-        break;
-    }
-    frame = nsLayoutUtils::GetParentOrPlaceholderFor(frame);
-  }
-
-  // convert internal values to standard values
-  if (selectStyle == StyleUserSelect::Auto ||
-      selectStyle == StyleUserSelect::MozText) {
-    selectStyle = StyleUserSelect::Text;
-  }
-
-  // If user tries to select all of a non-editable content,
-  // prevent selection if it contains editable content.
-  bool allowSelection = true;
-  if (selectStyle == StyleUserSelect::All) {
-    allowSelection = !containsEditable;
-  }
-
-  // return stuff
+  auto style = UsedUserSelect(this);
   if (aSelectStyle) {
-    *aSelectStyle = selectStyle;
-  }
-
-  return !(mState & NS_FRAME_GENERATED_CONTENT) &&
-         allowSelection &&
-         selectStyle != StyleUserSelect::None;
+    *aSelectStyle = style;
+  }
+  return style != StyleUserSelect::None;
 }
 
 /**
   * Handles the Mouse Press Event for the frame
  */
 NS_IMETHODIMP
 nsFrame::HandlePress(nsPresContext* aPresContext,
                      WidgetGUIEvent* aEvent,
@@ -4971,25 +4921,31 @@ static FrameTarget GetSelectionClosestFr
     return DrillDownToSelectionFrame(aParent, true, aFlags);
   nsIFrame* frame = aLine->mFirstChild;
   nsIFrame* closestFromIStart = nullptr;
   nsIFrame* closestFromIEnd = nullptr;
   nscoord closestIStart = aLine->IStart(), closestIEnd = aLine->IEnd();
   WritingMode wm = aLine->mWritingMode;
   LogicalPoint pt(wm, aPoint, aLine->mContainerSize);
   bool canSkipBr = false;
+  bool lastFrameWasEditable = false;
   for (int32_t n = aLine->GetChildCount(); n;
        --n, frame = frame->GetNextSibling()) {
     // Skip brFrames. Can only skip if the line contains at least
-    // one selectable and non-empty frame before
+    // one selectable and non-empty frame before. Also, avoid skipping brs if
+    // the previous thing had a different editableness than us, since then we
+    // may end up not being able to select after it if the br is the last thing
+    // on the line.
     if (!SelfIsSelectable(frame, aFlags) || frame->IsEmpty() ||
-        (canSkipBr && frame->IsBrFrame())) {
+        (canSkipBr && frame->IsBrFrame() &&
+         lastFrameWasEditable == frame->GetContent()->IsEditable())) {
       continue;
     }
     canSkipBr = true;
+    lastFrameWasEditable = frame->GetContent() && frame->GetContent()->IsEditable();
     LogicalRect frameRect = LogicalRect(wm, frame->GetRect(),
                                         aLine->mContainerSize);
     if (pt.I(wm) >= frameRect.IStart(wm)) {
       if (pt.I(wm) < frameRect.IEnd(wm)) {
         return GetSelectionClosestFrameForChild(frame, aPoint, aFlags);
       }
       if (frameRect.IEnd(wm) >= closestIStart) {
         closestFromIStart = frame;
@@ -5170,54 +5126,48 @@ OffsetsForSingleFrame(nsIFrame* aFrame, 
   }
   offsets.associate =
       offsets.offset == range.start ? CARET_ASSOCIATE_AFTER : CARET_ASSOCIATE_BEFORE;
   return offsets;
 }
 
 static nsIFrame* AdjustFrameForSelectionStyles(nsIFrame* aFrame) {
   nsIFrame* adjustedFrame = aFrame;
-  for (nsIFrame* frame = aFrame; frame; frame = frame->GetParent())
-  {
+  for (nsIFrame* frame = aFrame; frame; frame = frame->GetParent()) {
     // These are the conditions that make all children not able to handle
     // a cursor.
     StyleUserSelect userSelect = frame->StyleUIReset()->mUserSelect;
-    if (userSelect == StyleUserSelect::MozText) {
-      // If we see a -moz-text element, we shouldn't look further up the parent
-      // chain!
+    if (userSelect != StyleUserSelect::Auto && userSelect != StyleUserSelect::All) {
       break;
     }
-    if (userSelect == StyleUserSelect::All ||
-        frame->IsGeneratedContentFrame()) {
+    if (userSelect == StyleUserSelect::All || frame->IsGeneratedContentFrame()) {
       adjustedFrame = frame;
     }
   }
   return adjustedFrame;
 }
 
 nsIFrame::ContentOffsets nsIFrame::GetContentOffsetsFromPoint(const nsPoint& aPoint,
                                                               uint32_t aFlags)
 {
   nsIFrame *adjustedFrame;
   if (aFlags & IGNORE_SELECTION_STYLE) {
     adjustedFrame = this;
-  }
-  else {
+  } else {
     // This section of code deals with special selection styles.  Note that
     // -moz-all exists, even though it doesn't need to be explicitly handled.
     //
     // The offset is forced not to end up in generated content; content offsets
     // cannot represent content outside of the document's content tree.
 
     adjustedFrame = AdjustFrameForSelectionStyles(this);
 
     // -moz-user-select: all needs special handling, because clicking on it
     // should lead to the whole frame being selected
-    if (adjustedFrame && adjustedFrame->StyleUIReset()->mUserSelect ==
-        StyleUserSelect::All) {
+    if (adjustedFrame->StyleUIReset()->mUserSelect == StyleUserSelect::All) {
       nsPoint adjustedPoint = aPoint + this->GetOffsetTo(adjustedFrame);
       return OffsetsForSingleFrame(adjustedFrame, adjustedPoint);
     }
 
     // For other cases, try to find a closest frame starting from the parent of
     // the unselectable frame
     if (adjustedFrame != this)
       adjustedFrame = adjustedFrame->GetParent();
@@ -8127,18 +8077,17 @@ nsFrame::GetChildFrameContainingOffset(i
 // aOutSideLimit != 0 means ignore aLineStart, instead work from
 // the end (if > 0) or beginning (if < 0).
 //
 nsresult
 nsFrame::GetNextPrevLineFromeBlockFrame(nsPresContext* aPresContext,
                                         nsPeekOffsetStruct *aPos,
                                         nsIFrame *aBlockFrame,
                                         int32_t aLineStart,
-                                        int8_t aOutSideLimit
-                                        )
+                                        int8_t aOutSideLimit)
 {
   //magic numbers aLineStart will be -1 for end of block 0 will be start of block
   if (!aBlockFrame || !aPos)
     return NS_ERROR_NULL_POINTER;
 
   aPos->mResultFrame = nullptr;
   aPos->mResultContent = nullptr;
   aPos->mAttach =
@@ -8236,16 +8185,29 @@ nsFrame::GetNextPrevLineFromeBlockFrame(
                                     false, // aVisual
                                     aPos->mScrollViewStop,
                                     false, // aFollowOOFs
                                     false // aSkipPopupChecks
                                     );
       if (NS_FAILED(result))
         return result;
 
+      auto FoundValidFrame = [aPos](const ContentOffsets& aOffsets, const nsIFrame* aFrame) {
+        if (!aOffsets.content) {
+          return false;
+        }
+        if (!aFrame->IsSelectable(nullptr)) {
+          return false;
+        }
+        if (aPos->mForceEditableRegion && !aOffsets.content->IsEditable()) {
+          return false;
+        }
+        return true;
+      };
+
       nsIFrame *storeOldResultFrame = resultFrame;
       while ( !found ){
         nsPoint point;
         nsRect tempRect = resultFrame->GetRect();
         nsPoint offset;
         nsView * view; //used for call of get offset from view
         resultFrame->GetOffsetFromView(offset, &view);
         if (!view) {
@@ -8289,32 +8251,28 @@ nsFrame::GetNextPrevLineFromeBlockFrame(
                   aPos->mResultFrame = resultFrame->GetParent();
                   return NS_POSITION_BEFORE_TABLE;
                 }
               }
             }
           }
         }
 
-        if (!resultFrame->HasView())
-        {
+        if (!resultFrame->HasView()) {
           nsView* view;
           nsPoint offset;
           resultFrame->GetOffsetFromView(offset, &view);
           ContentOffsets offsets =
               resultFrame->GetContentOffsetsFromPoint(point - offset);
           aPos->mResultContent = offsets.content;
           aPos->mContentOffset = offsets.offset;
           aPos->mAttach = offsets.associate;
-          if (offsets.content)
-          {
-            if (resultFrame->IsSelectable(nullptr)) {
-              found = true;
-              break;
-            }
+          if (FoundValidFrame(offsets, resultFrame)) {
+            found = true;
+            break;
           }
         }
 
         if (aPos->mDirection == eDirPrevious && (resultFrame == farStoppingFrame))
           break;
         if (aPos->mDirection == eDirNext && (resultFrame == nearStoppingFrame))
           break;
         //always try previous on THAT line if that fails go the other way
@@ -8341,26 +8299,23 @@ nsFrame::GetNextPrevLineFromeBlockFrame(
         nsView* view;
         nsPoint offset;
         resultFrame->GetOffsetFromView(offset, &view);
         ContentOffsets offsets =
             resultFrame->GetContentOffsetsFromPoint(point - offset);
         aPos->mResultContent = offsets.content;
         aPos->mContentOffset = offsets.offset;
         aPos->mAttach = offsets.associate;
-        if (offsets.content)
-        {
-          if (resultFrame->IsSelectable(nullptr)) {
-            found = true;
-            if (resultFrame == farStoppingFrame)
-              aPos->mAttach = CARET_ASSOCIATE_BEFORE;
-            else
-              aPos->mAttach = CARET_ASSOCIATE_AFTER;
-            break;
-          }
+        if (FoundValidFrame(offsets, resultFrame)) {
+          found = true;
+          if (resultFrame == farStoppingFrame)
+            aPos->mAttach = CARET_ASSOCIATE_BEFORE;
+          else
+            aPos->mAttach = CARET_ASSOCIATE_AFTER;
+          break;
         }
         if (aPos->mDirection == eDirPrevious && (resultFrame == nearStoppingFrame))
           break;
         if (aPos->mDirection == eDirNext && (resultFrame == farStoppingFrame))
           break;
         //previous didnt work now we try "next"
         frameTraversal->Next();
         nsIFrame *tempFrame = frameTraversal->CurrentItem();
@@ -8574,30 +8529,29 @@ nsIFrame::PeekOffset(nsPeekOffsetStruct*
 
         movedOverNonSelectableText |= (peekSearchState == CONTINUE_UNSELECTABLE);
 
         if (peekSearchState != FOUND) {
           bool movedOverNonSelectable = false;
           result =
             current->GetFrameFromDirection(aPos->mDirection, aPos->mVisual,
                                            aPos->mJumpLines, aPos->mScrollViewStop,
+                                           aPos->mForceEditableRegion,
                                            &current, &offset, &jumpedLine,
                                            &movedOverNonSelectable);
           if (NS_FAILED(result))
             return result;
 
           // If we jumped lines, it's as if we found a character, but we still need
           // to eat non-renderable content on the new line.
           if (jumpedLine)
             eatingNonRenderableWS = true;
 
           // Remember if we moved over non-selectable text when finding another frame.
-          if (movedOverNonSelectable) {
-            movedOverNonSelectableText = true;
-          }
+          movedOverNonSelectableText |= movedOverNonSelectable;
         }
 
         // Found frame, but because we moved over non selectable text we want the offset
         // to be at the frame edge. Note that if we are extending the selection, this
         // doesn't matter.
         if (peekSearchState == FOUND && movedOverNonSelectableText &&
             !aPos->mExtend)
         {
@@ -8677,16 +8631,17 @@ nsIFrame::PeekOffset(nsPeekOffsetStruct*
 
         if (!done) {
           nsIFrame* nextFrame;
           int32_t nextFrameOffset;
           bool jumpedLine, movedOverNonSelectableText;
           result =
             current->GetFrameFromDirection(aPos->mDirection, aPos->mVisual,
                                            aPos->mJumpLines, aPos->mScrollViewStop,
+                                           aPos->mForceEditableRegion,
                                            &nextFrame, &nextFrameOffset, &jumpedLine,
                                            &movedOverNonSelectableText);
           // We can't jump lines if we're looking for whitespace following
           // non-whitespace, and we already encountered non-whitespace.
           if (NS_FAILED(result) ||
               (jumpedLine && !wordSelectEatSpace && state.mSawBeforeType)) {
             done = true;
             // If we've crossed the line boundary, check to make sure that we
@@ -8846,27 +8801,33 @@ nsIFrame::PeekOffset(nsPeekOffsetStruct*
           if (frameIsRTL != lineIsRTL) {
             endOfLine = !endOfLine;
           }
         }
       } else {
         it->GetLine(thisLine, &firstFrame, &lineFrameCount, usedRect);
 
         nsIFrame* frame = firstFrame;
+        bool lastFrameWasEditable = false;
         for (int32_t count = lineFrameCount; count;
              --count, frame = frame->GetNextSibling()) {
-          if (!frame->IsGeneratedContentFrame()) {
-            // When jumping to the end of the line with the "end" key,
-            // skip over brFrames
-            if (endOfLine && lineFrameCount > 1 && frame->IsBrFrame()) {
-              continue;
-            }
-            baseFrame = frame;
-            if (!endOfLine)
-              break;
+          if (frame->IsGeneratedContentFrame()) {
+            continue;
+          }
+          // When jumping to the end of the line with the "end" key,
+          // try to skip over brFrames
+          if (endOfLine && lineFrameCount > 1 && frame->IsBrFrame() &&
+              lastFrameWasEditable == frame->GetContent()->IsEditable()) {
+            continue;
+          }
+          lastFrameWasEditable =
+            frame->GetContent() && frame->GetContent()->IsEditable();
+          baseFrame = frame;
+          if (!endOfLine) {
+            break;
           }
         }
       }
       if (!baseFrame)
         return NS_ERROR_FAILURE;
       FrameTarget targetFrame = DrillDownToSelectionFrame(baseFrame,
                                                           endOfLine, 0);
       FrameContentRange range = GetRangeForFrame(targetFrame.frame);
@@ -9034,16 +8995,17 @@ nsFrame::GetLineNumber(nsIFrame *aFrame,
   if (aContainingBlock)
     *aContainingBlock = blockFrame;
   return it->FindLineContaining(thisBlock);
 }
 
 nsresult
 nsIFrame::GetFrameFromDirection(nsDirection aDirection, bool aVisual,
                                 bool aJumpLines, bool aScrollViewStop,
+                                bool aForceEditableRegion,
                                 nsIFrame** aOutFrame, int32_t* aOutOffset,
                                 bool* aOutJumpedLine, bool* aOutMovedOverNonSelectableText)
 {
   nsresult result;
 
   if (!aOutFrame || !aOutOffset || !aOutJumpedLine)
     return NS_ERROR_NULL_POINTER;
 
@@ -9138,34 +9100,41 @@ nsIFrame::GetFrameFromDirection(nsDirect
 
     // Skip anonymous elements, but watch out for generated content
     if (!traversedFrame ||
         (!traversedFrame->IsGeneratedContentFrame() &&
          traversedFrame->GetContent()->IsRootOfNativeAnonymousSubtree())) {
       return NS_ERROR_FAILURE;
     }
 
-    // Skip brFrames, but only if they are not the only frame in the line
-    if (atLineEdge && aDirection == eDirPrevious &&
-        traversedFrame->IsBrFrame()) {
-      int32_t lineFrameCount;
-      nsIFrame *currentBlockFrame, *currentFirstFrame;
-      nsRect usedRect;
-      int32_t currentLine = nsFrame::GetLineNumber(traversedFrame, aScrollViewStop, &currentBlockFrame);
-      nsAutoLineIterator iter = currentBlockFrame->GetLineIterator();
-      result = iter->GetLine(currentLine, &currentFirstFrame, &lineFrameCount, usedRect);
-      if (NS_FAILED(result)) {
-        return result;
-      }
-      if (lineFrameCount > 1) {
+    auto IsSelectable = [aForceEditableRegion](const nsIFrame* aFrame) {
+      if (!aFrame->IsSelectable(nullptr)) {
+        return false;
+      }
+      return !aForceEditableRegion || aFrame->GetContent()->IsEditable();
+    };
+
+    // Skip brFrames, but only we can select something before hitting the end of
+    // the line or a non-selectable region.
+    if (atLineEdge && aDirection == eDirPrevious && traversedFrame->IsBrFrame()) {
+      bool canSkipBr = false;
+      for (nsIFrame* current = traversedFrame->GetPrevSibling();
+           current;
+           current = current->GetPrevSibling()) {
+        if (IsSelectable(current)) {
+          canSkipBr = true;
+          break;
+        }
+      }
+      if (canSkipBr) {
         continue;
       }
     }
 
-    selectable = traversedFrame->IsSelectable(nullptr);
+    selectable = IsSelectable(traversedFrame);
     if (!selectable) {
       *aOutMovedOverNonSelectableText = true;
     }
   } // while (!selectable)
 
   *aOutOffset = (aDirection == eDirNext) ? 0 : -1;
 
   if (aVisual && IsReversedDirectionFrame(traversedFrame)) {
--- a/layout/generic/nsFrameSelection.cpp
+++ b/layout/generic/nsFrameSelection.cpp
@@ -114,27 +114,29 @@ nsPeekOffsetStruct::nsPeekOffsetStruct(n
                                        nsDirection aDirection,
                                        int32_t aStartOffset,
                                        nsPoint aDesiredPos,
                                        bool aJumpLines,
                                        bool aScrollViewStop,
                                        bool aIsKeyboardSelect,
                                        bool aVisual,
                                        bool aExtend,
+                                       ForceEditableRegion aForceEditableRegion,
                                        EWordMovementType aWordMovementType)
   : mAmount(aAmount)
   , mDirection(aDirection)
   , mStartOffset(aStartOffset)
   , mDesiredPos(aDesiredPos)
   , mWordMovementType(aWordMovementType)
   , mJumpLines(aJumpLines)
   , mScrollViewStop(aScrollViewStop)
   , mIsKeyboardSelect(aIsKeyboardSelect)
   , mVisual(aVisual)
   , mExtend(aExtend)
+  , mForceEditableRegion(aForceEditableRegion == ForceEditableRegion::Yes)
   , mResultContent()
   , mResultFrame(nullptr)
   , mContentOffset(0)
   , mAttach(CARET_ASSOCIATE_BEFORE)
 {
 }
 
 // Array which contains index of each SelecionType in Selection::mDOMSelections.
@@ -701,22 +703,18 @@ nsFrameSelection::MoveCaret(nsDirection 
   nsPoint desiredPos(0, 0); //we must keep this around and revalidate it when its just UP/DOWN
 
   int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
   RefPtr<Selection> sel = mDomSelections[index];
   if (!sel)
     return NS_ERROR_NULL_POINTER;
 
   int32_t scrollFlags = Selection::SCROLL_FOR_CARET_MOVE;
-  nsINode* focusNode = sel->GetFocusNode();
-  if (focusNode &&
-      (focusNode->IsEditable() ||
-       (focusNode->IsElement() &&
-        focusNode->AsElement()->State().
-          HasState(NS_EVENT_STATE_MOZ_READWRITE)))) {
+  const bool isEditorSelection = sel->IsEditorSelection();
+  if (isEditorSelection) {
     // If caret moves in editor, it should cause scrolling even if it's in
     // overflow: hidden;.
     scrollFlags |= Selection::SCROLL_OVERFLOW_HIDDEN;
   }
 
   int32_t caretStyle = Preferences::GetInt("layout.selection.caret_style", 0);
   if (caretStyle == 0
 #ifdef XP_WIN
@@ -774,21 +772,24 @@ nsFrameSelection::MoveCaret(nsDirection 
   nsIFrame *frame;
   int32_t offsetused = 0;
   nsresult result = sel->GetPrimaryFrameForFocusNode(&frame, &offsetused,
                                                      visualMovement);
 
   if (NS_FAILED(result) || !frame)
     return NS_FAILED(result) ? result : NS_ERROR_FAILURE;
 
+  const auto forceEditableRegion = isEditorSelection
+    ? nsPeekOffsetStruct::ForceEditableRegion::Yes
+    : nsPeekOffsetStruct::ForceEditableRegion::No;
   //set data using mLimiter to stop on scroll views.  If we have a limiter then we stop peeking
   //when we hit scrollable views.  If no limiter then just let it go ahead
   nsPeekOffsetStruct pos(aAmount, eDirPrevious, offsetused, desiredPos,
                          true, mLimiter != nullptr, true, visualMovement,
-                         aContinueSelection);
+                         aContinueSelection, forceEditableRegion);
 
   nsBidiDirection paraDir = nsBidiPresUtils::ParagraphDirection(frame);
 
   CaretAssociateHint tHint(mHint); //temporary variable so we dont set mHint until it is necessary
   switch (aAmount){
    case eSelectCharacter:
     case eSelectCluster:
     case eSelectWord:
@@ -940,17 +941,17 @@ nsFrameSelection::GetPrevNextBidiLevels(
     levels.SetData(currentFrame, currentFrame, currentLevel, currentLevel);
     return levels;
   }
 
   nsIFrame *newFrame;
   int32_t offset;
   bool jumpedLine, movedOverNonSelectableText;
   nsresult rv = currentFrame->GetFrameFromDirection(direction, false,
-                                                    aJumpLines, true,
+                                                    aJumpLines, true, false,
                                                     &newFrame, &offset, &jumpedLine,
                                                     &movedOverNonSelectableText);
   if (NS_FAILED(rv))
     newFrame = nullptr;
 
   FrameBidiData currentBidi = currentFrame->GetBidiData();
   nsBidiLevel currentLevel = currentBidi.embeddingLevel;
   nsBidiLevel newLevel = newFrame ? newFrame->GetEmbeddingLevel()
--- a/layout/generic/nsFrameSelection.h
+++ b/layout/generic/nsFrameSelection.h
@@ -70,25 +70,32 @@ struct SelectionCustomColors
 class nsIPresShell;
 
 /** PeekOffsetStruct is used to group various arguments (both input and output)
  *  that are passed to nsFrame::PeekOffset(). See below for the description of
  *  individual arguments.
  */
 struct MOZ_STACK_CLASS nsPeekOffsetStruct
 {
+  enum class ForceEditableRegion
+  {
+    No,
+    Yes,
+  };
+
   nsPeekOffsetStruct(nsSelectionAmount aAmount,
                      nsDirection aDirection,
                      int32_t aStartOffset,
                      nsPoint aDesiredPos,
                      bool aJumpLines,
                      bool aScrollViewStop,
                      bool aIsKeyboardSelect,
                      bool aVisual,
                      bool aExtend,
+                     ForceEditableRegion = ForceEditableRegion::No,
                      mozilla::EWordMovementType aWordMovementType = mozilla::eDefaultBehavior);
 
   // Note: Most arguments (input and output) are only used with certain values
   // of mAmount. These values are indicated for each argument below.
   // Arguments with no such indication are used with all values of mAmount.
 
   /*** Input arguments ***/
   // Note: The value of some of the input arguments may be changed upon exit.
@@ -137,24 +144,28 @@ struct MOZ_STACK_CLASS nsPeekOffsetStruc
 
   // mVisual: Whether bidi caret behavior is visual (true) or logical (false).
   //          Used with: eSelectCharacter, eSelectWord, eSelectBeginLine, eSelectEndLine.
   bool mVisual;
 
   // mExtend: Whether the selection is being extended or moved.
   bool mExtend;
 
+  // mForceEditableRegion: If true, the offset has to end up in an editable
+  // node, otherwise we'll keep searching.
+  const bool mForceEditableRegion;
+
   /*** Output arguments ***/
 
   // mResultContent: Content reached as a result of the peek.
   nsCOMPtr<nsIContent> mResultContent;
 
   // mResultFrame: Frame reached as a result of the peek.
   //               Used with: eSelectCharacter, eSelectWord.
-  nsIFrame *mResultFrame;
+  nsIFrame* mResultFrame;
 
   // mContentOffset: Offset into content reached as a result of the peek.
   int32_t mContentOffset;
 
   // mAttachForward: When the result position is between two frames,
   //                 indicates which of the two frames the caret should be painted in.
   //                 false means "the end of the frame logically before the caret",
   //                 true means "the beginning of the frame logically after the caret".
--- a/layout/generic/nsIFrame.h
+++ b/layout/generic/nsIFrame.h
@@ -3283,16 +3283,17 @@ public:
    *  @param aOutOffset [out] 0 indicates that we arrived at the beginning of the output frame;
    *                          -1 indicates that we arrived at its end.
    *  @param aOutJumpedLine [out] whether this frame and the returned frame are on different lines
    *  @param aOutMovedOverNonSelectableText [out] whether we jumped over a non-selectable
    *                                              frame during the search
    */
   nsresult GetFrameFromDirection(nsDirection aDirection, bool aVisual,
                                  bool aJumpLines, bool aScrollViewStop,
+                                 bool aForceEditableRegion,
                                  nsIFrame** aOutFrame, int32_t* aOutOffset,
                                  bool* aOutJumpedLine, bool* aOutMovedOverNonSelectableText);
 
   /**
    *  called to see if the children of the frame are visible from indexstart to index end.
    *  this does not change any state. returns true only if the indexes are valid and any of
    *  the children are visible.  for textframes this index is the character index.
    *  if aStart = aEnd result will be false
--- a/layout/style/contenteditable.css
+++ b/layout/style/contenteditable.css
@@ -4,25 +4,16 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 @namespace url(http://www.w3.org/1999/xhtml); /* set default namespace to HTML */
 
 *|*::-moz-canvas {
   cursor: text;
 }
 
-*|*:-moz-read-write :-moz-read-only {
-  -moz-user-select: all;
-}
-
-*|*:-moz-read-only > :-moz-read-write {
-  /* override the above -moz-user-select: all rule. */
-  -moz-user-select: -moz-text;
-}
-
 input:-moz-read-write > .anonymous-div:-moz-read-only,
 textarea:-moz-read-write > .anonymous-div:-moz-read-only {
   -moz-user-select: text;
 }
 
 /* Use default arrow over objects with size that 
    are selected when clicked on.
    Override the browser's pointer cursor over links
@@ -95,17 +86,17 @@ input[contenteditable="true"][type="file
 
 *|*:-moz-read-write > input[type="hidden"],
 input[contenteditable="true"][type="hidden"] {
   border: 1px solid black !important;
   visibility: visible !important;
 }
 
 label:-moz-read-write {
-    -moz-user-select: all;
+  -moz-user-select: all;
 }
 
 *|*::-moz-display-comboboxcontrol-frame {
   -moz-user-select: text;
 }
 
 option:-moz-read-write {
   -moz-user-select: text;
--- a/servo/components/style/properties/longhands/ui.mako.rs
+++ b/servo/components/style/properties/longhands/ui.mako.rs
@@ -34,16 +34,17 @@
 ${helpers.predefined_type(
     "-moz-user-select",
     "UserSelect",
     "computed::UserSelect::Auto",
     products="gecko",
     gecko_ffi_name="mUserSelect",
     alias="-webkit-user-select",
     animation_value_type="discrete",
+    needs_context=False,
     spec="https://drafts.csswg.org/css-ui-4/#propdef-user-select",
 )}
 
 // TODO(emilio): This probably should be hidden from content.
 ${helpers.single_keyword(
     "-moz-window-dragging",
     "default drag no-drag",
     products="gecko",
--- a/servo/components/style/values/specified/ui.rs
+++ b/servo/components/style/values/specified/ui.rs
@@ -136,21 +136,16 @@ impl Parse for ScrollbarColor {
         }
         Ok(generics::ScrollbarColor::Colors {
             thumb: Color::parse(context, input)?,
             track: Color::parse(context, input)?,
         })
     }
 }
 
-fn in_ua_sheet(context: &ParserContext) -> bool {
-    use crate::stylesheets::Origin;
-    context.stylesheet_origin == Origin::UserAgent
-}
-
 /// The specified value for the `user-select` property.
 ///
 /// https://drafts.csswg.org/css-ui-4/#propdef-user-select
 #[allow(missing_docs)]
 #[derive(
     Clone,
     Copy,
     Debug,
@@ -163,20 +158,11 @@ fn in_ua_sheet(context: &ParserContext) 
     ToCss,
 )]
 #[repr(u8)]
 pub enum UserSelect {
     Auto,
     Text,
     #[parse(aliases = "-moz-none")]
     None,
-    /// Force selection of all children, unless an ancestor has `none` set.
+    /// Force selection of all children.
     All,
-    /// Like `text`, except that it won't get overridden by ancestors having
-    /// `all`.
-    ///
-    /// FIXME(emilio): This only has one use in contenteditable.css, can we find
-    /// a better way to do this?
-    ///
-    /// See bug 1181130.
-    #[parse(condition = "in_ua_sheet")]
-    MozText,
 }