Bug 1637307 - Push/Pop dialog to top layer when needed r=smaug,emilio
authorsefeng <sefeng@mozilla.com>
Sat, 23 May 2020 01:45:33 +0000
changeset 531751 fcfe734c80312372987f142c64747cccf95e511a
parent 531750 b0080e65593a641aceed99950ef6780305a0ecbf
child 531752 fb421eb632f650cdf5396f928795847a6e56f758
push id37442
push userncsoregi@mozilla.com
push dateSat, 23 May 2020 09:21:24 +0000
treeherdermozilla-central@bbcc193fe0f0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug, emilio
bugs1637307
milestone78.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 1637307 - Push/Pop dialog to top layer when needed r=smaug,emilio This patch completes the top layer requirement for showModal() Spec: https://html.spec.whatwg.org/multipage/interactive-elements.html#dom-dialog-showmodal Differential Revision: https://phabricator.services.mozilla.com/D74922
dom/events/EventStates.h
dom/html/HTMLDialogElement.cpp
dom/html/HTMLDialogElement.h
layout/style/res/html.css
servo/components/style/element_state.rs
servo/components/style/gecko/non_ts_pseudo_class_list.rs
servo/components/style/gecko/wrapper.rs
--- a/dom/events/EventStates.h
+++ b/dom/events/EventStates.h
@@ -287,16 +287,18 @@ class EventStates {
 // Element is filled by Autofill feature.
 #define NS_EVENT_STATE_AUTOFILL NS_DEFINE_EVENT_STATE_MACRO(50)
 // Element is filled with preview data by Autofill feature.
 #define NS_EVENT_STATE_AUTOFILL_PREVIEW NS_DEFINE_EVENT_STATE_MACRO(51)
 // Element matches the :focus-visible pseudo-class.
 //
 // TODO(emilio): We should eventually unify this and FOCUSRING.
 #define NS_EVENT_STATE_FOCUS_VISIBLE NS_DEFINE_EVENT_STATE_MACRO(52)
+// Modal <dialog> element
+#define NS_EVENT_STATE_MODAL_DIALOG NS_DEFINE_EVENT_STATE_MACRO(53)
 
 /**
  * NOTE: do not go over 63 without updating EventStates::InternalType!
  */
 
 #define DIRECTION_STATES (NS_EVENT_STATE_LTR | NS_EVENT_STATE_RTL)
 
 #define DIR_ATTR_STATES                                        \
@@ -322,13 +324,13 @@ class EventStates {
 // INTRINSIC_STATES, which are are computed by the element itself
 // and returned from Element::IntrinsicState.
 #define EXTERNALLY_MANAGED_STATES                                              \
   (MANUALLY_MANAGED_STATES | DIR_ATTR_STATES | DISABLED_STATES |               \
    REQUIRED_STATES | NS_EVENT_STATE_ACTIVE | NS_EVENT_STATE_DEFINED |          \
    NS_EVENT_STATE_DRAGOVER | NS_EVENT_STATE_FOCUS | NS_EVENT_STATE_FOCUSRING | \
    NS_EVENT_STATE_FOCUS_WITHIN | NS_EVENT_STATE_FULLSCREEN |                   \
    NS_EVENT_STATE_HOVER | NS_EVENT_STATE_URLTARGET |                           \
-   NS_EVENT_STATE_FOCUS_VISIBLE)
+   NS_EVENT_STATE_FOCUS_VISIBLE | NS_EVENT_STATE_MODAL_DIALOG)
 
 #define INTRINSIC_STATES (~EXTERNALLY_MANAGED_STATES)
 
 #endif  // mozilla_EventStates_h_
--- a/dom/html/HTMLDialogElement.cpp
+++ b/dom/html/HTMLDialogElement.cpp
@@ -39,36 +39,63 @@ void HTMLDialogElement::Close(
     return;
   }
   if (aReturnValue.WasPassed()) {
     SetReturnValue(aReturnValue.Value());
   }
   ErrorResult ignored;
   SetOpen(false, ignored);
   ignored.SuppressException();
+
+  RemoveFromTopLayerIfNeeded();
+
   RefPtr<AsyncEventDispatcher> eventDispatcher = new AsyncEventDispatcher(
       this, NS_LITERAL_STRING("close"), CanBubble::eNo);
   eventDispatcher->PostDOMEvent();
 }
 
 void HTMLDialogElement::Show() {
   if (Open()) {
     return;
   }
   ErrorResult ignored;
   SetOpen(true, ignored);
   ignored.SuppressException();
 }
 
