Bug 1732845 - Add `nsINode::IsInDesignMode()` to check whether the node is directly in design mode r=smaug
authorMasayuki Nakano <masayuki@d-toybox.com>
Tue, 12 Oct 2021 03:14:43 +0000
changeset 595432 680ebfd6ee371e885e294af566f81e5bf17df6c0
parent 595431 1acc1364ea106d80a55d3db70d4077712fc2431c
child 595433 d5128422f25a2cad707e2571f300c0240b9828d7
push id38870
push userccozmuta@mozilla.com
push dateTue, 12 Oct 2021 09:32:00 +0000
treeherdermozilla-central@d51a3f460230 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs1732845
milestone95.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 1732845 - Add `nsINode::IsInDesignMode()` to check whether the node is directly in design mode r=smaug There are a lot of check of `Document`'s editable state **with** comments. This means that it's unclear for developers that only `Document` node is editable in design mode. Additionally, there are some points which use composed document rather than uncomposed document even though the raw API uses uncomposed document. Comparing with the other browsers, checking uncomposed document is compatible behavior, i.e., nodes in shadow trees are not editable unless `contenteditable`. Therefore, `nsINode` should have a method to check whether it's in design mode or not. Note that it may be called with a node in UA widget. Therefore, this patch adds new checks if it's in UA widget subtree or native anonymous subtree, checking whether it's in design mode with its host. Differential Revision: https://phabricator.services.mozilla.com/D126764
accessible/generic/DocAccessible.cpp
dom/base/Document.cpp
dom/base/FragmentOrElement.cpp
dom/base/Selection.cpp
dom/base/nsContentUtils.cpp
dom/base/nsFocusManager.cpp
dom/base/nsIContentInlines.h
dom/base/nsINode.cpp
dom/base/nsINode.h
dom/events/GlobalKeyListener.cpp
dom/events/IMEStateManager.cpp
dom/html/HTMLObjectElement.cpp
dom/html/nsGenericHTMLElement.cpp
dom/mathml/MathMLElement.cpp
dom/svg/SVGGraphicsElement.cpp
editor/libeditor/EditorBase.cpp
editor/libeditor/EditorEventListener.cpp
editor/libeditor/HTMLEditor.cpp
editor/libeditor/HTMLEditor.h
layout/base/nsLayoutUtils.cpp
testing/web-platform/tests/editing/include/editor-test-utils.js
testing/web-platform/tests/editing/other/editable-state-and-focus-in-shadow-dom-in-designMode.tentative.html
toolkit/components/sessionstore/SessionStoreDataCollector.cpp
toolkit/components/sessionstore/SessionStoreUtils.cpp
widget/tests/test_imestate.html
--- a/accessible/generic/DocAccessible.cpp
+++ b/accessible/generic/DocAccessible.cpp
@@ -23,16 +23,17 @@
 #include "TreeWalker.h"
 #include "xpcAccessibleDocument.h"
 
 #include "nsCommandManager.h"
 #include "nsContentUtils.h"
 #include "nsIDocShell.h"
 #include "mozilla/dom/Document.h"
 #include "nsPIDOMWindow.h"
+#include "nsIContentInlines.h"
 #include "nsIEditingSession.h"
 #include "nsIFrame.h"
 #include "nsIInterfaceRequestorUtils.h"
 #include "nsImageFrame.h"
 #include "nsViewManager.h"
 #include "nsIScrollableFrame.h"
 #include "nsUnicharUtils.h"
 #include "nsIURI.h"
