From 53d47fbd7acf3262ef62c01f56cb81292d110470 Mon Sep 17 00:00:00 2001 From: Keith Cirkel Date: Mon, 10 Jun 2024 06:23:34 -0700 Subject: [PATCH] Allow top layer elements to be nested within popovers https://bugs.webkit.org/show_bug.cgi?id=269928 Reviewed by Tim Nguyen. This implements the changes to the spec as defined by https://github.com/whatwg/html/pull/10116 * LayoutTests/imported/w3c/web-platform-tests/html/semantics/popovers/popover-top-layer-nesting-anchor.tentative-expected.txt: * LayoutTests/imported/w3c/web-platform-tests/html/semantics/popovers/popover-top-layer-nesting-expected.txt: * LayoutTests/imported/w3c/web-platform-tests/html/semantics/popovers/popover-top-layer-nesting-hints.tentative-expected.txt: * Source/WebCore/dom/Element.cpp: (WebCore::Element::topmostPopoverAncestor): * Source/WebCore/dom/Element.h: * Source/WebCore/dom/FullscreenManager.cpp: (WebCore::FullscreenManager::willEnterFullscreen): * Source/WebCore/html/HTMLDialogElement.cpp: (WebCore::HTMLDialogElement::show): (WebCore::HTMLDialogElement::showModal): * Source/WebCore/html/HTMLElement.cpp: (WebCore::HTMLElement::showPopover): (WebCore::topmostPopoverAncestor): Deleted. Canonical link: https://commits.webkit.org/279874@main --- ...ayer-nesting-anchor.tentative-expected.txt | 20 ++++---- .../popover-top-layer-nesting-expected.txt | 16 +++---- ...layer-nesting-hints.tentative-expected.txt | 8 ++-- Source/WebCore/dom/Element.cpp | 48 +++++++++++++++++++ Source/WebCore/dom/Element.h | 4 ++ Source/WebCore/dom/FullscreenManager.cpp | 5 +- Source/WebCore/html/HTMLDialogElement.cpp | 9 +++- Source/WebCore/html/HTMLElement.cpp | 48 +------------------ 8 files changed, 87 insertions(+), 71 deletions(-) diff --git a/LayoutTests/imported/w3c/web-platform-tests/html/semantics/popovers/popover-top-layer-nesting-anchor.tentative-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/html/semantics/popovers/popover-top-layer-nesting-anchor.tentative-expected.txt index c86675d45790f..0728b078a3a4b 100644 --- a/LayoutTests/imported/w3c/web-platform-tests/html/semantics/popovers/popover-top-layer-nesting-anchor.tentative-expected.txt +++ b/LayoutTests/imported/w3c/web-platform-tests/html/semantics/popovers/popover-top-layer-nesting-anchor.tentative-expected.txt @@ -4,34 +4,34 @@ Nested popover=auto ancestors Nested popover=auto ancestors, target is outer Top layer inside of nested element -FAIL Single popover=auto ancestor with dialog assert_equals: Incorrect behavior expected true but got false +PASS Single popover=auto ancestor with dialog PASS Single popover=auto ancestor with dialog, top layer element *is* a popover FAIL Single popover=auto ancestor with dialog, anchor attribute assert_equals: Incorrect behavior expected true but got false -FAIL Single popover=auto ancestor with fullscreen assert_equals: Incorrect behavior expected true but got false +PASS Single popover=auto ancestor with fullscreen PASS Single popover=auto ancestor with fullscreen, top layer element *is* a popover FAIL Single popover=auto ancestor with fullscreen, anchor attribute promise_test: Unhandled rejection with value: object "TypeError: Type error" -FAIL Single popover=manual ancestor with dialog assert_equals: Incorrect behavior expected true but got false +PASS Single popover=manual ancestor with dialog PASS Single popover=manual ancestor with dialog, top layer element *is* a popover FAIL Single popover=manual ancestor with dialog, anchor attribute assert_equals: Incorrect behavior expected true but got false -FAIL Single popover=manual ancestor with fullscreen assert_equals: Incorrect behavior expected true but got false +PASS Single popover=manual ancestor with fullscreen PASS Single popover=manual ancestor with fullscreen, top layer element *is* a popover FAIL Single popover=manual ancestor with fullscreen, anchor attribute promise_test: Unhandled rejection with value: object "TypeError: Type error" -FAIL Nested popover=auto ancestors with dialog assert_equals: Incorrect behavior expected true but got false +PASS Nested popover=auto ancestors with dialog PASS Nested popover=auto ancestors with dialog, top layer element *is* a popover FAIL Nested popover=auto ancestors with dialog, anchor attribute assert_equals: Incorrect behavior expected true but got false -FAIL Nested popover=auto ancestors with fullscreen assert_equals: Incorrect behavior expected true but got false +PASS Nested popover=auto ancestors with fullscreen PASS Nested popover=auto ancestors with fullscreen, top layer element *is* a popover FAIL Nested popover=auto ancestors with fullscreen, anchor attribute promise_test: Unhandled rejection with value: object "TypeError: Type error" -FAIL Nested popover=auto ancestors, target is outer with dialog assert_equals: Incorrect behavior expected true but got false +PASS Nested popover=auto ancestors, target is outer with dialog PASS Nested popover=auto ancestors, target is outer with dialog, top layer element *is* a popover FAIL Nested popover=auto ancestors, target is outer with dialog, anchor attribute assert_equals: Incorrect behavior expected true but got false -FAIL Nested popover=auto ancestors, target is outer with fullscreen assert_equals: Incorrect behavior expected true but got false +PASS Nested popover=auto ancestors, target is outer with fullscreen PASS Nested popover=auto ancestors, target is outer with fullscreen, top layer element *is* a popover FAIL Nested popover=auto ancestors, target is outer with fullscreen, anchor attribute promise_test: Unhandled rejection with value: object "TypeError: Type error" -FAIL Top layer inside of nested element with dialog assert_equals: Incorrect behavior expected true but got false +PASS Top layer inside of nested element with dialog PASS Top layer inside of nested element with dialog, top layer element *is* a popover FAIL Top layer inside of nested element with dialog, anchor attribute assert_equals: Incorrect behavior expected true but got false -FAIL Top layer inside of nested element with fullscreen assert_equals: Incorrect behavior expected true but got false +PASS Top layer inside of nested element with fullscreen PASS Top layer inside of nested element with fullscreen, top layer element *is* a popover FAIL Top layer inside of nested element with fullscreen, anchor attribute promise_test: Unhandled rejection with value: object "TypeError: Type error" diff --git a/LayoutTests/imported/w3c/web-platform-tests/html/semantics/popovers/popover-top-layer-nesting-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/html/semantics/popovers/popover-top-layer-nesting-expected.txt index 6c72cd1953b2b..117702e486b24 100644 --- a/LayoutTests/imported/w3c/web-platform-tests/html/semantics/popovers/popover-top-layer-nesting-expected.txt +++ b/LayoutTests/imported/w3c/web-platform-tests/html/semantics/popovers/popover-top-layer-nesting-expected.txt @@ -4,24 +4,24 @@ Nested popover=auto ancestors Nested popover=auto ancestors, target is outer Top layer inside of nested element -FAIL Single popover=auto ancestor with dialog assert_equals: Incorrect behavior expected true but got false +PASS Single popover=auto ancestor with dialog PASS Single popover=auto ancestor with dialog, top layer element *is* a popover -FAIL Single popover=auto ancestor with fullscreen assert_equals: Incorrect behavior expected true but got false +PASS Single popover=auto ancestor with fullscreen PASS Single popover=auto ancestor with fullscreen, top layer element *is* a popover -FAIL Single popover=manual ancestor with dialog assert_equals: Incorrect behavior expected true but got false +PASS Single popover=manual ancestor with dialog PASS Single popover=manual ancestor with dialog, top layer element *is* a popover FAIL Single popover=manual ancestor with fullscreen promise_test: Unhandled rejection with value: object "TypeError: Type error" PASS Single popover=manual ancestor with fullscreen, top layer element *is* a popover -FAIL Nested popover=auto ancestors with dialog assert_equals: Incorrect behavior expected true but got false +PASS Nested popover=auto ancestors with dialog PASS Nested popover=auto ancestors with dialog, top layer element *is* a popover -FAIL Nested popover=auto ancestors with fullscreen assert_equals: Incorrect behavior expected true but got false +PASS Nested popover=auto ancestors with fullscreen PASS Nested popover=auto ancestors with fullscreen, top layer element *is* a popover -FAIL Nested popover=auto ancestors, target is outer with dialog assert_equals: Incorrect behavior expected true but got false +PASS Nested popover=auto ancestors, target is outer with dialog PASS Nested popover=auto ancestors, target is outer with dialog, top layer element *is* a popover FAIL Nested popover=auto ancestors, target is outer with fullscreen promise_test: Unhandled rejection with value: object "TypeError: Type error" PASS Nested popover=auto ancestors, target is outer with fullscreen, top layer element *is* a popover -FAIL Top layer inside of nested element with dialog assert_equals: Incorrect behavior expected true but got false +PASS Top layer inside of nested element with dialog PASS Top layer inside of nested element with dialog, top layer element *is* a popover -FAIL Top layer inside of nested element with fullscreen assert_equals: Incorrect behavior expected true but got false +PASS Top layer inside of nested element with fullscreen PASS Top layer inside of nested element with fullscreen, top layer element *is* a popover diff --git a/LayoutTests/imported/w3c/web-platform-tests/html/semantics/popovers/popover-top-layer-nesting-hints.tentative-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/html/semantics/popovers/popover-top-layer-nesting-hints.tentative-expected.txt index 7de7b71729b3b..8261bb761b260 100644 --- a/LayoutTests/imported/w3c/web-platform-tests/html/semantics/popovers/popover-top-layer-nesting-hints.tentative-expected.txt +++ b/LayoutTests/imported/w3c/web-platform-tests/html/semantics/popovers/popover-top-layer-nesting-hints.tentative-expected.txt @@ -8,19 +8,19 @@ PASS Single popover=hint ancestor with dialog PASS Single popover=hint ancestor with dialog, top layer element *is* a popover PASS Single popover=hint ancestor with fullscreen PASS Single popover=hint ancestor with fullscreen, top layer element *is* a popover -FAIL Nested auto/hint ancestors with dialog assert_equals: Incorrect behavior expected true but got false +PASS Nested auto/hint ancestors with dialog PASS Nested auto/hint ancestors with dialog, top layer element *is* a popover FAIL Nested auto/hint ancestors with fullscreen promise_test: Unhandled rejection with value: object "TypeError: Type error" PASS Nested auto/hint ancestors with fullscreen, top layer element *is* a popover -FAIL Nested auto/hint ancestors, target is auto with dialog assert_equals: Incorrect behavior expected true but got false +FAIL Nested auto/hint ancestors, target is auto with dialog assert_equals: Incorrect behavior expected false but got true PASS Nested auto/hint ancestors, target is auto with dialog, top layer element *is* a popover -FAIL Nested auto/hint ancestors, target is auto with fullscreen assert_equals: Incorrect behavior expected true but got false +FAIL Nested auto/hint ancestors, target is auto with fullscreen assert_equals: Incorrect behavior expected false but got true PASS Nested auto/hint ancestors, target is auto with fullscreen, top layer element *is* a popover FAIL Unrelated hint, target=hint with dialog assert_equals: Incorrect behavior expected true but got false PASS Unrelated hint, target=hint with dialog, top layer element *is* a popover FAIL Unrelated hint, target=hint with fullscreen promise_test: Unhandled rejection with value: object "TypeError: Type error" PASS Unrelated hint, target=hint with fullscreen, top layer element *is* a popover -FAIL Unrelated hint, target=auto with dialog assert_equals: Incorrect behavior expected true but got false +FAIL Unrelated hint, target=auto with dialog assert_equals: Incorrect behavior expected false but got true PASS Unrelated hint, target=auto with dialog, top layer element *is* a popover FAIL Unrelated hint, target=auto with fullscreen assert_equals: Incorrect behavior expected true but got false PASS Unrelated hint, target=auto with fullscreen, top layer element *is* a popover diff --git a/Source/WebCore/dom/Element.cpp b/Source/WebCore/dom/Element.cpp index d47bf4a514f2a..834e465d4b7db 100644 --- a/Source/WebCore/dom/Element.cpp +++ b/Source/WebCore/dom/Element.cpp @@ -5850,4 +5850,52 @@ TextStream& operator<<(TextStream& ts, ContentRelevancy relevancy) return ts; } +// https://html.spec.whatwg.org/#topmost-popover-ancestor +// Consider both DOM ancestors and popovers where the given popover was invoked from as ancestors. +// Use top layer positions to disambiguate the topmost one when both exist. +HTMLElement* Element::topmostPopoverAncestor(TopLayerElementType topLayerType) +{ + // Store positions to avoid having to do O(n) search for every popover invoker. + HashMap, size_t> topLayerPositions; + size_t i = 0; + for (auto& element : document().autoPopoverList()) + topLayerPositions.add(element, i++); + + if (topLayerType == TopLayerElementType::Popover) + topLayerPositions.add(*this, i); + + i++; + + RefPtr topmostAncestor; + + auto checkAncestor = [&](Element* candidate) { + if (!candidate) + return; + + // https://html.spec.whatwg.org/#nearest-inclusive-open-popover + auto nearestInclusiveOpenPopover = [](Element& candidate) -> HTMLElement* { + for (RefPtr element = &candidate; element; element = element->parentElementInComposedTree()) { + if (auto* htmlElement = dynamicDowncast(element.get())) { + if (htmlElement->popoverState() == PopoverState::Auto && htmlElement->popoverData()->visibilityState() == PopoverVisibilityState::Showing) + return htmlElement; + } + } + return nullptr; + }; + + auto* candidateAncestor = nearestInclusiveOpenPopover(*candidate); + if (!candidateAncestor) + return; + if (!topmostAncestor || topLayerPositions.get(*topmostAncestor) < topLayerPositions.get(*candidateAncestor)) + topmostAncestor = candidateAncestor; + }; + + checkAncestor(parentElementInComposedTree()); + + if (topLayerType == TopLayerElementType::Popover) + checkAncestor(popoverData()->invoker()); + + return topmostAncestor.get(); +} + } // namespace WebCore diff --git a/Source/WebCore/dom/Element.h b/Source/WebCore/dom/Element.h index bea831d51ef7e..7acef266fff00 100644 --- a/Source/WebCore/dom/Element.h +++ b/Source/WebCore/dom/Element.h @@ -64,6 +64,7 @@ class ElementRareData; class FormAssociatedCustomElement; class FormListedElement; class HTMLDocument; +class HTMLElement; class HTMLFormControlElement; class IntSize; class JSCustomElementInterface; @@ -192,6 +193,9 @@ class Element : public ContainerNode { inline const AtomString& attributeWithDefaultARIA(const QualifiedName&) const; inline String attributeTrimmedWithDefaultARIA(const QualifiedName&) const; + enum class TopLayerElementType : bool { Other, Popover }; + HTMLElement* topmostPopoverAncestor(TopLayerElementType topLayerType); + #if DUMP_NODE_STATISTICS bool hasNamedNodeMap() const; #endif diff --git a/Source/WebCore/dom/FullscreenManager.cpp b/Source/WebCore/dom/FullscreenManager.cpp index 4343be3130e4d..9ba45171fa64d 100644 --- a/Source/WebCore/dom/FullscreenManager.cpp +++ b/Source/WebCore/dom/FullscreenManager.cpp @@ -32,6 +32,7 @@ #include "ChromeClient.h" #include "Document.h" #include "DocumentInlines.h" +#include "Element.h" #include "ElementInlines.h" #include "EventLoop.h" #include "EventNames.h" @@ -545,7 +546,9 @@ bool FullscreenManager::willEnterFullscreen(Element& element, HTMLMediaElementEn } while ((ancestor = ancestor->document().ownerElement())); for (auto ancestor : makeReversedRange(ancestorsInTreeOrder)) { - ancestor->document().hideAllPopoversUntil(nullptr, FocusPreviousElement::No, FireEvents::No); + auto hideUntil = ancestor->topmostPopoverAncestor(Element::TopLayerElementType::Other); + + ancestor->document().hideAllPopoversUntil(hideUntil, FocusPreviousElement::No, FireEvents::No); auto containingBlockBeforeStyleResolution = SingleThreadWeakPtr { }; if (auto* renderer = ancestor->renderer()) diff --git a/Source/WebCore/html/HTMLDialogElement.cpp b/Source/WebCore/html/HTMLDialogElement.cpp index 8202750d1e266..11c147c577300 100644 --- a/Source/WebCore/html/HTMLDialogElement.cpp +++ b/Source/WebCore/html/HTMLDialogElement.cpp @@ -31,6 +31,7 @@ #include "EventLoop.h" #include "EventNames.h" #include "FocusOptions.h" +#include "HTMLElement.h" #include "HTMLNames.h" #include "PopoverData.h" #include "PseudoClassChangeInvalidation.h" @@ -64,7 +65,9 @@ ExceptionOr HTMLDialogElement::show() m_previouslyFocusedElement = document().focusedElement(); - document().hideAllPopoversUntil(nullptr, FocusPreviousElement::No, FireEvents::No); + auto hideUntil = topmostPopoverAncestor(TopLayerElementType::Other); + + document().hideAllPopoversUntil(hideUntil, FocusPreviousElement::No, FireEvents::No); runFocusingSteps(); return { }; @@ -104,7 +107,9 @@ ExceptionOr HTMLDialogElement::showModal() m_previouslyFocusedElement = document().focusedElement(); - document().hideAllPopoversUntil(nullptr, FocusPreviousElement::No, FireEvents::No); + auto hideUntil = topmostPopoverAncestor(TopLayerElementType::Other); + + document().hideAllPopoversUntil(hideUntil, FocusPreviousElement::No, FireEvents::No); runFocusingSteps(); diff --git a/Source/WebCore/html/HTMLElement.cpp b/Source/WebCore/html/HTMLElement.cpp index d9c8d4430b092..90bb1115e6221 100644 --- a/Source/WebCore/html/HTMLElement.cpp +++ b/Source/WebCore/html/HTMLElement.cpp @@ -1300,50 +1300,6 @@ static ExceptionOr checkPopoverValidity(HTMLElement& element, PopoverVisib return true; } -// https://html.spec.whatwg.org/#topmost-popover-ancestor -// Consider both DOM ancestors and popovers where the given popover was invoked from as ancestors. -// Use top layer positions to disambiguate the topmost one when both exist. -static HTMLElement* topmostPopoverAncestor(HTMLElement& newPopover) -{ - // Store positions to avoid having to do O(n) search for every popover invoker. - HashMap, size_t> topLayerPositions; - size_t i = 0; - for (auto& element : newPopover.document().autoPopoverList()) - topLayerPositions.add(element, i++); - - topLayerPositions.add(newPopover, i); - - RefPtr topmostAncestor; - - auto checkAncestor = [&](Element* candidate) { - if (!candidate) - return; - - // https://html.spec.whatwg.org/#nearest-inclusive-open-popover - auto nearestInclusiveOpenPopover = [](Element& candidate) -> HTMLElement* { - for (RefPtr element = &candidate; element; element = element->parentElementInComposedTree()) { - if (auto* htmlElement = dynamicDowncast(element.get())) { - if (htmlElement->popoverState() == PopoverState::Auto && htmlElement->popoverData()->visibilityState() == PopoverVisibilityState::Showing) - return htmlElement; - } - } - return nullptr; - }; - - auto* candidateAncestor = nearestInclusiveOpenPopover(*candidate); - if (!candidateAncestor) - return; - if (!topmostAncestor || topLayerPositions.get(*topmostAncestor) < topLayerPositions.get(*candidateAncestor)) - topmostAncestor = candidateAncestor; - }; - - checkAncestor(newPopover.parentElementInComposedTree()); - - checkAncestor(newPopover.popoverData()->invoker()); - - return topmostAncestor.get(); -} - // https://html.spec.whatwg.org/#popover-focusing-steps static void runPopoverFocusingSteps(HTMLElement& popover) { @@ -1420,8 +1376,8 @@ ExceptionOr HTMLElement::showPopover(const HTMLFormControlElement* invoker if (popoverState() == PopoverState::Auto) { auto originalState = popoverState(); - RefPtr ancestor = topmostPopoverAncestor(*this); - document->hideAllPopoversUntil(ancestor.get(), FocusPreviousElement::No, fireEvents); + auto hideUntil = topmostPopoverAncestor(TopLayerElementType::Popover); + document->hideAllPopoversUntil(hideUntil, FocusPreviousElement::No, fireEvents); if (popoverState() != originalState) return Exception { ExceptionCode::InvalidStateError, "The value of the popover attribute was changed while hiding the popover."_s };