+bool HTMLDialogElement::IsInTopLayer() const {
+  return State().HasState(NS_EVENT_STATE_MODAL_DIALOG);
+}
+
+void HTMLDialogElement::RemoveFromTopLayerIfNeeded() {
+  if (!IsInTopLayer()) {
+    return;
+  }
+  auto predictFunc = [&](Element* element) { return element == this; };
+
+  DebugOnly<Element*> removedElement = OwnerDoc()->TopLayerPop(predictFunc);
+  MOZ_ASSERT(removedElement == this);
+  RemoveStates(NS_EVENT_STATE_MODAL_DIALOG);
+}
+
+void HTMLDialogElement::UnbindFromTree(bool aNullParent) {
+  RemoveFromTopLayerIfNeeded();
+  nsGenericHTMLElement::UnbindFromTree(aNullParent);
+}
+
 void HTMLDialogElement::ShowModal(ErrorResult& aError) {
   if (!IsInComposedDoc() || Open()) {
     aError.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
     return;
   }
 
+  if (!IsInTopLayer() && OwnerDoc()->TopLayerPush(this)) {
+    AddStates(NS_EVENT_STATE_MODAL_DIALOG);
+  }
+
   SetOpen(true, aError);
   aError.SuppressException();
 }
 
 JSObject* HTMLDialogElement::WrapNode(JSContext* aCx,
                                       JS::Handle<JSObject*> aGivenProto) {
   return HTMLDialogElement_Binding::Wrap(aCx, this, aGivenProto);
 }
--- a/dom/html/HTMLDialogElement.h
+++ b/dom/html/HTMLDialogElement.h
@@ -32,24 +32,31 @@ class HTMLDialogElement final : public n
     SetHTMLBoolAttr(nsGkAtoms::open, aOpen, aError);
   }
 
   void GetReturnValue(nsAString& aReturnValue) { aReturnValue = mReturnValue; }
   void SetReturnValue(const nsAString& aReturnValue) {
     mReturnValue = aReturnValue;
   }
 
+  void UnbindFromTree(bool aNullParent = true) override;
+
   void Close(const mozilla::dom::Optional<nsAString>& aReturnValue);
   void Show();
   void ShowModal(ErrorResult& aError);
 
+  bool IsInTopLayer() const;
+
   nsString mReturnValue;
 
  protected:
   virtual ~HTMLDialogElement();
   JSObject* WrapNode(JSContext* aCx,
                      JS::Handle<JSObject*> aGivenProto) override;
+
+ private:
+  void RemoveFromTopLayerIfNeeded();
 };
 
 }  // namespace dom
 }  // namespace mozilla
 
 #endif
--- a/layout/style/res/html.css
+++ b/layout/style/res/html.css
@@ -829,16 +829,25 @@ dialog {
   background: white;
   width: -moz-fit-content;
 }
 
 dialog:not([open]) {
   display: none;
 }
 