@@ -292,17 +293,17 @@ void DocAccessible::TakeFocus() const {
   fm->MoveFocus(mDocumentNode->GetWindow(), nullptr,
                 nsFocusManager::MOVEFOCUS_ROOT, 0, getter_AddRefs(newFocus));
 }
 
 // HyperTextAccessible method
 already_AddRefed<EditorBase> DocAccessible::GetEditor() const {
   // Check if document is editable (designMode="on" case). Otherwise check if
   // the html:body (for HTML document case) or document element is editable.
-  if (!mDocumentNode->HasFlag(NODE_IS_EDITABLE) &&
+  if (!mDocumentNode->IsInDesignMode() &&
       (!mContent || !mContent->HasFlag(NODE_IS_EDITABLE))) {
     return nullptr;
   }
 
   nsCOMPtr<nsIDocShell> docShell = mDocumentNode->GetDocShell();
   if (!docShell) {
     return nullptr;
   }
--- a/dom/base/Document.cpp
+++ b/dom/base/Document.cpp
@@ -296,16 +296,17 @@
 #include "nsIBFCacheEntry.h"
 #include "nsIBaseWindow.h"
 #include "nsIBrowserChild.h"
 #include "nsIBrowserUsage.h"
 #include "nsICSSLoaderObserver.h"
 #include "nsICategoryManager.h"
 #include "nsICertOverrideService.h"
 #include "nsIContent.h"
+#include "nsIContentInlines.h"
 #include "nsIContentPolicy.h"
 #include "nsIContentSecurityPolicy.h"
 #include "nsIContentSink.h"
 #include "nsICookieJarSettings.h"
 #include "nsICookieService.h"
 #include "nsIDOMXULCommandDispatcher.h"
 #include "nsIDocShell.h"
 #include "nsIDocShellTreeItem.h"
@@ -4386,17 +4387,17 @@ bool Document::HasFocus(ErrorResult& rv)
   if (!fm->IsInActiveWindow(bc)) {
     return false;
   }
 
   return fm->IsSameOrAncestor(bc, fm->GetFocusedBrowsingContext());
 }
 
 void Document::GetDesignMode(nsAString& aDesignMode) {
-  if (HasFlag(NODE_IS_EDITABLE)) {
+  if (IsInDesignMode()) {
     aDesignMode.AssignLiteral("on");
   } else {
     aDesignMode.AssignLiteral("off");
   }
 }
 
 void Document::SetDesignMode(const nsAString& aDesignMode,
                              nsIPrincipal& aSubjectPrincipal, ErrorResult& rv) {
@@ -4419,17 +4420,17 @@ static void NotifyEditableStateChange(Do
 void Document::SetDesignMode(const nsAString& aDesignMode,
                              const Maybe<nsIPrincipal*>& aSubjectPrincipal,
                              ErrorResult& rv) {
   if (aSubjectPrincipal.isSome() &&
       !aSubjectPrincipal.value()->Subsumes(NodePrincipal())) {
     rv.Throw(NS_ERROR_DOM_PROP_ACCESS_DENIED);
     return;
   }
-  bool editableMode = HasFlag(NODE_IS_EDITABLE);
+  const bool editableMode = IsInDesignMode();
   if (aDesignMode.LowerCaseEqualsASCII(editableMode ? "off" : "on")) {
     SetEditableFlag(!editableMode);
     // Changing the NODE_IS_EDITABLE flags on document changes the intrinsic
     // state of all descendant elements of it. Update that now.
     NotifyEditableStateChange(*this);
     rv = EditingStateChanged();
   }
 }
@@ -5921,17 +5922,17 @@ nsresult Document::EditingStateChanged()
   }
 
   if (mEditingState == EditingState::eSettingUp ||
       mEditingState == EditingState::eTearingDown) {
     // XXX We shouldn't recurse
     return NS_OK;
   }
 
-  bool designMode = HasFlag(NODE_IS_EDITABLE);
+  const bool designMode = IsInDesignMode();
   EditingState newState =
       designMode ? EditingState::eDesignMode
                  : (mContentEditableCount > 0 ? EditingState::eContentEditable
                                               : EditingState::eOff);
   if (mEditingState == newState) {
     // No changes in editing mode.
     return NS_OK;
   }
@@ -7951,17 +7952,17 @@ void Document::DispatchContentLoadedEven
     mSetCompleteAfterDOMContentLoaded = false;
   }
 
   UnblockOnload(true);
 }
 
 void Document::EndLoad() {
   bool turnOnEditing =
-      mParser && (HasFlag(NODE_IS_EDITABLE) || mContentEditableCount > 0);
+      mParser && (IsInDesignMode() || mContentEditableCount > 0);
 
 #if defined(DEBUG)
   // only assert if nothing stopped the load on purpose
   if (!mParserAborted) {
     nsContentSecurityUtils::AssertAboutPageHasCSP(this);
   }
 #endif
 
--- a/dom/base/FragmentOrElement.cpp
+++ b/dom/base/FragmentOrElement.cpp
@@ -240,17 +240,17 @@ dom::Element* nsIContent::GetEditingHost
   }
 
   Document* doc = GetComposedDoc();
   if (!doc) {
     return nullptr;
   }
 
   // If this is in designMode, we should return <body>
-  if (doc->HasFlag(NODE_IS_EDITABLE) && !IsInShadowTree()) {
+  if (IsInDesignMode() && !IsInShadowTree()) {
     return doc->GetBodyElement();
   }
 
   nsIContent* content = this;
   for (dom::Element* parent = GetParentElement();
        parent && parent->HasFlag(NODE_IS_EDITABLE);
        parent = content->GetParentElement()) {
     content = parent;
--- a/dom/base/Selection.cpp
+++ b/dom/base/Selection.cpp
@@ -33,16 +33,17 @@
 #include "nsCOMPtr.h"
 #include "nsDebug.h"
 #include "nsString.h"
 #include "nsFrameSelection.h"
 #include "nsISelectionListener.h"
 #include "nsContentCID.h"
 #include "nsDeviceContext.h"
 #include "nsIContent.h"
+#include "nsIContentInlines.h"
 #include "nsRange.h"
 #include "nsITableCellLayout.h"
 #include "nsTArray.h"
 #include "nsTableWrapperFrame.h"
 #include "nsTableCellFrame.h"
 #include "nsIScrollableFrame.h"
 #include "nsCCUncollectableMarker.h"
 #include "nsIDocumentEncoder.h"
@@ -3047,17 +3048,17 @@ void Selection::StyledRanges::MaybeFocus
   Document* document = aPresShell->GetDocument();
   if (!document) {
     return;
   }
 
   nsPIDOMWindowOuter* window = document->GetWindow();
   // If the document is in design mode or doesn't have contenteditable
   // element, we don't need to move focus.
-  if (window && !document->HasFlag(NODE_IS_EDITABLE) &&
+  if (window && !document->IsInDesignMode() &&
       nsContentUtils::GetHTMLEditor(presContext)) {
     RefPtr<Element> newEditingHost = GetCommonEditingHost();
     RefPtr<nsFocusManager> fm = nsFocusManager::GetFocusManager();
     nsCOMPtr<nsPIDOMWindowOuter> focusedWindow;
     nsIContent* focusedContent = nsFocusManager::GetFocusedDescendant(
         window, nsFocusManager::eOnlyCurrentWindow,
         getter_AddRefs(focusedWindow));
     nsCOMPtr<Element> focusedElement = do_QueryInterface(focusedContent);
--- a/dom/base/nsContentUtils.cpp
+++ b/dom/base/nsContentUtils.cpp
@@ -7092,17 +7092,17 @@ EditorBase* nsContentUtils::GetActiveEdi
 // static
 EditorBase* nsContentUtils::GetActiveEditor(nsPIDOMWindowOuter* aWindow) {
   if (!aWindow || !aWindow->GetExtantDoc()) {
     return nullptr;
   }
 
   // If it's in designMode, nobody can have focus.  Therefore, the HTMLEditor
   // handles all events.  I.e., it's focused editor in this case.
-  if (aWindow->GetExtantDoc()->HasFlag(NODE_IS_EDITABLE)) {
+  if (aWindow->GetExtantDoc()->IsInDesignMode()) {
     return GetHTMLEditor(nsDocShell::Cast(aWindow->GetDocShell()));
   }
 
   // If focused element is associated with TextEditor, it must be <input>
   // element or <textarea> element.  Let's return it even if it's in a
   // contenteditable element.
   nsCOMPtr<nsPIDOMWindowOuter> focusedWindow;
   if (Element* focusedElement = nsFocusManager::GetFocusedDescendant(
--- a/dom/base/nsFocusManager.cpp
+++ b/dom/base/nsFocusManager.cpp
@@ -10,16 +10,17 @@
 
 #include "ChildIterator.h"
 #include "nsIInterfaceRequestorUtils.h"
 #include "nsGkAtoms.h"
 #include "nsGlobalWindow.h"
 #include "nsContentUtils.h"
 #include "ContentParent.h"
 #include "nsPIDOMWindow.h"
+#include "nsIContentInlines.h"
 #include "nsIDocShell.h"
 #include "nsIDocShellTreeOwner.h"
 #include "nsIFormControl.h"
 #include "nsLayoutUtils.h"
 #include "nsFrameTraversal.h"
 #include "nsIWebNavigation.h"
 #include "nsCaret.h"
 #include "nsIBaseWindow.h"
@@ -2021,26 +2022,29 @@ bool nsFocusManager::IsWindowVisible(nsP
   baseWin->GetVisibility(&visible);
   return visible;
 }
 
 bool nsFocusManager::IsNonFocusableRoot(nsIContent* aContent) {
   MOZ_ASSERT(aContent, "aContent must not be NULL");
   MOZ_ASSERT(aContent->IsInComposedDoc(), "aContent must be in a document");
 
-  // If aContent is in designMode, the root element is not focusable.
-  // NOTE: in designMode, most elements are not focusable, just the document is
-  //       focusable.
-  // Also, if aContent is not editable but it isn't in designMode, it's not
+  // If the uncomposed document of aContent is in designMode, the root element
+  // is not focusable.
+  // NOTE: Most elements whose uncomposed document is in design mode are not
+  //       focusable, just the document is focusable.  However, if it's in a
+  //       shadow tree, it may be focus able even if the shadow host is in
+  //       design mode.
+  // Also, if aContent is not editable and it's not in designMode, it's not
   // focusable.
   // And in userfocusignored context nothing is focusable.
   Document* doc = aContent->GetComposedDoc();
   NS_ASSERTION(doc, "aContent must have current document");
   return aContent == doc->GetRootElement() &&
-         (doc->HasFlag(NODE_IS_EDITABLE) || !aContent->IsEditable());
+         (aContent->IsInDesignMode() || !aContent->IsEditable());
 }
 
 Element* nsFocusManager::FlushAndCheckIfFocusable(Element* aElement,
                                                   uint32_t aFlags) {
   if (!aElement) {
     return nullptr;
   }
 
@@ -2587,20 +2591,23 @@ void nsFocusManager::Focus(
                                                    : CallerType::System);
     }
   }
 
   // if switching to a new document, first fire the focus event on the
   // document and then the window.
   if (aIsNewDocument) {
     Document* doc = aWindow->GetExtantDoc();
-    // The focus change should be notified to IMEStateManager from here if
-    // the focused element is a designMode editor since any content won't
+    // The focus change should be notified to IMEStateManager from here if:
+    // * the focused element is in design mode or
+    // * nobody gets focus and the document is in design mode
+    // since any element whose uncomposed document is in design mode won't
     // receive focus event.
-    if (doc && doc->HasFlag(NODE_IS_EDITABLE)) {
+    if (doc && ((aElement && aElement->IsInDesignMode()) ||
+                (!aElement && doc->IsInDesignMode()))) {
       IMEStateManager::OnChangeFocus(presShell->GetPresContext(), nullptr,
                                      GetFocusMoveActionCause(aFlags));
     }
     if (doc && !focusInOtherContentProcess) {
       SendFocusOrBlurEvent(eFocus, presShell, doc, ToSupports(doc),
                            aWindowRaised);
     }
     if (GetFocusedBrowsingContext() == aWindow->GetBrowsingContext() &&
--- a/dom/base/nsIContentInlines.h
+++ b/dom/base/nsIContentInlines.h
@@ -155,21 +155,69 @@ inline bool nsINode::IsEditable() const 
 
   // All editable anonymous content should be made explicitly editable via the
   // NODE_IS_EDITABLE flag.
   if (IsInNativeAnonymousSubtree()) {
     return false;
   }
 
   // Check if the node is in a document and the document is in designMode.
-  //
+  return IsInDesignMode();
+}
+
+inline bool nsINode::IsInDesignMode() const {
+  if (!OwnerDoc()->HasFlag(NODE_IS_EDITABLE)) {
+    return false;
+  }
+
+  if (IsDocument()) {
+    return HasFlag(NODE_IS_EDITABLE);
+  }
+
   // NOTE(emilio): If you change this to be the composed doc you also need to
   // change NotifyEditableStateChange() in Document.cpp.
-  Document* doc = GetUncomposedDoc();
-  return doc && doc->HasFlag(NODE_IS_EDITABLE);
+  // NOTE(masayuki): Perhaps, we should keep this behavior because of
+  // web-compat.
+  if (IsInUncomposedDoc() && GetUncomposedDoc()->HasFlag(NODE_IS_EDITABLE)) {
+    return true;
+  }
+
+  // FYI: In design mode, form controls don't work as usual.  For example,
+  //      <input type=text> isn't focusable but can be deleted and replaced
+  //      with typed text. <select> is also not focusable but always selected
+  //      all to be deleted or replaced.  On the other hand, newer controls
+  //      don't behave as the traditional controls.  For example, data/time
+  //      picker can be opened and change the value from the picker.  And also
+  //      the buttons of <video controls> work as usual.  On the other hand,
+  //      their UI (i.e., nodes in their shadow tree) are not editable.
+  //      Therefore, we need special handling for nodes in anonymous subtree
+  //      unless we fix <https://bugzilla.mozilla.org/show_bug.cgi?id=1734512>.
+
+  // If the shadow host is not in design mode, this can never be in design
+  // mode.  Otherwise, the content is never editable by design mode of
+  // composed document.
+  if (IsInUAWidget()) {
+    nsIContent* host = GetContainingShadowHost();
+    MOZ_DIAGNOSTIC_ASSERT(host != this);
+    return host && host->IsInDesignMode();
+  }
+  MOZ_ASSERT(!IsUAWidget());
+
+  // If we're in a native anonymous subtree, we should consider it with the
+  // host.
+  if (IsInNativeAnonymousSubtree()) {
+    nsIContent* host = GetClosestNativeAnonymousSubtreeRootParent();
+    MOZ_DIAGNOSTIC_ASSERT(host != this);
+    return host && host->IsInDesignMode();
+  }
+
+  // Otherwise, i.e., when it's in a shadow tree which is not created by us,
+  // the node is not editable by design mode (but it's possible that it may be
+  // editable if this node is in `contenteditable` element in the shadow tree).
+  return false;
 }
 
 inline void nsIContent::HandleInsertionToOrRemovalFromSlot() {
   using mozilla::dom::HTMLSlotElement;
 
   MOZ_ASSERT(GetParentElement());
   if (!IsInShadowTree() || IsRootOfNativeAnonymousSubtree()) {
     return;
--- a/dom/base/nsINode.cpp
+++ b/dom/base/nsINode.cpp
@@ -553,18 +553,17 @@ nsIContent* nsINode::GetSelectionRootCon
     }
   }
 
   nsPresContext* presContext = aPresShell->GetPresContext();
   if (presContext) {
     HTMLEditor* htmlEditor = nsContentUtils::GetHTMLEditor(presContext);
     if (htmlEditor) {
       // This node is in HTML editor.
-      Document* doc = GetComposedDoc();
-      if (!doc || doc->HasFlag(NODE_IS_EDITABLE) ||
+      if (!IsInComposedDoc() || IsInDesignMode() ||
           !HasFlag(NODE_IS_EDITABLE)) {
         nsIContent* editorRoot = htmlEditor->GetRoot();
         NS_ENSURE_TRUE(editorRoot, nullptr);
         return nsContentUtils::IsInSameAnonymousTree(this, editorRoot)
                    ? editorRoot
                    : GetRootForContentSubtree(AsContent());
       }
       // If the document isn't editable but this is editable, this is in
--- a/dom/base/nsINode.h
+++ b/dom/base/nsINode.h
@@ -1288,16 +1288,28 @@ class nsINode : public mozilla::dom::Eve
     } else {
       UnsetFlags(NODE_IS_EDITABLE);
     }
   }
 
   inline bool IsEditable() const;
 
   /**
+   * Check if this node is in design mode or not.  When this returns true and:
+   * - if this is a Document node, it's the design mode root.
+   * - if this is a content node, it's connected, it's not in a shadow tree
+   *   (except shadow tree for UI widget and native anonymous subtree) and its
+   *   uncomposed document is in design mode.
+   * Note that returning true does NOT mean the node or its children is
+   * editable.  E.g., when this node is in a shadow tree of a UA widget and its
+   * host is in design mode.
+   */
+  inline bool IsInDesignMode() const;
+
+  /**
    * Returns true if |this| or any of its ancestors is native anonymous.
    */
   bool IsInNativeAnonymousSubtree() const {
     return HasFlag(NODE_IS_IN_NATIVE_ANONYMOUS_SUBTREE);
   }
 
   /**
    * If |this| or any ancestor is native anonymous, return the root of the
--- a/dom/events/GlobalKeyListener.cpp
+++ b/dom/events/GlobalKeyListener.cpp
@@ -22,16 +22,17 @@
 #include "mozilla/dom/EventBinding.h"
 #include "mozilla/dom/KeyboardEvent.h"
 #include "nsAtom.h"
 #include "nsCOMPtr.h"
 #include "nsContentUtils.h"
 #include "nsFocusManager.h"
 #include "nsGkAtoms.h"
 #include "nsIContent.h"
+#include "nsIContentInlines.h"
 #include "nsIDocShell.h"
 #include "nsNetUtil.h"
 #include "nsPIDOMWindow.h"
 
 namespace mozilla {
 
 using namespace mozilla::layers;
 
@@ -684,18 +685,17 @@ bool RootWindowGlobalKeyListener::IsHTML
     return false;
   }
 
   HTMLEditor* htmlEditor = docShell->GetHTMLEditor();
   if (!htmlEditor) {
     return false;
   }
 
-  dom::Document* doc = htmlEditor->GetDocument();
-  if (doc->HasFlag(NODE_IS_EDITABLE)) {
+  if (htmlEditor->IsInDesignMode()) {
     // Don't need to perform any checks in designMode documents.
     return true;
   }
 
   nsINode* focusedNode = fm->GetFocusedElement();
   if (focusedNode && focusedNode->IsElement()) {
     // If there is a focused element, make sure it's in the active editing host.
     // Note that GetActiveEditingHost finds the current editing host based on
--- a/dom/events/IMEStateManager.cpp
+++ b/dom/events/IMEStateManager.cpp
@@ -19,29 +19,30 @@
 #include "mozilla/StaticPrefs_dom.h"
 #include "mozilla/StaticPrefs_intl.h"
 #include "mozilla/TextComposition.h"
 #include "mozilla/TextEvents.h"
 #include "mozilla/ToString.h"
 #include "mozilla/Unused.h"
 #include "mozilla/dom/BrowserBridgeChild.h"
 #include "mozilla/dom/BrowserParent.h"
+#include "mozilla/dom/Document.h"
 #include "mozilla/dom/HTMLFormElement.h"
 #include "mozilla/dom/HTMLTextAreaElement.h"
 #include "mozilla/dom/MouseEventBinding.h"
 #include "mozilla/dom/UserActivation.h"
 
 #include "HTMLInputElement.h"
 #include "IMEContentObserver.h"
 
 #include "nsCOMPtr.h"
 #include "nsContentUtils.h"
 #include "nsFocusManager.h"
 #include "nsIContent.h"
-#include "mozilla/dom/Document.h"
+#include "nsIContentInlines.h"
 #include "nsIFormControl.h"
 #include "nsINode.h"
 #include "nsISupports.h"
 #include "nsPresContext.h"
 
 namespace mozilla {
 
 using namespace dom;
@@ -1125,18 +1126,18 @@ IMEState IMEStateManager::GetNewIMEState
             ("  GetNewIMEState() returns IMEEnabled::Disabled because "
              "menu keyboard listener was installed"));
     return IMEState(IMEEnabled::Disabled);
   }
 
   if (!aContent) {
     // Even if there are no focused content, the focused document might be
     // editable, such case is design mode.
-    Document* doc = aPresContext->Document();
-    if (doc && doc->HasFlag(NODE_IS_EDITABLE)) {
+    if (aPresContext->Document() &&
+        aPresContext->Document()->IsInDesignMode()) {
       MOZ_LOG(sISMLog, LogLevel::Debug,
               ("  GetNewIMEState() returns IMEEnabled::Enabled because "
                "design mode editor has focus"));
       return IMEState(IMEEnabled::Enabled);
     }
     MOZ_LOG(sISMLog, LogLevel::Debug,
             ("  GetNewIMEState() returns IMEEnabled::Disabled because "
              "no content has focus"));
@@ -1934,21 +1935,19 @@ nsINode* IMEStateManager::GetRootEditabl
       if (node->IsContent() && node->AsContent()->HasIndependentSelection()) {
         return node;
       }
       root = node;
       node = node->GetParentNode();
     }
     return root;
   }
-  if (aPresContext) {
-    Document* document = aPresContext->Document();
-    if (document && document->IsEditable()) {
-      return document;
-    }
+  if (aPresContext && aPresContext->Document() &&
+      aPresContext->Document()->IsInDesignMode()) {
+    return aPresContext->Document();
   }
   return nullptr;
 }
 
 // static
 bool IMEStateManager::IsIMEObserverNeeded(const IMEState& aState) {
   return aState.IsEditable();
 }
--- a/dom/html/HTMLObjectElement.cpp
+++ b/dom/html/HTMLObjectElement.cpp
@@ -1,24 +1,25 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* 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 "mozilla/EventStates.h"
 #include "mozilla/dom/BindContext.h"
+#include "mozilla/dom/Document.h"
 #include "mozilla/dom/HTMLObjectElement.h"
 #include "mozilla/dom/HTMLObjectElementBinding.h"
 #include "mozilla/dom/ElementInlines.h"
 #include "mozilla/dom/WindowProxyHolder.h"
 #include "nsAttrValueInlines.h"
 #include "nsGkAtoms.h"
 #include "nsError.h"
-#include "mozilla/dom/Document.h"
+#include "nsIContentInlines.h"
 #include "nsIWidget.h"
 #include "nsContentUtils.h"
 #ifdef XP_MACOSX
 #  include "mozilla/EventDispatcher.h"
 #  include "mozilla/dom/Event.h"
 #  include "nsFocusManager.h"
 #endif
 
@@ -159,17 +160,17 @@ nsresult HTMLObjectElement::AfterMaybeCh
   return NS_OK;
 }
 
 bool HTMLObjectElement::IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable,
                                         int32_t* aTabIndex) {
   // TODO: this should probably be managed directly by IsHTMLFocusable.
   // See bug 597242.
   Document* doc = GetComposedDoc();
-  if (!doc || doc->HasFlag(NODE_IS_EDITABLE)) {
+  if (!doc || IsInDesignMode()) {
     if (aTabIndex) {
       *aTabIndex = -1;
     }
 
     *aIsFocusable = false;
     return false;
   }
 
--- a/dom/html/nsGenericHTMLElement.cpp
+++ b/dom/html/nsGenericHTMLElement.cpp
@@ -2133,17 +2133,17 @@ bool nsGenericHTMLElement::IsHTMLFocusab
   if (ShadowRoot* root = GetShadowRoot()) {
     if (root->DelegatesFocus()) {
       *aIsFocusable = false;
       return true;
     }
   }
 
   Document* doc = GetComposedDoc();
-  if (!doc || doc->HasFlag(NODE_IS_EDITABLE)) {
+  if (!doc || IsInDesignMode()) {
     // In designMode documents we only allow focusing the document.
     if (aTabIndex) {
       *aTabIndex = -1;
     }
 
     *aIsFocusable = false;
 
     return true;
@@ -2320,36 +2320,34 @@ void nsGenericHTMLElement::RecompileScri
 
     nsAutoString value;
     GetAttr(kNameSpaceID_None, attr, value);
     SetEventHandler(GetEventNameForAttr(attr), value, true);
   }
 }
 
 bool nsGenericHTMLElement::IsEditableRoot() const {
-  Document* document = GetComposedDoc();
-  if (!document) {
+  if (!IsInComposedDoc()) {
     return false;
   }
 
-  if (document->HasFlag(NODE_IS_EDITABLE)) {
+  if (IsInDesignMode()) {
     return false;
   }
 
   if (GetContentEditableValue() != eTrue) {
     return false;
   }
 
   nsIContent* parent = GetParent();
 
   return !parent || !parent->HasFlag(NODE_IS_EDITABLE);
 }
 
-static void MakeContentDescendantsEditable(nsIContent* aContent,
-                                           Document* aDocument) {
+static void MakeContentDescendantsEditable(nsIContent* aContent) {
   // If aContent is not an element, we just need to update its
   // 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;
   }
@@ -2358,48 +2356,48 @@ static void MakeContentDescendantsEditab
 
   element->UpdateEditableState(true);
 
   for (nsIContent* child = aContent->GetFirstChild(); child;
        child = child->GetNextSibling()) {
     if (!child->IsElement() ||
         !child->AsElement()->HasAttr(kNameSpaceID_None,
                                      nsGkAtoms::contenteditable)) {
-      MakeContentDescendantsEditable(child, aDocument);
+      MakeContentDescendantsEditable(child);
     }
   }
 }
 
 void nsGenericHTMLElement::ChangeEditableState(int32_t aChange) {
   Document* document = GetComposedDoc();
   if (!document) {
     return;
   }
 
   Document::EditingState previousEditingState = Document::EditingState::eOff;
   if (aChange != 0) {
     document->ChangeContentEditableCount(this, aChange);
     previousEditingState = document->GetEditingState();
   }
 
-  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.
   // We might as well wrap it all in one script blocker.
   nsAutoScriptBlocker scriptBlocker;
-  MakeContentDescendantsEditable(this, document);
+  MakeContentDescendantsEditable(this);
 
   // If the document already had contenteditable and JS adds new
   // contenteditable, that might cause changing editing host to current editing
   // host's ancestor.  In such case, HTMLEditor needs to know that
   // synchronously to update selection limitter.
-  if (document && aChange > 0 &&
+  // Additionally, elements in shadow DOM is not editable in the normal cases,
+  // but if its content has `contenteditable`, only in it can be ediable.
+  // So we don't need to notify HTMLEditor of this change only when we're not
+  // in shadow DOM and the composed document is in design mode.
+  if (IsInDesignMode() && !IsInShadowTree() && aChange > 0 &&
       previousEditingState == Document::EditingState::eContentEditable) {
     if (HTMLEditor* htmlEditor =
             nsContentUtils::GetHTMLEditor(document->GetPresContext())) {
       htmlEditor->NotifyEditingHostMaybeChanged();
     }
   }
 }
 
--- a/dom/mathml/MathMLElement.cpp
+++ b/dom/mathml/MathMLElement.cpp
@@ -835,18 +835,17 @@ void MathMLElement::SetIncrementScriptLe
 
 int32_t MathMLElement::TabIndexDefault() {
   nsCOMPtr<nsIURI> uri;
   return IsLink(getter_AddRefs(uri)) ? 0 : -1;
 }
 
 // XXX Bug 1586011: Share logic with other element classes.
 bool MathMLElement::IsFocusableInternal(int32_t* aTabIndex, bool aWithMouse) {
-  Document* doc = GetComposedDoc();
-  if (!doc || doc->HasFlag(NODE_IS_EDITABLE)) {
+  if (!IsInComposedDoc() || IsInDesignMode()) {
     // In designMode documents we only allow focusing the document.
     if (aTabIndex) {
       *aTabIndex = -1;
     }
     return false;
   }
 
   int32_t tabIndex = TabIndex();
--- a/dom/svg/SVGGraphicsElement.cpp
+++ b/dom/svg/SVGGraphicsElement.cpp
@@ -4,16 +4,18 @@
  * 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 "mozilla/dom/SVGGraphicsElement.h"
 
 #include "mozilla/dom/BindContext.h"
 #include "mozilla/dom/Document.h"
 
+#include "nsIContentInlines.h"
+
 namespace mozilla {
 namespace dom {
 
 //----------------------------------------------------------------------
 // nsISupports methods
 
 NS_IMPL_ADDREF_INHERITED(SVGGraphicsElement, SVGGraphicsElementBase)
 NS_IMPL_RELEASE_INHERITED(SVGGraphicsElement, SVGGraphicsElementBase)
@@ -28,18 +30,17 @@ NS_INTERFACE_MAP_END_INHERITING(SVGGraph
 SVGGraphicsElement::SVGGraphicsElement(
     already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
     : SVGGraphicsElementBase(std::move(aNodeInfo)) {}
 
 bool SVGGraphicsElement::IsSVGFocusable(bool* aIsFocusable,
                                         int32_t* aTabIndex) {
   // XXXedgar, maybe we could factor out the common code for SVG, HTML and
   // MathML elements, see bug 1586011.
-  Document* doc = GetComposedDoc();
-  if (!doc || doc->HasFlag(NODE_IS_EDITABLE)) {
+  if (!IsInComposedDoc() || IsInDesignMode()) {
     // In designMode documents we only allow focusing the document.
     if (aTabIndex) {
       *aTabIndex = -1;
     }
 
     *aIsFocusable = false;
 
     return true;
--- a/editor/libeditor/EditorBase.cpp
+++ b/editor/libeditor/EditorBase.cpp
@@ -91,16 +91,17 @@
 #include "nsDebug.h"                   // for NS_WARNING, etc.
 #include "nsError.h"                   // for NS_OK, etc.
 #include "nsFocusManager.h"            // for nsFocusManager
 #include "nsFrameSelection.h"          // for nsFrameSelection
 #include "nsGenericHTMLElement.h"      // for nsGenericHTMLElement
 #include "nsGkAtoms.h"                 // for nsGkAtoms, nsGkAtoms::dir
 #include "nsIClipboard.h"              // for nsIClipboard
 #include "nsIContent.h"                // for nsIContent
+#include "nsIContentInlines.h"         // for nsINode::IsInDesignMode()
 #include "nsIDocumentEncoder.h"        // for nsIDocumentEncoder
 #include "nsIDocumentStateListener.h"  // for nsIDocumentStateListener
 #include "nsIDocShell.h"               // for nsIDocShell
 #include "nsIEditActionListener.h"     // for nsIEditActionListener
 #include "nsIFrame.h"                  // for nsIFrame
 #include "nsIInlineSpellChecker.h"     // for nsIInlineSpellChecker, etc.
 #include "nsNameSpaceManager.h"        // for kNameSpaceID_None, etc.
 #include "nsINode.h"                   // for nsINode, etc.
@@ -4526,17 +4527,21 @@ nsresult EditorBase::HandleDropEvent(Dra
   }
 
   // Before inserting dropping content, we need to move focus for compatibility
   // with Chrome and firing "beforeinput" event on new editing host.
   RefPtr<Element> focusedElement, newFocusedElement;
   if (IsTextEditor()) {
     newFocusedElement = GetExposedRoot();
     focusedElement = IsActiveInDOMWindow() ? newFocusedElement : nullptr;
-  } else if (!AsHTMLEditor()->IsInDesignMode()) {
+  }
+  // TODO: We need to add automated tests when dropping something into an
+  //       editing host for contenteditable which is in a shadow DOM tree
+  //       and its host which is in design mode.
+  else if (!AsHTMLEditor()->IsInDesignMode()) {
     focusedElement = AsHTMLEditor()->GetActiveEditingHost();
     if (focusedElement &&
         droppedAt.GetContainerAsContent()->IsInclusiveDescendantOf(
             focusedElement)) {
       newFocusedElement = focusedElement;
     } else {
       newFocusedElement = droppedAt.GetContainerAsContent()->GetEditingHost();
     }
@@ -5224,18 +5229,17 @@ nsresult EditorBase::InitializeSelection
       NS_SUCCEEDED(rvIgnored),
       "nsISelectionController::SetCaretEnabled() failed, but ignored");
   // 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(
-      aFocusEventTargetNode.OwnerDoc()->HasFlag(NODE_IS_EDITABLE));
+  caret->SetIgnoreUserModify(aFocusEventTargetNode.IsInDesignMode());
 
   // Init selection
   rvIgnored =
       selectionController->SetSelectionFlags(nsISelectionDisplay::DISPLAY_ALL);
   NS_WARNING_ASSERTION(
       NS_SUCCEEDED(rvIgnored),
       "nsISelectionController::SetSelectionFlags() failed, but ignored");
 
--- a/editor/libeditor/EditorEventListener.cpp
+++ b/editor/libeditor/EditorEventListener.cpp
@@ -19,29 +19,30 @@
 #include "mozilla/TextEditor.h"            // for TextEditor
 #include "mozilla/TextEvents.h"            // for WidgetCompositionEvent
 #include "mozilla/dom/Element.h"           // for Element
 #include "mozilla/dom/Event.h"             // for Event
 #include "mozilla/dom/EventTarget.h"       // for EventTarget
 #include "mozilla/dom/MouseEvent.h"        // for MouseEvent
 #include "mozilla/dom/Selection.h"
 #include "nsAString.h"
-#include "nsCaret.h"         // for nsCaret
-#include "nsDebug.h"         // for NS_WARNING, etc.
-#include "nsFocusManager.h"  // for nsFocusManager
-#include "nsGkAtoms.h"       // for nsGkAtoms, nsGkAtoms::input
-#include "nsIContent.h"      // for nsIContent
-#include "nsIController.h"   // for nsIController
+#include "nsCaret.h"            // for nsCaret
+#include "nsDebug.h"            // for NS_WARNING, etc.
+#include "nsFocusManager.h"     // for nsFocusManager
+#include "nsGkAtoms.h"          // for nsGkAtoms, nsGkAtoms::input
+#include "nsIContent.h"         // for nsIContent
+#include "nsIContentInlines.h"  // for nsINode::IsInDesignMode()
+#include "nsIController.h"      // for nsIController
 #include "nsID.h"
 #include "mozilla/dom/DOMStringList.h"
 #include "mozilla/dom/DataTransfer.h"
 #include "mozilla/dom/DragEvent.h"
 #include "mozilla/dom/Document.h"  // for Document
 #include "nsIFormControl.h"        // for nsIFormControl, etc.
-#include "nsINode.h"               // for nsINode, ::NODE_IS_EDITABLE, etc.
+#include "nsINode.h"               // for nsINode, etc.
 #include "nsIWidget.h"             // for nsIWidget
 #include "nsLiteralString.h"       // for NS_LITERAL_STRING
 #include "nsPIWindowRoot.h"        // for nsPIWindowRoot
 #include "nsPrintfCString.h"       // for nsPrintfCString
 #include "nsRange.h"
 #include "nsServiceManagerUtils.h"  // for do_GetService
 #include "nsString.h"               // for nsAutoString
 #include "nsQueryObject.h"          // for do_QueryObject
@@ -276,22 +277,21 @@ nsPresContext* EditorEventListener::GetP
 
 nsIContent* EditorEventListener::GetFocusedRootContent() {
   MOZ_ASSERT(!DetachedFromEditor());
   nsCOMPtr<nsIContent> focusedContent = mEditorBase->GetFocusedContent();
   if (!focusedContent) {
     return nullptr;
   }
 
-  Document* composedDoc = focusedContent->GetComposedDoc();
-  if (NS_WARN_IF(!composedDoc)) {
+  if (MOZ_UNLIKELY(NS_WARN_IF(!focusedContent->IsInComposedDoc()))) {
     return nullptr;
   }
 
-  if (composedDoc->HasFlag(NODE_IS_EDITABLE)) {
+  if (focusedContent->IsInDesignMode()) {
     return nullptr;
   }
 
   return focusedContent;
 }
 
 bool EditorEventListener::EditorHasFocus() {
   MOZ_ASSERT(!DetachedFromEditor());
@@ -1117,18 +1117,17 @@ nsresult EditorEventListener::Focus(Inte
   EventTarget* target = aFocusEvent->GetOriginalDOMEventTarget();
   nsCOMPtr<nsINode> eventTargetNode = do_QueryInterface(target);
   if (NS_WARN_IF(!eventTargetNode)) {
     return NS_ERROR_UNEXPECTED;
   }
 
   // If the target is a document node but it's not editable, we should ignore
   // it because actual focused element's event is going to come.
-  if (eventTargetNode->IsDocument() &&
-      !eventTargetNode->HasFlag(NODE_IS_EDITABLE)) {
+  if (eventTargetNode->IsDocument() && !eventTargetNode->IsInDesignMode()) {
     return NS_OK;
   }
 
   if (eventTargetNode->IsContent()) {
     nsIContent* content =
         eventTargetNode->AsContent()->FindFirstNonChromeOnlyAccessContent();
     // XXX If the focus event target is a form control in contenteditable
     // element, perhaps, the parent HTML editor should do nothing by this
@@ -1189,16 +1188,18 @@ nsresult EditorEventListener::Blur(Inter
     return NS_OK;
   }
 
   Element* focusedElement = focusManager->GetFocusedElement();
   if (!focusedElement) {
     // If it's in the designMode, and blur occurs, the target must be the
     // window.  If a blur event is fired and the target is an element, it
     // must be delayed blur event at initializing the `HTMLEditor`.
+    // TODO: Add automated tests for checking the case that the target node
+    //       is in a shadow DOM tree whose host is in design mode.
     if (mEditorBase->IsHTMLEditor() &&
         mEditorBase->AsHTMLEditor()->IsInDesignMode()) {
       if (nsCOMPtr<Element> targetElement =
               do_QueryInterface(aBlurEvent->mTarget)) {
         return NS_OK;
       }
     }
     RefPtr<EditorBase> editorBase(mEditorBase);
@@ -1258,18 +1259,17 @@ bool EditorEventListener::ShouldHandleNa
     return false;
   }
 
   RefPtr<HTMLEditor> htmlEditor = HTMLEditor::GetFrom(mEditorBase);
   if (!htmlEditor) {
     return false;
   }
 
-  RefPtr<Document> doc = htmlEditor->GetDocument();
-  if (doc->HasFlag(NODE_IS_EDITABLE)) {
+  if (htmlEditor->IsInDesignMode()) {
     // Don't need to perform any checks in designMode documents.
     return true;
   }
 
   nsIContent* editingHost = htmlEditor->GetActiveEditingHost();
   if (!editingHost) {
     return false;
   }
--- a/editor/libeditor/HTMLEditor.cpp
+++ b/editor/libeditor/HTMLEditor.cpp
@@ -47,16 +47,17 @@
 #include "nsContentUtils.h"
 #include "nsCRT.h"
 #include "nsElementTable.h"
 #include "nsFocusManager.h"
 #include "nsGenericHTMLElement.h"
 #include "nsGkAtoms.h"
 #include "nsHTMLDocument.h"
 #include "nsIContent.h"
+#include "nsIContentInlines.h"
 #include "nsIEditActionListener.h"
 #include "nsIFrame.h"
 #include "nsIPrincipal.h"
 #include "nsISelectionController.h"
 #include "nsIURI.h"
 #include "nsIWidget.h"
 #include "nsNetUtil.h"
 #include "nsPresContext.h"
@@ -602,24 +603,22 @@ void HTMLEditor::UpdateRootElement() {
 Element* HTMLEditor::FindSelectionRoot(nsINode* aNode) const {
   if (NS_WARN_IF(!aNode)) {
     return nullptr;
   }
 
   MOZ_ASSERT(aNode->IsDocument() || aNode->IsContent(),
              "aNode must be content or document node");
 
-  Document* document = aNode->GetComposedDoc();
-  if (NS_WARN_IF(!document)) {
+  if (MOZ_UNLIKELY(NS_WARN_IF(!aNode->IsInComposedDoc()))) {
     return nullptr;
   }
 
-  if (aNode->IsInUncomposedDoc() &&
-      (document->HasFlag(NODE_IS_EDITABLE) || !aNode->IsContent())) {
-    return document->GetRootElement();
+  if (aNode->IsInDesignMode()) {
+    return GetDocument()->GetRootElement();
   }
 
   // XXX If we have readonly flag, shouldn't return the element which has
   // contenteditable="true"?  However, such case isn't there without chrome
   // permission script.
   if (IsReadonly()) {
     // We still want to allow selection in a readonly editor.
     return GetRoot();
@@ -636,16 +635,28 @@ Element* HTMLEditor::FindSelectionRoot(n
     return nullptr;
   }
 
   // For non-readonly editors we want to find the root of the editable subtree
   // containing aContent.
   return content->GetEditingHost();
 }
 
+bool HTMLEditor::IsInDesignMode() const {
+  // TODO: If active editing host is in a shadow tree, it means that we should
+  //       behave exactly same as contenteditable mode because shadow tree
+  //       content is not editable even if composed document is in design mode,
+  //       but contenteditable elements in shoadow trees are focusable and
+  //       their content is editable.  Changing this affects to drop event
+  //       handler and blur event handler, so please add new tests for them
+  //       when you change here.
+  Document* document = GetDocument();
+  return document && document->IsInDesignMode();
+}
+
 void HTMLEditor::CreateEventListeners() {
   // Don't create the handler twice
   if (!mEventListener) {
     mEventListener = new HTMLEditorEventListener();
   }
 }
 
 nsresult HTMLEditor::InstallEventListeners() {
@@ -5741,17 +5752,17 @@ nsIContent* HTMLEditor::GetFocusedConten
   }
 
   nsIContent* focusedContent = focusManager->GetFocusedElement();
 
   Document* document = GetDocument();
   if (NS_WARN_IF(!document)) {
     return nullptr;
   }
-  bool inDesignMode = document->HasFlag(NODE_IS_EDITABLE);
+  const bool inDesignMode = IsInDesignMode();
   if (!focusedContent) {
     // in designMode, nobody gets focus in most cases.
     if (inDesignMode && OurWindowHasFocus()) {
       return document->GetRootElement();
     }
     return nullptr;
   }
 
@@ -5779,30 +5790,30 @@ nsIContent* HTMLEditor::GetFocusedConten
   if (!focusedContent) {
     return nullptr;
   }
 
   Document* document = GetDocument();
   if (NS_WARN_IF(!document)) {
     return nullptr;
   }
-  return document->HasFlag(NODE_IS_EDITABLE) ? nullptr : focusedContent;
+  return IsInDesignMode() ? nullptr : focusedContent;
 }
 
 bool HTMLEditor::IsActiveInDOMWindow() const {
   nsFocusManager* focusManager = nsFocusManager::GetFocusManager();
   if (NS_WARN_IF(!focusManager)) {
     return false;
   }
 
   Document* document = GetDocument();
   if (NS_WARN_IF(!document)) {
     return false;
   }
-  bool inDesignMode = document->HasFlag(NODE_IS_EDITABLE);
+  const bool inDesignMode = IsInDesignMode();
 
   // If we're in designMode, we're always active in the DOM window.
   if (inDesignMode) {
     return true;
   }
 
   nsPIDOMWindowOuter* ourWindow = document->GetWindow();
   nsCOMPtr<nsPIDOMWindowOuter> win;
@@ -5825,17 +5836,17 @@ bool HTMLEditor::IsActiveInDOMWindow() c
 
 Element* HTMLEditor::GetActiveEditingHost(
     LimitInBodyElement aLimitInBodyElement /* = LimitInBodyElement::Yes */)
     const {
   Document* document = GetDocument();
   if (NS_WARN_IF(!document)) {
     return nullptr;
   }
-  if (document->HasFlag(NODE_IS_EDITABLE)) {
+  if (IsInDesignMode()) {
     return document->GetBodyElement();
   }
 
   // We're HTML editor for contenteditable
   AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
   if (NS_WARN_IF(!editActionData.CanHandle())) {
     return nullptr;
   }
@@ -5863,19 +5874,20 @@ Element* HTMLEditor::GetActiveEditingHos
                  (document->GetBodyElement() &&
                   nsContentUtils::ContentIsFlattenedTreeDescendantOf(
                       candidateEditingHost, document->GetBodyElement()))
              ? candidateEditingHost
              : document->GetBodyElement();
 }
 
 void HTMLEditor::NotifyEditingHostMaybeChanged() {
-  Document* document = GetDocument();
-  if (NS_WARN_IF(!document) ||
-      NS_WARN_IF(document->HasFlag(NODE_IS_EDITABLE))) {
+  // Note that even if the document is in design mode, a contenteditable element
+  // in a shadow tree is focusable.   Therefore, we may need to update editing
+  // host even when the document is in design mode.
+  if (MOZ_UNLIKELY(NS_WARN_IF(!GetDocument()))) {
     return;
   }
 
   // We're HTML editor for contenteditable
   AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
   if (NS_WARN_IF(!editActionData.CanHandle())) {
     return;
   }
@@ -5891,17 +5903,21 @@ void HTMLEditor::NotifyEditingHostMaybeC
   // Compute current editing host.
   nsIContent* editingHost = GetActiveEditingHost();
   if (NS_WARN_IF(!editingHost)) {
     return;
   }
 
   // Update selection ancestor limit if current editing host includes the
   // previous editing host.
-  if (ancestorLimiter->IsInclusiveDescendantOf(editingHost)) {
+  // Additionally, the editing host may be an element in shadow DOM and the
+  // shadow host is in designMode.  In this case, we need to set the editing
+  // host as the new selection limiter.
+  if (ancestorLimiter->IsInclusiveDescendantOf(editingHost) ||
+      (ancestorLimiter->IsInDesignMode() != editingHost->IsInDesignMode())) {
     // Note that don't call HTMLEditor::InitializeSelectionAncestorLimit()
     // here because it may collapse selection to the first editable node.
     EditorBase::InitializeSelectionAncestorLimit(*editingHost);
   }
 }
 
 EventTarget* HTMLEditor::GetDOMEventTarget() const {
   // Don't use getDocument here, because we have no way of knowing
@@ -6045,27 +6061,35 @@ bool HTMLEditor::IsAcceptableInputEvent(
     }
   }
 
   RefPtr<Document> document = GetDocument();
   if (NS_WARN_IF(!document)) {
     return false;
   }
 
-  if (document->HasFlag(NODE_IS_EDITABLE)) {
+  if (IsInDesignMode()) {
     // If this editor is in designMode and the event target is the document,
     // the event is for this editor.
     if (eventTargetNode->IsDocument()) {
       return eventTargetNode == document;
     }
     // Otherwise, check whether the event target is in this document or not.
     if (NS_WARN_IF(!eventTargetNode->IsContent())) {
       return false;
     }
-    return document == eventTargetNode->GetUncomposedDoc();
+    if (document == eventTargetNode->GetUncomposedDoc()) {
+      return true;
+    }
+    // If the event target is in a shadow tree, the content is not editable
+    // by default, but if the focused content is an editing host, we need to
+    // handle it as contenteditable mode.
+    if (!eventTargetNode->IsInShadowTree()) {
+      return false;
+    }
   }
 
   // This HTML editor is for contenteditable.  We need to check the validity
   // of the target.
   if (NS_WARN_IF(!eventTargetNode->IsContent())) {
     return false;
   }
 
--- a/editor/libeditor/HTMLEditor.h
+++ b/editor/libeditor/HTMLEditor.h
@@ -670,20 +670,17 @@ class HTMLEditor final : public EditorBa
    */
   enum class LimitInBodyElement { No, Yes };
   Element* GetActiveEditingHost(
       LimitInBodyElement aLimitInBodyElement = LimitInBodyElement::Yes) const;
 
   /**
    * Retruns true if we're in designMode.
    */
-  bool IsInDesignMode() const {
-    Document* document = GetDocument();
-    return document && document->HasFlag(NODE_IS_EDITABLE);
-  }
+  bool IsInDesignMode() const;
 
   /**
    * Basically, this always returns true if we're for `contenteditable` or
    * `designMode` editor in web apps.  However, e.g., Composer of SeaMonkey
    * can make the editor not tabbable.
    */
   bool IsTabbable() const { return IsInteractionAllowed(); }
 
--- a/layout/base/nsLayoutUtils.cpp
+++ b/layout/base/nsLayoutUtils.cpp
@@ -121,16 +121,17 @@
 #include "nsFontInflationData.h"
 #include "nsFontMetrics.h"
 #include "nsFrameList.h"
 #include "nsFrameSelection.h"
 #include "nsGenericHTMLElement.h"
 #include "nsGkAtoms.h"
 #include "nsICanvasRenderingContextInternal.h"
 #include "nsIContent.h"
+#include "nsIContentInlines.h"
 #include "nsIContentViewer.h"
 #include "nsIDocShell.h"
 #include "nsIFrameInlines.h"
 #include "nsIImageLoadingContent.h"
 #include "nsIInterfaceRequestorUtils.h"
 #include "nsIScrollableFrame.h"
 #include "nsIWidget.h"
 #include "nsListControlFrame.h"
@@ -7406,17 +7407,17 @@ SurfaceFromElementResult nsLayoutUtils::
 
   return SurfaceFromElement(imageLoader, aSurfaceFlags, aTarget);
 }
 
 /* static */
 Element* nsLayoutUtils::GetEditableRootContentByContentEditable(
     Document* aDocument) {
   // If the document is in designMode we should return nullptr.
-  if (!aDocument || aDocument->HasFlag(NODE_IS_EDITABLE)) {
+  if (!aDocument || aDocument->IsInDesignMode()) {
     return nullptr;
   }
 
   // contenteditable only works with HTML document.
   // XXXbz should this test IsHTMLOrXHTML(), or just IsHTML()?
   if (!aDocument->IsHTMLOrXHTML()) {
     return nullptr;
   }
--- a/testing/web-platform/tests/editing/include/editor-test-utils.js
+++ b/testing/web-platform/tests/editing/include/editor-test-utils.js
@@ -68,16 +68,37 @@ class EditorTestUtils {
     return this.sendKey(kHome, modifier);
   }
 
   sendEndKey(modifier) {
     const kEnd = "\uE010";
     return this.sendKey(kEnd, modifier);
   }
 
+  sendSelectAllShortcutKey() {
+    return this.sendKey(
+      "a",
+      (() => {
+        // Gecko for Linux defines only Alt-A as a shortcut key for select all,
+        // although in most environment, Ctrl-A works as so too, but it depends
+        // on the OS settings.
+        if (
+          this.window.navigator.userAgent.includes("Linux") &&
+          this.window.navigator.userAgent.includes("Gecko") &&
+          !this.window.navigator.userAgent.includes("KHTML")
+        ) {
+          return this.kAlt;
+        }
+        return this.window.navigator.platform.includes("Mac")
+          ? this.kMeta
+          : this.kControl;
+      })()
+    );
+  }
+
   // Similar to `setupDiv` in editing/include/tests.js, this method sets
   // innerHTML value of this.editingHost, and sets multiple selection ranges
   // specified with the markers.
   // - `[` specifies start boundary in a text node
   // - `{` specifies start boundary before a node
   // - `]` specifies end boundary in a text node
   // - `}` specifies end boundary after a node
   setupEditingHost(innerHTMLWithRangeMarkers) {
@@ -137,17 +158,17 @@ class EditorTestUtils {
             return null;
           }
           if (scanResult[0] === "}" || scanResult[0] === "]") {
             throw "An end marker is found before a start marker";
           }
           return {
             marker: scanResult[0],
             container: textNode,
-            offset: scanResult.index + offset
+            offset: scanResult.index + offset,
           };
         };
         if (startContainer.nodeType === Node.TEXT_NODE) {
           let scanResult = scanStartMakerInTextNode(
             startContainer,
             startOffset
           );
           if (scanResult !== null) {
@@ -176,17 +197,17 @@ class EditorTestUtils {
             return null;
           }
           if (scanResult[0] === "{" || scanResult[0] === "[") {
             throw "A start marker is found before an end marker";
           }
           return {
             marker: scanResult[0],
             container: textNode,
-            offset: scanResult.index + offset
+            offset: scanResult.index + offset,
           };
         };
         if (startContainer.nodeType === Node.TEXT_NODE) {
           let scanResult = scanEndMarkerInTextNode(startContainer, startOffset);
           if (scanResult !== null) {
             return scanResult;
           }
         }
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/editing/other/editable-state-and-focus-in-shadow-dom-in-designMode.tentative.html
@@ -0,0 +1,252 @@
+<!doctype html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Testing editable state and focus in shadow DOM in design mode</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="../include/editor-test-utils.js"></script>
+</head>
+<body>
+<h3>open</h3>
+<my-shadow data-mode="open"></my-shadow>
+<h3>closed</h3>
+<my-shadow data-mode="closed"></my-shadow>
+
+<script>
+"use strict";
+
+document.designMode = "on";
+const utils = new EditorTestUtils(document.body);
+
+class MyShadow extends HTMLElement {
+  #defaultInnerHTML =
+    "<style>:focus { outline: 3px red solid; }</style>" +
+    "<div>text" +
+      "<div contenteditable=\"\">editable</div>" +
+      "<object tabindex=\"0\">object</object>" +
+      "<p tabindex=\"0\">paragraph</p>" +
+    "</div>";
+  #shadowRoot;
+
+  constructor() {
+    super();
+    this.#shadowRoot = this.attachShadow({mode: this.getAttribute("data-mode")});
+    this.#shadowRoot.innerHTML = this.#defaultInnerHTML;
+  }
+
+  reset() {
+    this.#shadowRoot.innerHTML = this.#defaultInnerHTML;
+    this.#shadowRoot.querySelector("div").getBoundingClientRect();
+  }
+
+  focusText() {
+    this.focus();
+    const div = this.#shadowRoot.querySelector("div");
+    getSelection().collapse(div.firstChild || div, 0);
+  }
+
+  focusContentEditable() {
+    this.focus();
+    const contenteditable = this.#shadowRoot.querySelector("div[contenteditable]");
+    contenteditable.focus();
+    getSelection().collapse(contenteditable.firstChild || contenteditable, 0);
+  }
+
+  focusObject() {
+    this.focus();
+    this.#shadowRoot.querySelector("object[tabindex]").focus();
+  }
+
+  focusParagraph() {
+    this.focus();
+    const tabbableP = this.#shadowRoot.querySelector("p[tabindex]");
+    tabbableP.focus();
+    getSelection().collapse(tabbableP.firstChild || tabbableP, 0);
+  }
+
+  getInnerHTML() {
+    return this.#shadowRoot.innerHTML;
+  }
+
+  getDefaultInnerHTML() {
+    return this.#defaultInnerHTML;
+  }
+
+  getFocusedElementName() {
+    return this.#shadowRoot.querySelector(":focus")?.tagName.toLocaleLowerCase() || "";
+  }
+
+  getSelectedRange() {
+    // XXX There is no standardized way to retrieve selected ranges in
+    //     shadow trees, therefore, we use non-standardized API for now
+    //     since the main purpose of this test is checking the behavior of
+    //     selection changes in shadow trees, not checking the selection API.
+    const selection =
+      this.#shadowRoot.getSelection !== undefined
+        ? this.#shadowRoot.getSelection()
+        : getSelection();
+    return selection.getRangeAt(0);
+  }
+}
+
+customElements.define("my-shadow", MyShadow);
+
+function getRangeDescription(range) {
+  function getNodeDescription(node) {
+    if (!node) {
+      return "null";
+    }
+    switch (node.nodeType) {
+      case Node.TEXT_NODE:
+      case Node.COMMENT_NODE:
+      case Node.CDATA_SECTION_NODE:
+        return `${node.nodeName} "${node.data}"`;
+      case Node.ELEMENT_NODE:
+        return `<${node.nodeName.toLowerCase()}>`;
+      default:
+        return `${node.nodeName}`;
+    }
+  }
+  if (range === null) {
+    return "null";
+  }
+  if (range === undefined) {
+    return "undefined";
+  }
+  return range.startContainer == range.endContainer &&
+    range.startOffset == range.endOffset
+    ? `(${getNodeDescription(range.startContainer)}, ${range.startOffset})`
+    : `(${getNodeDescription(range.startContainer)}, ${
+        range.startOffset
+      }) - (${getNodeDescription(range.endContainer)}, ${range.endOffset})`;
+}
+
+promise_test(async () => {
+  await new Promise(resolve => addEventListener("load", resolve, {once: true}));
+  assert_true(true, "Load event is fired");
+}, "Waiting for load");
+
+/**
+ * The expected result of this test is based on Blink and Gecko's behavior.
+ */
+
+for (const mode of ["open", "closed"]) {
+  const host = document.querySelector(`my-shadow[data-mode=${mode}]`);
+  promise_test(async (t) => {
+    host.reset();
+    host.focusText();
+    test(() => {
+      assert_equals(
+        host.getFocusedElementName(),
+        "",
+        `No element should have focus after ${t.name}`
+      );
+    }, `Focus after ${t.name}`);
+    await utils.sendKey("A");
+    test(() => {
+      assert_equals(
+        host.getInnerHTML(),
+        host.getDefaultInnerHTML(),
+        `The shadow DOM shouldn't be modified after ${t.name}`
+      );
+    }, `Typing "A" after ${t.name}`);
+  }, `Collapse selection into text in the ${mode} shadow DOM`);
+
+  promise_test(async (t) => {
+    host.reset();
+    host.focusContentEditable();
+    test(() => {
+      assert_equals(
+        host.getFocusedElementName(),
+        "div",
+        `<div contenteditable> should have focus after ${t.name}`
+      );
+    }, `Focus after ${t.name}`);
+    await utils.sendKey("A");
+    test(() => {
+      assert_equals(
+        host.getInnerHTML(),
+        host.getDefaultInnerHTML().replace("<div contenteditable=\"\">", "<div contenteditable=\"\">A"),
+        `The shadow DOM shouldn't be modified after ${t.name}`
+      );
+    }, `Typing "A" after ${t.name}`);
+  }, `Collapse selection into text in <div contenteditable> in the ${mode} shadow DOM`);
+
+  promise_test(async (t) => {
+    host.reset();
+    host.focusObject();
+    test(() => {
+      assert_equals(
+        host.getFocusedElementName(),
+        "object",
+        `The <object> element should have focus after ${t.name}`
+      );
+    }, `Focus after ${t.name}`);
+    await utils.sendKey("A");
+    test(() => {
+      assert_equals(
+        host.getInnerHTML(),
+        host.getDefaultInnerHTML(),
+        `The shadow DOM shouldn't be modified after ${t.name}`
+      );
+    }, `Typing "A" after ${t.name}`);
+  }, `Set focus to <object> in the ${mode} shadow DOM`);
+
+  promise_test(async (t) => {
+    host.reset();
+    host.focusParagraph();
+    test(() => {
+      assert_equals(
+        host.getFocusedElementName(),
+        "p",
+        `The <p tabindex="0"> element should have focus after ${t.name}`
+      );
+    }, `Focus after ${t.name}`);
+    await utils.sendKey("A");
+    test(() => {
+      assert_equals(
+        host.getInnerHTML(),
+        host.getDefaultInnerHTML(),
+        `The shadow DOM shouldn't be modified after ${t.name}`
+      );
+    }, `Typing "A" after ${t.name}`);
+  }, `Set focus to <p tabindex="0"> in the ${mode} shadow DOM`);
+
+  promise_test(async (t) => {
+    host.reset();
+    host.focusParagraph();
+    await utils.sendSelectAllShortcutKey();
+    assert_in_array(
+      getRangeDescription(host.getSelectedRange()),
+      [
+        // Feel free to add reasonable select all result in the <my-shadow>.
+        "(#document-fragment, 0) - (#document-fragment, 2)",
+        "(#text \"text\", 0) - (#text \"paragraph\", 9)",
+      ],
+      `Only all children of the ${mode} shadow DOM should be selected`
+    );
+    getSelection().collapse(document.body, 0);
+  }, `SelectAll in the ${mode} shadow DOM`);
+
+  promise_test(async (t) => {
+    host.reset();
+    host.focusContentEditable();
+    await utils.sendSelectAllShortcutKey();
+    assert_in_array(
+      getRangeDescription(host.getSelectedRange()),
+      [
+        // Feel free to add reasonable select all result in the <div contenteditable>.
+        "(<div>, 0) - (<div>, 1)",
+        "(#text \"editable\", 0) - (#text \"editable\", 8)",
+      ]
+    );
+    getSelection().collapse(document.body, 0);
+  }, `SelectAll in the <div contenteditable> in the ${mode} shadow DOM`);
+}
+</script>
+</body>
+</html>
--- a/toolkit/components/sessionstore/SessionStoreDataCollector.cpp
+++ b/toolkit/components/sessionstore/SessionStoreDataCollector.cpp
@@ -13,16 +13,17 @@
 #include "mozilla/dom/Document.h"
 #include "mozilla/dom/Element.h"
 #include "mozilla/dom/sessionstore/SessionStoreTypes.h"
 #include "mozilla/dom/SessionStoreUtils.h"
 #include "mozilla/dom/WindowGlobalChild.h"
 #include "mozilla/dom/WindowGlobalParent.h"
 
 #include "nsGenericHTMLElement.h"
+#include "nsIContentInlines.h"
 
 namespace mozilla::dom {
 
 NS_IMPL_CYCLE_COLLECTION(SessionStoreDataCollector, mWindowChild, mTimer)
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SessionStoreDataCollector)
   NS_INTERFACE_MAP_ENTRY(nsITimerCallback)
   NS_INTERFACE_MAP_ENTRY(nsINamed)
@@ -121,17 +122,17 @@ void SessionStoreDataCollector::Collect(
 
   Maybe<sessionstore::FormData> maybeFormData;
   if (mInputChanged) {
     maybeFormData.emplace();
     auto& formData = maybeFormData.ref();
     uint32_t size = SessionStoreUtils::CollectFormData(document, formData);
 
     Element* body = document->GetBody();
-    if (document->HasFlag(NODE_IS_EDITABLE) && body) {
+    if (body && body->IsInDesignMode()) {
       IgnoredErrorResult result;
       body->GetInnerHTML(formData.innerHTML(), result);
       size += formData.innerHTML().Length();
       if (!result.Failed()) {
         formData.hasData() = true;
       }
     }
 
--- a/toolkit/components/sessionstore/SessionStoreUtils.cpp
+++ b/toolkit/components/sessionstore/SessionStoreUtils.cpp
@@ -30,16 +30,17 @@
 #include "mozilla/ipc/BackgroundUtils.h"
 #include "mozilla/ReverseIterator.h"
 #include "mozilla/UniquePtr.h"
 #include "nsCharSeparatedTokenizer.h"
 #include "nsContentList.h"
 #include "nsContentUtils.h"
 #include "nsFocusManager.h"
 #include "nsGlobalWindowOuter.h"
+#include "nsIContentInlines.h"
 #include "nsIDocShell.h"
 #include "nsIFormControl.h"
 #include "nsIScrollableFrame.h"
 #include "nsISHistory.h"
 #include "nsIXULRuntime.h"
 #include "nsPresContext.h"
 #include "nsPrintfCString.h"
 
@@ -937,17 +938,17 @@ static void CollectCurrentFormData(JSCon
   /* input element */
   SessionStoreUtils::CollectFromInputElement(aDocument, generatedCount, aCx,
                                              aRetVal);
   /* select element */
   SessionStoreUtils::CollectFromSelectElement(aDocument, generatedCount, aCx,
                                               aRetVal);
 
   Element* bodyElement = aDocument.GetBody();
-  if (aDocument.HasFlag(NODE_IS_EDITABLE) && bodyElement) {
+  if (bodyElement && bodyElement->IsInDesignMode()) {
     bodyElement->GetInnerHTML(aRetVal.SetValue().mInnerHTML.Construct(),
                               IgnoreErrors());
   }
 
   if (aRetVal.IsNull()) {
     return;
   }
 
@@ -1135,17 +1136,17 @@ static void SetSessionData(JSContext* aC
   } else {
     JS_ClearPendingException(aCx);
   }
 }
 
 MOZ_CAN_RUN_SCRIPT
 static void SetInnerHTML(Document& aDocument, const nsString& aInnerHTML) {
   RefPtr<Element> bodyElement = aDocument.GetBody();
-  if (aDocument.HasFlag(NODE_IS_EDITABLE) && bodyElement) {
+  if (bodyElement && bodyElement->IsInDesignMode()) {
     IgnoredErrorResult rv;
     bodyElement->SetInnerHTML(aInnerHTML, aDocument.NodePrincipal(), rv);
     if (!rv.Failed()) {
       nsContentUtils::DispatchInputEvent(bodyElement);
     }
   }
 }
 
--- a/widget/tests/test_imestate.html
+++ b/widget/tests/test_imestate.html
@@ -44,16 +44,20 @@
   <select id="select_multiple" multiple="multiple">
     <option value="option" selected="selected"/>
   </select><br/>
   <isindex id="isindex" prompt="isindex"/><br/>
 
   <!-- a element -->
   <a id="a_href" href="about:blank">a[href]</a><br/>
 
+  <!-- multimedia element -->
+  <audio id="audio_with_controls" controls=""></audio><br/>
+  <video id="video_with_controls" controls=""></video><br/>
+
   <!-- ime-mode test -->
   <input type="text" id="ime_mode_auto"     style="ime-mode: auto;"/><br/>
   <input type="text" id="ime_mode_normal"   style="ime-mode: normal;"/><br/>
   <input type="text" id="ime_mode_active"   style="ime-mode: active;"/><br/>
   <input type="text" id="ime_mode_inactive" style="ime-mode: inactive;"/><br/>
   <input type="text" id="ime_mode_disabled" style="ime-mode: disabled;"/><br/>
 
   <input type="text" id="ime_mode_auto_url"     style="ime-mode: auto;"/><br/>
@@ -166,17 +170,18 @@ function runBasicTest(aIsEditable, aInDe
     ok(false, "runBasicTest(): failed to begin input transaction");
     return;
   }
 
   function test(aTest) {
     function moveFocus(aTestInner, aFocusEventHandler) {
       function getFocusedElement() {
         let focusedElement = gFM.focusedElement;
-        if (focusedElement?.containingShadowRoot?.isUAWidget()) {
+        if (aTest.idInUAWidget === undefined &&
+            focusedElement?.containingShadowRoot?.isUAWidget()) {
           focusedElement = focusedElement.containingShadowRoot.host;
         }
         return focusedElement;
       }
 
       if (aInDesignMode) {
         if (document.activeElement) {
           document.activeElement.blur();
@@ -185,16 +190,20 @@ function runBasicTest(aIsEditable, aInDe
         document.getElementById("display").focus();
       } else if (aTest.expectedEnabled == gUtils.IME_STATUS_ENABLED) {
         document.getElementById("password").focus();
       } else {
         document.getElementById("text").focus();
       }
       var previousFocusedElement = getFocusedElement();
       var element = document.getElementById(aTest.id);
+      if (aTest.idInUAWidget !== undefined) {
+        element = SpecialPowers.wrap(element).
+          openOrClosedShadowRoot.getElementById(aTest.idInUAWidget);
+      }
       var focusEventTarget = element;
       if (element.contentDocument) {
         focusEventTarget = element.contentDocument;
         element = element.contentDocument.documentElement;
       }
 
       focusEventTarget.addEventListener("focus", aFocusEventHandler, true);
       onIMEFocusBlurHandler = aFocusEventHandler;
@@ -355,16 +364,18 @@ function runBasicTest(aIsEditable, aInDe
   //     shouldn't be focusable.
   const kEnabledStateOnNonEditableElement =
     (aInDesignMode || aIsEditable) ? gUtils.IME_STATUS_ENABLED :
                                      gUtils.IME_STATUS_DISABLED;
   const kEnabledStateOnPasswordField =
     aInDesignMode ? gUtils.IME_STATUS_ENABLED : gUtils.IME_STATUS_PASSWORD;
   const kEnabledStateOnReadonlyField =
     aInDesignMode ? gUtils.IME_STATUS_ENABLED : gUtils.IME_STATUS_DISABLED;
+  const kEnabledStateInUAWidget =
+    aInDesignMode ? gUtils.IME_STATUS_ENABLED : gUtils.IME_STATUS_DISABLED;
   const kTests = [
     { id: "text",
       description: "input[type=text]",
       focusable: !aInDesignMode,
       expectedEnabled: gUtils.IME_STATUS_ENABLED,
       expectedType: "text" },
     { id: "text_readonly",
       description: "input[type=text][readonly]",
@@ -477,16 +488,68 @@ function runBasicTest(aIsEditable, aInDe
       expectedEnabled: kEnabledStateOnNonEditableElement },
 
     // a element
     { id: "a_href",
       description: "a[href]",
       focusable: !aIsEditable && !aInDesignMode,
       expectedEnabled: kEnabledStateOnNonEditableElement },
 
+    // audio element
+    { id: "audio_with_controls",
+      description: "audio",
+      focusable: !aInDesignMode,
+      expectedEnabled: kEnabledStateOnNonEditableElement },
+    { id: "audio_with_controls",
+      idInUAWidget: "playButton",
+      description: "playButton in audio",
+      focusable: !aInDesignMode,
+      expectedEnabled: kEnabledStateInUAWidget },
+    { id: "audio_with_controls",
+      idInUAWidget: "scrubber",
+      description: "scrubber in audio",
+      focusable: !aInDesignMode,
+      expectedEnabled: kEnabledStateInUAWidget },
+    { id: "audio_with_controls",
+      idInUAWidget: "muteButton",
+      description: "muteButton in audio",
+      focusable: !aInDesignMode,
+      expectedEnabled: kEnabledStateInUAWidget },
+    { id: "audio_with_controls",
+      idInUAWidget: "volumeControl",
+      description: "volumeControl in audio",
+      focusable: !aInDesignMode,
+      expectedEnabled: kEnabledStateInUAWidget },
+
+    // video element
+    { id: "video_with_controls",
+      description: "video",
+      focusable: !aInDesignMode,
+      expectedEnabled: kEnabledStateOnNonEditableElement },
+    { id: "video_with_controls",
+      idInUAWidget: "playButton",
+      description: "playButton in video",
+      focusable: !aInDesignMode,
+      expectedEnabled: kEnabledStateInUAWidget },
+    { id: "video_with_controls",
+      idInUAWidget: "scrubber",
+      description: "scrubber in video",
+      focusable: !aInDesignMode,
+      expectedEnabled: kEnabledStateInUAWidget },
+    { id: "video_with_controls",
+      idInUAWidget: "muteButton",
+      description: "muteButton in video",
+      focusable: !aInDesignMode,
+      expectedEnabled: kEnabledStateInUAWidget },
+    { id: "video_with_controls",
+      idInUAWidget: "volumeControl",
+      description: "volumeControl in video",
+      focusable: !aInDesignMode,
+      expectedEnabled: kEnabledStateInUAWidget },
+
     // ime-mode
     { id: "ime_mode_auto",
       description: "input[type=text][style=\"ime-mode: auto;\"]",
       focusable: !aInDesignMode,
       expectedEnabled: gUtils.IME_STATUS_ENABLED },
     { id: "ime_mode_normal",
       description: "input[type=text][style=\"ime-mode: normal;\"]",
       focusable: !aInDesignMode,