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
This commit is contained in:
Sean Feng 2020-08-13 19:05:37 +00:00
Родитель 514d0b9647
Коммит 3f16476b10
14 изменённых файлов: 138 добавлений и 20 удалений

Просмотреть файл

@ -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> 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> 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<bool(Element*)> aPredicateFunc) {
if (mTopLayer.IsEmpty()) {
return nullptr;

Просмотреть файл

@ -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.

Просмотреть файл

@ -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.

Просмотреть файл

@ -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 <dialog> 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)

Просмотреть файл

@ -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<Element*> removedElement = OwnerDoc()->TopLayerPop(predictFunc);
Document* doc = OwnerDoc();
DebugOnly<Element*> 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();

Просмотреть файл

@ -56,6 +56,7 @@ class HTMLDialogElement final : public nsGenericHTMLElement {
JS::Handle<JSObject*> aGivenProto) override;
private:
void AddToTopLayerIfNeeded();
void RemoveFromTopLayerIfNeeded();
};

Просмотреть файл

@ -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

Просмотреть файл

@ -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;
}
}

Просмотреть файл

@ -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, _),

Просмотреть файл

@ -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 => {

Просмотреть файл

@ -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;

Просмотреть файл

@ -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

Просмотреть файл

@ -1,4 +0,0 @@
[inert-node-is-unfocusable.html]
[Test that inert nodes are not focusable.]
expected: FAIL

Просмотреть файл

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html>
<head>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
</head>
<body id="body">
<dialog>
This is a dialog
</dialog>
<input />
<script>
"use strict";
function testFocus(element, expectFocus) {
var focusedElement = null;
element.addEventListener('focus', function() { focusedElement = element; }, false);
element.focus();
var theElement = element;
assert_equals(focusedElement === theElement, expectFocus, element.id);
}
test(function() {
var dialog = document.querySelector('dialog');
dialog.showModal();
var input = document.querySelector('input');
testFocus(input, false);
dialog.remove();
testFocus(input, true);
}, "Test that removing dialog unblocks the document.");
</script>
</body>
</html>