Skip to content

Commit

Permalink
Allow top layer elements to be nested within popovers
Browse files Browse the repository at this point in the history
https://bugs.webkit.org/show_bug.cgi?id=269928

Reviewed by Tim Nguyen.

This implements the changes to the spec as defined by
whatwg/html#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
  • Loading branch information
keithamus committed Jun 10, 2024
1 parent 0f1d0a1 commit 7742091
Show file tree
Hide file tree
Showing 8 changed files with 87 additions and 71 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Original file line number Diff line number Diff line change
Expand Up @@ -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

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
48 changes: 48 additions & 0 deletions Source/WebCore/dom/Element.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<Ref<const Element>, 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<HTMLElement> 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<HTMLElement>(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
4 changes: 4 additions & 0 deletions Source/WebCore/dom/Element.h
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ class ElementRareData;
class FormAssociatedCustomElement;
class FormListedElement;
class HTMLDocument;
class HTMLElement;
class HTMLFormControlElement;
class IntSize;
class JSCustomElementInterface;
Expand Down Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion Source/WebCore/dom/FullscreenManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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<RenderBlock> { };
if (auto* renderer = ancestor->renderer())
Expand Down
9 changes: 7 additions & 2 deletions Source/WebCore/html/HTMLDialogElement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -64,7 +65,9 @@ ExceptionOr<void> 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 { };
Expand Down Expand Up @@ -104,7 +107,9 @@ ExceptionOr<void> 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();

Expand Down
48 changes: 2 additions & 46 deletions Source/WebCore/html/HTMLElement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1300,50 +1300,6 @@ static ExceptionOr<bool> 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<Ref<const HTMLElement>, size_t> topLayerPositions;
size_t i = 0;
for (auto& element : newPopover.document().autoPopoverList())
topLayerPositions.add(element, i++);

topLayerPositions.add(newPopover, i);

RefPtr<HTMLElement> 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<HTMLElement>(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)
{
Expand Down Expand Up @@ -1420,8 +1376,8 @@ ExceptionOr<void> 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 };
Expand Down

0 comments on commit 7742091

Please sign in to comment.