+dialog:-moz-modal-dialog {
+  -moz-top-layer: top !important;
+}
+
+/* https://html.spec.whatwg.org/#flow-content-3 */
+dialog::backdrop {
+  background: rgba(0, 0, 0, 0.1);
+}
+
 marquee {
   inline-size: -moz-available;
   display: inline-block;
   vertical-align: text-bottom;
   text-align: start;
 }
 
 marquee[direction="up"], marquee[direction="down"] {
--- a/servo/components/style/element_state.rs
+++ b/servo/components/style/element_state.rs
@@ -136,16 +136,20 @@ bitflags! {
         /// Non-standard & undocumented.
         const IN_AUTOFILL_STATE = 1 << 50;
         /// Non-standard & undocumented.
         const IN_AUTOFILL_PREVIEW_STATE = 1 << 51;
         /// :focus-visible
         ///
         /// https://drafts.csswg.org/selectors-4/#the-focus-visible-pseudo
         const IN_FOCUS_VISIBLE_STATE = 1 << 52;
+        /// State that dialog element is modal, for centered alignment
+        ///
+        /// https://html.spec.whatwg.org/#centered-alignment
+        const IN_MODAL_DIALOG_STATE = 1 << 53;
     }
 }
 
 bitflags! {
     /// Event-based document states.
     ///
     /// NB: Is important for this to remain in sync with Gecko's
     /// dom/base/Document.h.
--- a/servo/components/style/gecko/non_ts_pseudo_class_list.rs
+++ b/servo/components/style/gecko/non_ts_pseudo_class_list.rs
@@ -45,16 +45,17 @@ macro_rules! apply_non_ts_list {
                 ("focus-visible", FocusVisible, IN_FOCUS_VISIBLE_STATE, _),
                 ("hover", Hover, IN_HOVER_STATE, _),
                 ("-moz-drag-over", MozDragOver, IN_DRAGOVER_STATE, _),
                 ("target", Target, IN_TARGET_STATE, _),
                 ("indeterminate", Indeterminate, IN_INDETERMINATE_STATE, _),
                 ("-moz-devtools-highlighted", MozDevtoolsHighlighted, IN_DEVTOOLS_HIGHLIGHTED_STATE, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS),
                 ("-moz-styleeditor-transitioning", MozStyleeditorTransitioning, IN_STYLEEDITOR_TRANSITIONING_STATE, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS),
                 ("fullscreen", Fullscreen, IN_FULLSCREEN_STATE, _),
+                ("-moz-modal-dialog", MozModalDialog, IN_MODAL_DIALOG_STATE, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS),
                 // TODO(emilio): This is inconsistently named (the capital R).
                 ("-moz-focusring", MozFocusRing, IN_FOCUSRING_STATE, _),
                 ("-moz-broken", MozBroken, IN_BROKEN_STATE, _),
                 ("-moz-loading", MozLoading, IN_LOADING_STATE, _),
                 ("-moz-suppressed", MozSuppressed, IN_SUPPRESSED_STATE, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS_AND_CHROME),
                 ("-moz-has-dir-attr", MozHasDirAttr, IN_HAS_DIR_ATTR_STATE, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS),
                 ("-moz-dir-attr-ltr", MozDirAttrLTR, IN_HAS_DIR_ATTR_LTR_STATE, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS),
                 ("-moz-dir-attr-rtl", MozDirAttrRTL, IN_HAS_DIR_ATTR_RTL_STATE, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS),
--- a/servo/components/style/gecko/wrapper.rs
+++ b/servo/components/style/gecko/wrapper.rs
@@ -2057,16 +2057,17 @@ impl<'le> ::selectors::Element for Gecko
             NonTSPseudoClass::MozMeterOptimum |
             NonTSPseudoClass::MozMeterSubOptimum |
             NonTSPseudoClass::MozMeterSubSubOptimum |
             NonTSPseudoClass::MozHasDirAttr |
             NonTSPseudoClass::MozDirAttrLTR |
             NonTSPseudoClass::MozDirAttrRTL |
             NonTSPseudoClass::MozDirAttrLikeAuto |
             NonTSPseudoClass::MozAutofill |
+            NonTSPseudoClass::MozModalDialog |
             NonTSPseudoClass::Active |
             NonTSPseudoClass::Hover |
             NonTSPseudoClass::MozAutofillPreview => {
                 self.state().intersects(pseudo_class.state_flag())
             },
             NonTSPseudoClass::AnyLink => self.is_link(),
             NonTSPseudoClass::Link => {
                 self.is_link() && context.visited_handling().matches_unvisited()