LibWeb: Dispatch mouse events to topmost element instead of hit target

This improves our spec compliance by allowing the user to click
non-element nodes (like text) and having the click be registered with
the parent element (like a div or button). This makes Fandom's cookie
accept button work if you click the text. Additionally, the events test
page contains a test to check the target element, which would previously
not exist when we fired the event at a non-element.
This commit is contained in:
kleines Filmröllchen 2022-05-03 16:50:38 +02:00 committed by Linus Groh
parent a33b9a8bca
commit a7a5721149
2 changed files with 71 additions and 3 deletions

View file

@ -16,5 +16,33 @@
</head>
<body>
<div id="my_div">Hello there!</div>
<div style="border: 1px solid black; width: 500px; height: 200px" id="divel">
CLICK ME CLICK ME CLICK ME CLICK ME CLICK ME CLICK ME CLICK ME CLICK ME CLICK ME CLICK
ME CLICK ME CLICK ME CLICK ME CLICK ME CLICK ME CLICK ME CLICK ME CLICK ME CLICK ME
CLICK ME CLICK ME CLICK ME CLICK ME CLICK ME CLICK ME
</div>
<p id="result">This text should be green, whether you click on the div border or the div text.</p>
<p>
<script>
const divel = document.getElementById("divel");
const result = document.getElementById("result");
divel.addEventListener("click", event => {
try {
const text = `Result: Clicked on divel element with id ${event.target.getAttribute(
"id"
)}`;
console.log(text);
result.innerText = text;
result.style.setProperty("color", "green");
} catch (e) {
const text = `Result: ${e.message}`;
console.error(text);
result.innerText = text;
result.style.setProperty("color", "red");
}
});
</script>
</p>
</body>
</html>

View file

@ -190,7 +190,21 @@ bool EventHandler::handle_mouseup(Gfx::IntPoint const& position, unsigned button
return nested_browsing_context->event_handler().handle_mouseup(position.translated(compute_mouse_event_offset({}, paintable->layout_node())), button, modifiers);
return false;
}
auto offset = compute_mouse_event_offset(position, paintable->layout_node());
// Search for the first parent of the hit target that's an element.
// "The click event type MUST be dispatched on the topmost event target indicated by the pointer." (https://www.w3.org/TR/uievents/#event-type-click)
// "The topmost event target MUST be the element highest in the rendering order which is capable of being an event target." (https://www.w3.org/TR/uievents/#topmost-event-target)
auto* layout_node = &paintable->layout_node();
while (layout_node && node && !node->is_element() && layout_node->parent()) {
layout_node = layout_node->parent();
node = layout_node->dom_node();
}
if (!node || !layout_node) {
// FIXME: This is pretty ugly but we need to bail out here.
goto after_node_use;
}
auto offset = compute_mouse_event_offset(position, *layout_node);
node->dispatch_event(UIEvents::MouseEvent::create_from_platform_event(UIEvents::EventNames::mouseup, offset.x(), offset.y(), position.x(), position.y(), button));
handled_event = true;
@ -251,6 +265,7 @@ bool EventHandler::handle_mouseup(Gfx::IntPoint const& position, unsigned button
}
}
after_node_use:
if (button == GUI::MouseButton::Primary)
m_in_mouse_selection = false;
return handled_event;
@ -306,8 +321,19 @@ bool EventHandler::handle_mousedown(Gfx::IntPoint const& position, unsigned butt
if (auto* page = m_browsing_context.page())
page->set_focused_browsing_context({}, m_browsing_context);
// Search for the first parent of the hit target that's an element.
// "The click event type MUST be dispatched on the topmost event target indicated by the pointer." (https://www.w3.org/TR/uievents/#event-type-click)
// "The topmost event target MUST be the element highest in the rendering order which is capable of being an event target." (https://www.w3.org/TR/uievents/#topmost-event-target)
auto* layout_node = &paintable->layout_node();
while (layout_node && node && !node->is_element() && layout_node->parent()) {
layout_node = layout_node->parent();
node = layout_node->dom_node();
}
if (!node || !layout_node)
return false;
m_mousedown_target = node;
auto offset = compute_mouse_event_offset(position, paintable->layout_node());
auto offset = compute_mouse_event_offset(position, *layout_node);
node->dispatch_event(UIEvents::MouseEvent::create_from_platform_event(UIEvents::EventNames::mousedown, offset.x(), offset.y(), position.x(), position.y(), button));
}
@ -415,12 +441,26 @@ bool EventHandler::handle_mousemove(Gfx::IntPoint const& position, unsigned butt
hovered_node_cursor = cursor_css_to_gfx(cursor);
}
auto offset = compute_mouse_event_offset(position, paintable->layout_node());
// Search for the first parent of the hit target that's an element.
// "The click event type MUST be dispatched on the topmost event target indicated by the pointer." (https://www.w3.org/TR/uievents/#event-type-click)
// "The topmost event target MUST be the element highest in the rendering order which is capable of being an event target." (https://www.w3.org/TR/uievents/#topmost-event-target)
auto* layout_node = &paintable->layout_node();
while (layout_node && node && !node->is_element() && layout_node->parent()) {
layout_node = layout_node->parent();
node = layout_node->dom_node();
}
if (!node || !layout_node) {
// FIXME: This is pretty ugly but we need to bail out here.
goto after_node_use;
}
auto offset = compute_mouse_event_offset(position, *layout_node);
node->dispatch_event(UIEvents::MouseEvent::create_from_platform_event(UIEvents::EventNames::mousemove, offset.x(), offset.y(), position.x(), position.y()));
// NOTE: Dispatching an event may have disturbed the world.
if (!paint_root() || paint_root() != node->document().paint_box())
return true;
}
after_node_use:
if (m_in_mouse_selection) {
auto hit = paint_root()->hit_test(position.to_type<float>(), Painting::HitTestType::TextCursor);
if (start_index.has_value() && hit.has_value() && hit->dom_node()) {