Skip to content

Commit

Permalink
Bug 1771151 - Make modal dialog code more generic, and make it apply …
Browse files Browse the repository at this point in the history
…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 w3c/csswg-drafts#6965 and
so on. But I think the code is simple enough to land this.

Differential Revision: https://phabricator.services.mozilla.com/D147295
  • Loading branch information
emilio committed May 30, 2022
1 parent 804d415 commit 8cd9847
Show file tree
Hide file tree
Showing 11 changed files with 179 additions and 115 deletions.
168 changes: 81 additions & 87 deletions dom/base/Document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14486,7 +14486,8 @@ void Document::RestorePreviousFullscreenState(UniquePtr<FullscreenExit> aExit) {
// completely exit from the fullscreen state as well.
Document* newFullscreenDoc;
if (fullscreenCount > 1) {
lastDoc->UnsetFullscreenElement();
DebugOnly<bool> removedFullscreenElement = lastDoc->PopFullscreenElement();
MOZ_ASSERT(removedFullscreenElement);
newFullscreenDoc = lastDoc;
} else {
lastDoc->CleanupFullscreenState();
Expand Down Expand Up @@ -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> 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;

Expand All @@ -14593,99 +14566,93 @@ 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;
};
TopLayerPop(predictFunc);

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> 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> 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<Element*> 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> 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<bool(Element*)> aPredicate) {
Expand Down Expand Up @@ -14722,6 +14689,33 @@ Element* Document::TopLayerPop(FunctionRef<bool(Element*)> 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> 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;
}

Expand Down
6 changes: 2 additions & 4 deletions dom/base/Document.h
Original file line number Diff line number Diff line change
Expand Up @@ -1905,8 +1905,6 @@ class Document : public nsINode,
void RequestFullscreenInParentProcess(UniquePtr<FullscreenRequest> aRequest,
bool applyFullScreenDirectly);

static void ClearFullscreenStateOnElement(Element&);

// Pushes aElement onto the top layer
void TopLayerPush(Element&);

Expand All @@ -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.
Expand Down
1 change: 1 addition & 0 deletions dom/base/test/mochitest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
65 changes: 65 additions & 0 deletions dom/base/test/test_fullscreen_modal.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<!doctype html>
<title>Test for bug 1771150</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<script src="/tests/SimpleTest/EventUtils.js"></script>
<link rel="stylesheet" href="/tests/SimpleTest/test.css">
<style>
#fullscreen {
background-color: rgba(0, 255, 0, .5);
}
#fullscreen::backdrop {
background-color: transparent;
}
#fullscreen, #fullscreen::backdrop {
pointer-events: none;
}
</style>
<div id="fullscreen"></div>
<button>Go fullscreen</button>
<script>
const button = document.querySelector("button");
let clickCount = 0;
let lastFullscreenPromise = null;
let shouldEnterFullscreen = false;
button.addEventListener("click", function(e) {
clickCount++;
if (shouldEnterFullscreen) {
lastFullscreenPromise = document.getElementById("fullscreen").requestFullscreen();
}
});

function clickButton(expectEvent) {
let lastClickCount = clickCount;
synthesizeMouseAtCenter(button, {});
(expectEvent ? isnot : is)(lastClickCount, clickCount, `Should've ${expectEvent ? "" : "not "}been able to click`);
}

function enterFullscreen() {
lastFullscreenPromise = null;
shouldEnterFullscreen = true;
clickButton(true);
shouldEnterFullscreen = false;
isnot(lastFullscreenPromise, null, "Should be transitioning to fullscreen");
return lastFullscreenPromise;
}

async function testFullscreenIsModal(modal) {
info("testing modal: " + modal);
is(document.fullscreenElement, null, "Shouldn't be in fullscreen");
await SpecialPowers.pushPrefEnv({ set: [["dom.fullscreen.modal", modal]] });
await enterFullscreen();

clickButton(/* expectEvent = */ !modal);

await document.exitFullscreen();
clickButton(/* expectEvent = */ true);
}

add_task(async function() {
await testFullscreenIsModal(true);
});

add_task(async function() {
await testFullscreenIsModal(false);
});
</script>
6 changes: 3 additions & 3 deletions dom/events/EventStates.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 <dialog> 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.
Expand Down Expand Up @@ -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)

Expand Down
11 changes: 0 additions & 11 deletions layout/style/res/html.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 8cd9847

Please sign in to comment.