From 759a321bba44d6893552eb7427fa67e6e855a3ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emilio=20Cobos=20=C3=81lvarez?= Date: Mon, 30 May 2022 11:06:06 +0000 Subject: [PATCH] Bug 1771151 - Make modal dialog code more generic, and make it apply to fullscreen too behind a pref. r=edgar For now, don't turn it on by default yet, because I want to wait for more discussion in https://github.com/w3c/csswg-drafts/issues/6965 and so on. But I think the code is simple enough to land this. Differential Revision: https://phabricator.services.mozilla.com/D147295 --- dom/base/Document.cpp | 168 +++++++++--------- dom/base/Document.h | 6 +- dom/base/test/mochitest.ini | 1 + dom/base/test/test_fullscreen_modal.html | 65 +++++++ dom/events/EventStates.h | 6 +- layout/style/res/html.css | 11 -- layout/style/res/ua.css | 22 ++- modules/libpref/init/StaticPrefList.yaml | 7 + servo/components/style/element_state.rs | 4 +- .../style/gecko/non_ts_pseudo_class_list.rs | 2 +- servo/components/style/gecko/wrapper.rs | 2 +- 11 files changed, 179 insertions(+), 115 deletions(-) create mode 100644 dom/base/test/test_fullscreen_modal.html diff --git a/dom/base/Document.cpp b/dom/base/Document.cpp index 5b7e6f79eec7d..466a094810389 100644 --- a/dom/base/Document.cpp +++ b/dom/base/Document.cpp @@ -14486,7 +14486,8 @@ void Document::RestorePreviousFullscreenState(UniquePtr aExit) { // completely exit from the fullscreen state as well. Document* newFullscreenDoc; if (fullscreenCount > 1) { - lastDoc->UnsetFullscreenElement(); + DebugOnly removedFullscreenElement = lastDoc->PopFullscreenElement(); + MOZ_ASSERT(removedFullscreenElement); newFullscreenDoc = lastDoc; } else { lastDoc->CleanupFullscreenState(); @@ -14551,38 +14552,10 @@ static void NotifyFullScreenChangedForMediaElement(Element& aElement) { } } -/* static */ -void Document::ClearFullscreenStateOnElement(Element& aElement) { - // Remove styles from existing top element. - aElement.RemoveStates(NS_EVENT_STATE_FULLSCREEN); - NotifyFullScreenChangedForMediaElement(aElement); - // Reset iframe fullscreen flag. - if (auto* iframe = HTMLIFrameElement::FromNode(aElement)) { - iframe->SetFullscreenFlag(false); - } -} - void Document::CleanupFullscreenState() { - // Iterate the top layer and clear the fullscreen states. - // Since we also need to clear the fullscreen-ancestor state, and - // currently fullscreen elements can only be placed in hierarchy - // order in the stack, reversely iterating the stack could be more - // efficient. NOTE that fullscreen-ancestor state would be removed - // in bug 1199529, and the elements may not in hierarchy order - // after bug 1195213. - mTopLayer.RemoveElementsBy([&](const nsWeakPtr& weakPtr) { - nsCOMPtr element(do_QueryReferent(weakPtr)); - if (!element || !element->IsInComposedDoc() || - element->OwnerDoc() != this) { - return true; - } - - if (element->State().HasState(NS_EVENT_STATE_FULLSCREEN)) { - ClearFullscreenStateOnElement(*element); - return true; - } - return false; - }); + while (PopFullscreenElement()) { + /* Remove the next one if appropriate */ + } mFullscreenRoot = nullptr; @@ -14593,28 +14566,47 @@ void Document::CleanupFullscreenState() { mSavedResolution, ResolutionChangeOrigin::MainThreadRestore); } } - - UpdateViewportScrollbarOverrideForFullscreen(this); } -void Document::UnsetFullscreenElement() { +bool Document::PopFullscreenElement() { Element* removedElement = TopLayerPop([](Element* element) -> bool { return element->State().HasState(NS_EVENT_STATE_FULLSCREEN); }); + if (!removedElement) { + return false; + } + MOZ_ASSERT(removedElement->State().HasState(NS_EVENT_STATE_FULLSCREEN)); - ClearFullscreenStateOnElement(*removedElement); + removedElement->RemoveStates(NS_EVENT_STATE_FULLSCREEN); + NotifyFullScreenChangedForMediaElement(*removedElement); + // Reset iframe fullscreen flag. + if (auto* iframe = HTMLIFrameElement::FromNode(removedElement)) { + iframe->SetFullscreenFlag(false); + } UpdateViewportScrollbarOverrideForFullscreen(this); + return true; } void Document::SetFullscreenElement(Element& aElement) { - TopLayerPush(aElement); aElement.AddStates(NS_EVENT_STATE_FULLSCREEN); + TopLayerPush(aElement); NotifyFullScreenChangedForMediaElement(aElement); UpdateViewportScrollbarOverrideForFullscreen(this); } +static EventStates TopLayerModalStates() { + EventStates modalStates = NS_EVENT_STATE_MODAL_DIALOG; + if (StaticPrefs::dom_fullscreen_modal()) { + modalStates |= NS_EVENT_STATE_FULLSCREEN; + } + return modalStates; +} + void Document::TopLayerPush(Element& aElement) { + const bool modal = + aElement.State().HasAtLeastOneOfStates(TopLayerModalStates()); + auto predictFunc = [&aElement](Element* element) { return element == &aElement; }; @@ -14622,70 +14614,45 @@ void Document::TopLayerPush(Element& aElement) { mTopLayer.AppendElement(do_GetWeakReference(&aElement)); NS_ASSERTION(GetTopLayerTop() == &aElement, "Should match"); -} -void Document::AddModalDialog(HTMLDialogElement& aDialogElement) { - TopLayerPush(aDialogElement); + if (modal) { + aElement.AddStates(NS_EVENT_STATE_TOPMOST_MODAL); - 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_MODAL_DIALOG | - 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 + bool foundExistingModalElement = false; + for (const nsWeakPtr& weakPtr : Reversed(mTopLayer)) { + nsCOMPtr element(do_QueryReferent(weakPtr)); + if (element && element != &aElement && + element->State().HasState(NS_EVENT_STATE_TOPMOST_MODAL)) { + element->RemoveStates(NS_EVENT_STATE_TOPMOST_MODAL); + foundExistingModalElement = true; break; } } + + if (!foundExistingModalElement) { + Element* root = GetRootElement(); + MOZ_RELEASE_ASSERT(root, "top layer element outside of document?"); + if (&aElement != root) { + // Add inert to the root element so that the inertness is applied to the + // entire document. + root->AddStates(NS_EVENT_STATE_MOZINERT); + } + } } } -void Document::RemoveModalDialog(HTMLDialogElement& aDialogElement) { - aDialogElement.RemoveStates(NS_EVENT_STATE_MODAL_DIALOG | - NS_EVENT_STATE_TOPMOST_MODAL_DIALOG); +void Document::AddModalDialog(HTMLDialogElement& aDialogElement) { + aDialogElement.AddStates(NS_EVENT_STATE_MODAL_DIALOG); + TopLayerPush(aDialogElement); +} +void Document::RemoveModalDialog(HTMLDialogElement& aDialogElement) { auto predicate = [&aDialogElement](Element* element) -> bool { return element == &aDialogElement; }; - DebugOnly removedElement = TopLayerPop(predicate); MOZ_ASSERT(removedElement == &aDialogElement); - - // 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 early 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); - } + aDialogElement.RemoveStates(NS_EVENT_STATE_MODAL_DIALOG); } Element* Document::TopLayerPop(FunctionRef aPredicate) { @@ -14722,6 +14689,33 @@ Element* Document::TopLayerPop(FunctionRef aPredicate) { } } + if (!removedElement) { + return nullptr; + } + + const EventStates modalStates = TopLayerModalStates(); + const bool modal = removedElement->State().HasAtLeastOneOfStates(modalStates); + + if (modal) { + removedElement->RemoveStates(NS_EVENT_STATE_TOPMOST_MODAL); + bool foundExistingModalElement = false; + for (const nsWeakPtr& weakPtr : Reversed(mTopLayer)) { + nsCOMPtr element(do_QueryReferent(weakPtr)); + if (element && element->State().HasAtLeastOneOfStates(modalStates)) { + element->AddStates(NS_EVENT_STATE_TOPMOST_MODAL); + foundExistingModalElement = true; + break; + } + } + // No more modal elements, make the document not inert anymore. + if (!foundExistingModalElement) { + Element* root = GetRootElement(); + if (root && !root->GetBoolAttr(nsGkAtoms::inert)) { + root->RemoveStates(NS_EVENT_STATE_MOZINERT); + } + } + } + return removedElement; } diff --git a/dom/base/Document.h b/dom/base/Document.h index d04aee703c53e..7339cbd601879 100644 --- a/dom/base/Document.h +++ b/dom/base/Document.h @@ -1905,8 +1905,6 @@ class Document : public nsINode, void RequestFullscreenInParentProcess(UniquePtr aRequest, bool applyFullScreenDirectly); - static void ClearFullscreenStateOnElement(Element&); - // Pushes aElement onto the top layer void TopLayerPush(Element&); @@ -1920,8 +1918,8 @@ class Document : public nsINode, void CleanupFullscreenState(); // Pops the fullscreen element from the top layer and clears its - // fullscreen flag. - void UnsetFullscreenElement(); + // fullscreen flag. Returns whether there was any fullscreen element. + bool PopFullscreenElement(); // Pushes the given element into the top of top layer and set fullscreen // flag. diff --git a/dom/base/test/mochitest.ini b/dom/base/test/mochitest.ini index 86287d9cc9933..550df56f53ad0 100644 --- a/dom/base/test/mochitest.ini +++ b/dom/base/test/mochitest.ini @@ -674,6 +674,7 @@ support-files = [test_focus_display_none_xorigin_iframe.html] support-files = file_focus_display_none_xorigin_iframe_inner.html +[test_fullscreen_modal.html] [test_getAttribute_after_createAttribute.html] [test_getElementById.html] [test_getTranslationNodes.html] diff --git a/dom/base/test/test_fullscreen_modal.html b/dom/base/test/test_fullscreen_modal.html new file mode 100644 index 0000000000000..1cc6b956048c4 --- /dev/null +++ b/dom/base/test/test_fullscreen_modal.html @@ -0,0 +1,65 @@ + +Test for bug 1771150 + + + + +
+ + diff --git a/dom/events/EventStates.h b/dom/events/EventStates.h index d5b751e96ff4c..9ae56b52e06fd 100644 --- a/dom/events/EventStates.h +++ b/dom/events/EventStates.h @@ -271,8 +271,8 @@ class EventStates { #define NS_EVENT_STATE_MODAL_DIALOG NS_DEFINE_EVENT_STATE_MACRO(42) // Inert subtrees #define NS_EVENT_STATE_MOZINERT NS_DEFINE_EVENT_STATE_MACRO(43) -// Topmost Modal element in top layer -#define NS_EVENT_STATE_TOPMOST_MODAL_DIALOG NS_DEFINE_EVENT_STATE_MACRO(44) +// Topmost modal element in top layer +#define NS_EVENT_STATE_TOPMOST_MODAL NS_DEFINE_EVENT_STATE_MACRO(44) // Devtools highlighter (but it's used for something else atm). #define NS_EVENT_STATE_DEVTOOLS_HIGHLIGHTED NS_DEFINE_EVENT_STATE_MACRO(45) // Devtools style inspector stuff. @@ -316,7 +316,7 @@ class EventStates { NS_EVENT_STATE_FOCUS_WITHIN | NS_EVENT_STATE_FULLSCREEN | \ NS_EVENT_STATE_HOVER | NS_EVENT_STATE_URLTARGET | \ NS_EVENT_STATE_MODAL_DIALOG | NS_EVENT_STATE_MOZINERT | \ - NS_EVENT_STATE_TOPMOST_MODAL_DIALOG | NS_EVENT_STATE_REVEALED) + NS_EVENT_STATE_TOPMOST_MODAL | NS_EVENT_STATE_REVEALED) #define INTRINSIC_STATES (~EXTERNALLY_MANAGED_STATES) diff --git a/layout/style/res/html.css b/layout/style/res/html.css index b38b67ee74318..4488d85cabd5c 100644 --- a/layout/style/res/html.css +++ b/layout/style/res/html.css @@ -824,17 +824,6 @@ dialog:not([open]) { display: none; } -/* This pseudo-class is used to remove the inertness for the topmost modal - * dialog in top layer. - * - * Avoid doing this if the dialog is explicitly inert though. */ -dialog:not([inert]):-moz-topmost-modal-dialog { - -moz-inert: none; - /* Topmost modal dialog needs to be selectable even though ancestors are - * inert, but allow users to override this if they want to. */ - user-select: text; -} - dialog:-moz-modal-dialog { -moz-top-layer: top !important; position: fixed; diff --git a/layout/style/res/ua.css b/layout/style/res/ua.css index 7875577c7bb23..f7a93092f8f04 100644 --- a/layout/style/res/ua.css +++ b/layout/style/res/ua.css @@ -3,6 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ @namespace parsererror url(http://www.mozilla.org/newlayout/xml/parsererror.xml); +@namespace html url(http://www.w3.org/1999/xhtml); @namespace xul url(http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul); /* magic -- some of these rules are important to keep pages from overriding @@ -343,14 +344,12 @@ /* Printing */ @media print { - * { cursor: default !important; } - } -*|*:fullscreen:not(:root) { +:fullscreen:not(:root) { position: fixed !important; top: 0 !important; left: 0 !important; @@ -376,6 +375,17 @@ xul|*:fullscreen:not(:root, [hidden="true"]) { display: block; } +/* This pseudo-class is used to remove the inertness for the topmost modal + * element in top layer. + * + * Avoid doing this if the element is explicitly inert though. */ +:-moz-topmost-modal:not(html|*[inert]) { + -moz-inert: none; + /* Topmost modal elements need to be selectable even though ancestors are + * inert, but allow users to override this if they want to. */ + user-select: text; +} + /** * Ensure we recompute the default color for the root based on its * computed color-scheme. This matches other browsers. @@ -393,11 +403,11 @@ xul|*:fullscreen:not(:root, [hidden="true"]) { /* Selectors here should match the check in * nsViewportFrame.cpp:ShouldInTopLayerForFullscreen() */ -*|*:fullscreen:not(:root, :-moz-browser-frame) { +:fullscreen:not(:root, :-moz-browser-frame) { -moz-top-layer: top !important; } -*|*::backdrop { +::backdrop { -moz-top-layer: top !important; display: block; position: fixed; @@ -407,7 +417,7 @@ xul|*:fullscreen:not(:root, [hidden="true"]) { user-select: none; } -*|*:-moz-full-screen:not(:root)::backdrop { +:fullscreen:not(:root)::backdrop { background: black; } diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml index 297f9d6fb472d..1ae82586128a3 100644 --- a/modules/libpref/init/StaticPrefList.yaml +++ b/modules/libpref/init/StaticPrefList.yaml @@ -2320,6 +2320,13 @@ value: false mirror: always +# Whether fullscreen should make the rest of the document inert. +# This matches other browsers but historically not Gecko. +- name: dom.fullscreen.modal + type: bool + value: false + mirror: always + # Whether the Gamepad API is enabled - name: dom.gamepad.enabled type: bool diff --git a/servo/components/style/element_state.rs b/servo/components/style/element_state.rs index 28d505129a1af..f8f4629ef9b71 100644 --- a/servo/components/style/element_state.rs +++ b/servo/components/style/element_state.rs @@ -118,8 +118,8 @@ bitflags! { const IN_MODAL_DIALOG_STATE = 1 << 42; /// const IN_MOZINERT_STATE = 1 << 43; - /// State for the topmost dialog element in top layer - const IN_TOPMOST_MODAL_DIALOG_STATE = 1 << 44; + /// State for the topmost modal element in top layer + const IN_TOPMOST_MODAL_TOP_LAYER_STATE = 1 << 44; /// Initially used for the devtools highlighter, but now somehow only /// used for the devtools accessibility inspector. const IN_DEVTOOLS_HIGHLIGHTED_STATE = 1 << 45; 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 7d6190ba47cfb..672e8104fe700 100644 --- a/servo/components/style/gecko/non_ts_pseudo_class_list.rs +++ b/servo/components/style/gecko/non_ts_pseudo_class_list.rs @@ -54,7 +54,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), + ("-moz-topmost-modal", MozTopmostModal, IN_TOPMOST_MODAL_TOP_LAYER_STATE, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), ("-moz-broken", MozBroken, IN_BROKEN_STATE, _), ("-moz-loading", MozLoading, IN_LOADING_STATE, _), ("-moz-has-dir-attr", MozHasDirAttr, IN_HAS_DIR_ATTR_STATE, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), diff --git a/servo/components/style/gecko/wrapper.rs b/servo/components/style/gecko/wrapper.rs index 7aa32764955ff..9c37a5ef49d9e 100644 --- a/servo/components/style/gecko/wrapper.rs +++ b/servo/components/style/gecko/wrapper.rs @@ -2141,7 +2141,7 @@ impl<'le> ::selectors::Element for GeckoElement<'le> { NonTSPseudoClass::MozDirAttrRTL | NonTSPseudoClass::MozDirAttrLikeAuto | NonTSPseudoClass::MozModalDialog | - NonTSPseudoClass::MozTopmostModalDialog | + NonTSPseudoClass::MozTopmostModal | NonTSPseudoClass::Active | NonTSPseudoClass::Hover | NonTSPseudoClass::MozAutofillPreview |