From 3f16476b101c65a0f977257e5ca9c0241d06193f Mon Sep 17 00:00:00 2001 From: Sean Feng Date: Thu, 13 Aug 2020 19:05:37 +0000 Subject: [PATCH] Bug 1200896 - Make the document blocked by the topmost element in the top layer r=emilio Spec: https://html.spec.whatwg.org/multipage/#blocked-by-a-modal-dialog Differential Revision: https://phabricator.services.mozilla.com/D86227 --- dom/base/Document.cpp | 54 +++++++++++++++++++ dom/base/Document.h | 5 ++ dom/base/Element.h | 8 +-- dom/events/EventStates.h | 5 +- dom/html/HTMLDialogElement.cpp | 20 +++++-- dom/html/HTMLDialogElement.h | 1 + layout/style/res/html.css | 14 +++++ servo/components/style/element_state.rs | 2 + .../style/gecko/non_ts_pseudo_class_list.rs | 1 + servo/components/style/gecko/wrapper.rs | 1 + servo/components/style/style_adjuster.rs | 2 + ...-does-not-match-disabled-selector.html.ini | 7 --- .../inert-node-is-unfocusable.html.ini | 4 -- ...remove-dialog-should-unblock-document.html | 34 ++++++++++++ 14 files changed, 138 insertions(+), 20 deletions(-) delete mode 100644 testing/web-platform/meta/html/semantics/interactive-elements/the-dialog-element/inert-does-not-match-disabled-selector.html.ini delete mode 100644 testing/web-platform/meta/html/semantics/interactive-elements/the-dialog-element/inert-node-is-unfocusable.html.ini create mode 100644 testing/web-platform/tests/html/semantics/interactive-elements/the-dialog-element/remove-dialog-should-unblock-document.html diff --git a/dom/base/Document.cpp b/dom/base/Document.cpp index 1016d47196ec..8d9b37edbcd6 100644 --- a/dom/base/Document.cpp +++ b/dom/base/Document.cpp @@ -13664,6 +13664,60 @@ void Document::TopLayerPush(Element* aElement) { NS_ASSERTION(GetTopLayerTop() == aElement, "Should match"); } +void Document::SetBlockedByModalDialog(HTMLDialogElement& aDialogElement) { + Element* root = GetRootElement(); + MOZ_RELEASE_ASSERT(root, "dialog in document without root?"); + + // Add inert to the root element so that the inertness is + // applied to the entire document. Since the modal dialog + // also inherits the inertness, adding + // NS_EVENT_STATE_TOPMOST_MODAL_DIALOG to remove the inertness + // explicitly. + root->AddStates(NS_EVENT_STATE_MOZINERT); + aDialogElement.AddStates(NS_EVENT_STATE_TOPMOST_MODAL_DIALOG); + + // It's possible that there's another modal dialog has opened + // previously which doesn't have the inertness (because we've + // removed the inertness explicitly). Since a + // new modal dialog is opened, we need to grant the inertness + // to the previous one. + for (const nsWeakPtr& weakPtr : Reversed(mTopLayer)) { + nsCOMPtr element(do_QueryReferent(weakPtr)); + if (auto* dialog = HTMLDialogElement::FromNodeOrNull(element)) { + if (dialog != &aDialogElement) { + dialog->RemoveStates(NS_EVENT_STATE_TOPMOST_MODAL_DIALOG); + // It's ok to exit the loop as only one modal dialog should + // have the state + break; + } + } + } +} + +void Document::UnsetBlockedByModalDialog(HTMLDialogElement& aDialogElement) { + aDialogElement.RemoveStates(NS_EVENT_STATE_TOPMOST_MODAL_DIALOG); + + // The document could still be blocked by another modal dialog. + // We need to remove the inertness from this modal dialog. + for (const nsWeakPtr& weakPtr : Reversed(mTopLayer)) { + nsCOMPtr element(do_QueryReferent(weakPtr)); + if (auto* dialog = HTMLDialogElement::FromNodeOrNull(element)) { + if (dialog != &aDialogElement) { + dialog->AddStates(NS_EVENT_STATE_TOPMOST_MODAL_DIALOG); + // Return here because we want to keep the inertness for the + // root element as the document is still blocked by a modal + // dialog + return; + } + } + } + + Element* root = GetRootElement(); + if (root && !root->GetBoolAttr(nsGkAtoms::inert)) { + root->RemoveStates(NS_EVENT_STATE_MOZINERT); + } +} + Element* Document::TopLayerPop(FunctionRef aPredicateFunc) { if (mTopLayer.IsEmpty()) { return nullptr; diff --git a/dom/base/Document.h b/dom/base/Document.h index cdbcc5a9b5a7..128cca6b5051 100644 --- a/dom/base/Document.h +++ b/dom/base/Document.h @@ -180,6 +180,7 @@ class ImageTracker; class HTMLAllCollection; class HTMLBodyElement; class HTMLMetaElement; +class HTMLDialogElement; class HTMLSharedElement; class HTMLImageElement; struct LifecycleCallbackArgs; @@ -1869,6 +1870,10 @@ class Document : public nsINode, // Cancel the dialog element if the document is blocked by the dialog void TryCancelDialog(); + void SetBlockedByModalDialog(HTMLDialogElement&); + + void UnsetBlockedByModalDialog(HTMLDialogElement&); + /** * Called when a frame in a child process has entered fullscreen or when a * fullscreen frame in a child process changes to another origin. diff --git a/dom/base/Element.h b/dom/base/Element.h index 98db4f64ba39..117304fcc0b9 100644 --- a/dom/base/Element.h +++ b/dom/base/Element.h @@ -623,9 +623,10 @@ class Element : public FragmentOrElement { FlushType aFlushType = FlushType::Layout); private: - // Need to allow the ESM, nsGlobalWindow, and the focus manager to - // set our state + // Need to allow the ESM, nsGlobalWindow, and the focus manager + // and Document to set our state friend class mozilla::EventStateManager; + friend class mozilla::dom::Document; friend class ::nsGlobalWindowInner; friend class ::nsGlobalWindowOuter; friend class ::nsFocusManager; @@ -644,7 +645,8 @@ class Element : public FragmentOrElement { EventStates StyleStateFromLocks() const; protected: - // Methods for the ESM, nsGlobalWindow and focus manager to manage state bits. + // Methods for the ESM, nsGlobalWindow, focus manager and Document to + // manage state bits. // These will handle setting up script blockers when they notify, so no need // to do it in the callers unless desired. States passed here must only be // those in EXTERNALLY_MANAGED_STATES. diff --git a/dom/events/EventStates.h b/dom/events/EventStates.h index 3aa7a055ae7c..5c0e121b58b8 100644 --- a/dom/events/EventStates.h +++ b/dom/events/EventStates.h @@ -296,7 +296,8 @@ class EventStates { #define NS_EVENT_STATE_MODAL_DIALOG NS_DEFINE_EVENT_STATE_MACRO(53) // Inert subtrees #define NS_EVENT_STATE_MOZINERT NS_DEFINE_EVENT_STATE_MACRO(54) - +// Topmost Modal element in top layer +#define NS_EVENT_STATE_TOPMOST_MODAL_DIALOG NS_DEFINE_EVENT_STATE_MACRO(55) /** * NOTE: do not go over 63 without updating EventStates::InternalType! */ @@ -332,7 +333,7 @@ class EventStates { 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_MODAL_DIALOG | \ - NS_EVENT_STATE_MOZINERT) + NS_EVENT_STATE_MOZINERT | NS_EVENT_STATE_TOPMOST_MODAL_DIALOG) #define INTRINSIC_STATES (~EXTERNALLY_MANAGED_STATES) diff --git a/dom/html/HTMLDialogElement.cpp b/dom/html/HTMLDialogElement.cpp index ebfbd4a60d5b..dc4f7b7f3e5a 100644 --- a/dom/html/HTMLDialogElement.cpp +++ b/dom/html/HTMLDialogElement.cpp @@ -66,15 +66,28 @@ bool HTMLDialogElement::IsInTopLayer() const { return State().HasState(NS_EVENT_STATE_MODAL_DIALOG); } +void HTMLDialogElement::AddToTopLayerIfNeeded() { + if (IsInTopLayer()) { + return; + } + + Document* doc = OwnerDoc(); + doc->TopLayerPush(this); + doc->SetBlockedByModalDialog(*this); + AddStates(NS_EVENT_STATE_MODAL_DIALOG); +} + void HTMLDialogElement::RemoveFromTopLayerIfNeeded() { if (!IsInTopLayer()) { return; } auto predictFunc = [&](Element* element) { return element == this; }; - DebugOnly removedElement = OwnerDoc()->TopLayerPop(predictFunc); + Document* doc = OwnerDoc(); + DebugOnly removedElement = doc->TopLayerPop(predictFunc); MOZ_ASSERT(removedElement == this); RemoveStates(NS_EVENT_STATE_MODAL_DIALOG); + doc->UnsetBlockedByModalDialog(*this); } void HTMLDialogElement::UnbindFromTree(bool aNullParent) { @@ -94,11 +107,10 @@ void HTMLDialogElement::ShowModal(ErrorResult& aError) { return; } - if (!IsInTopLayer() && OwnerDoc()->TopLayerPush(this)) { - AddStates(NS_EVENT_STATE_MODAL_DIALOG); - } + AddToTopLayerIfNeeded(); SetOpen(true, aError); + FocusDialog(); aError.SuppressException(); diff --git a/dom/html/HTMLDialogElement.h b/dom/html/HTMLDialogElement.h index 430b6b1ce0f6..10777f2658d6 100644 --- a/dom/html/HTMLDialogElement.h +++ b/dom/html/HTMLDialogElement.h @@ -56,6 +56,7 @@ class HTMLDialogElement final : public nsGenericHTMLElement { JS::Handle aGivenProto) override; private: + void AddToTopLayerIfNeeded(); void RemoveFromTopLayerIfNeeded(); }; diff --git a/layout/style/res/html.css b/layout/style/res/html.css index 786139c5987a..58b46fff2d73 100644 --- a/layout/style/res/html.css +++ b/layout/style/res/html.css @@ -835,6 +835,20 @@ dialog:not([open]) { display: none; } +/* This pseudo-class is used to remove the inertness for + the topmost modal dialog in top layer. This reverts + what StyleAdjuster's adjust_for_inert does. +*/ +dialog:-moz-topmost-modal-dialog { + -moz-inert: none; + -moz-user-focus: initial; + -moz-user-input: initial; + -moz-user-modify: initial; + user-select: text; + pointer-events: initial; + cursor: initial; +} + dialog:-moz-modal-dialog { -moz-top-layer: top !important; /* This is a temporary solution until the relevant CSSWG issues diff --git a/servo/components/style/element_state.rs b/servo/components/style/element_state.rs index da44186b44ba..a3d001a2fabf 100644 --- a/servo/components/style/element_state.rs +++ b/servo/components/style/element_state.rs @@ -148,6 +148,8 @@ bitflags! { /// https://html.spec.whatwg.org/multipage/interaction.html#inert-subtrees const IN_MOZINERT_STATE = 1 << 54; + /// State for the topmost dialog element in top layer + const IN_TOPMOST_MODAL_DIALOG_STATE = 1 << 55; } } diff --git a/servo/components/style/gecko/non_ts_pseudo_class_list.rs b/servo/components/style/gecko/non_ts_pseudo_class_list.rs index bdd994c70955..fedbed331f04 100644 --- a/servo/components/style/gecko/non_ts_pseudo_class_list.rs +++ b/servo/components/style/gecko/non_ts_pseudo_class_list.rs @@ -53,6 +53,7 @@ macro_rules! apply_non_ts_list { ("-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), + ("-moz-topmost-modal-dialog", MozTopmostModalDialog, IN_TOPMOST_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, _), diff --git a/servo/components/style/gecko/wrapper.rs b/servo/components/style/gecko/wrapper.rs index 218642d304f9..6f7ccfd0b17e 100644 --- a/servo/components/style/gecko/wrapper.rs +++ b/servo/components/style/gecko/wrapper.rs @@ -2057,6 +2057,7 @@ impl<'le> ::selectors::Element for GeckoElement<'le> { NonTSPseudoClass::MozDirAttrLikeAuto | NonTSPseudoClass::MozAutofill | NonTSPseudoClass::MozModalDialog | + NonTSPseudoClass::MozTopmostModalDialog | NonTSPseudoClass::Active | NonTSPseudoClass::Hover | NonTSPseudoClass::MozAutofillPreview => { diff --git a/servo/components/style/style_adjuster.rs b/servo/components/style/style_adjuster.rs index c48d1bc011a7..fa9a5f420e0d 100644 --- a/servo/components/style/style_adjuster.rs +++ b/servo/components/style/style_adjuster.rs @@ -162,6 +162,8 @@ impl<'a, 'b: 'a> StyleAdjuster<'a, 'b> { /// user-select: none; /// pointer-events: none; /// cursor: default; + /// dialog:-moz-topmost-modal-dialog is used to override above rules to remove + /// the inertness for the topmost modal dialog. fn adjust_for_inert(&mut self) { use properties::longhands::_moz_inert::computed_value::T as Inert; use properties::longhands::_moz_user_focus::computed_value::T as UserFocus; diff --git a/testing/web-platform/meta/html/semantics/interactive-elements/the-dialog-element/inert-does-not-match-disabled-selector.html.ini b/testing/web-platform/meta/html/semantics/interactive-elements/the-dialog-element/inert-does-not-match-disabled-selector.html.ini deleted file mode 100644 index efe94e2edd26..000000000000 --- a/testing/web-platform/meta/html/semantics/interactive-elements/the-dialog-element/inert-does-not-match-disabled-selector.html.ini +++ /dev/null @@ -1,7 +0,0 @@ -[inert-does-not-match-disabled-selector.html] - prefs: [dom.dialog_element.enabled:false] - [Tests inert elements do not match the :disabled selector.] - expected: - if (os == "android") and not debug: ["FAIL", "PASS"] - FAIL - diff --git a/testing/web-platform/meta/html/semantics/interactive-elements/the-dialog-element/inert-node-is-unfocusable.html.ini b/testing/web-platform/meta/html/semantics/interactive-elements/the-dialog-element/inert-node-is-unfocusable.html.ini deleted file mode 100644 index 92fe79a89716..000000000000 --- a/testing/web-platform/meta/html/semantics/interactive-elements/the-dialog-element/inert-node-is-unfocusable.html.ini +++ /dev/null @@ -1,4 +0,0 @@ -[inert-node-is-unfocusable.html] - [Test that inert nodes are not focusable.] - expected: FAIL - diff --git a/testing/web-platform/tests/html/semantics/interactive-elements/the-dialog-element/remove-dialog-should-unblock-document.html b/testing/web-platform/tests/html/semantics/interactive-elements/the-dialog-element/remove-dialog-should-unblock-document.html new file mode 100644 index 000000000000..2f2fbad1fc65 --- /dev/null +++ b/testing/web-platform/tests/html/semantics/interactive-elements/the-dialog-element/remove-dialog-should-unblock-document.html @@ -0,0 +1,34 @@ + + + + + + + + + This is a dialog + + + + +