LibWeb+WebContent: Rename Document::focused_element to ::focused_area

And make it a DOM::Node, not DOM::Element. This makes everything flow
much better, such as spec texts that explicitly mention "focused area"
as the fact that we don't necessarily need to traverse a tree of
elements, since a Node can be focusable as well.

Eventually this will need to be a struct with a separate "focused area"
and "DOM anchor", but this change will make it easier to achieve that.
This commit is contained in:
Jelle Raaijmakers 2025-08-21 17:09:40 +02:00 committed by Jelle Raaijmakers
commit 518c048eb4
Notes: github-actions[bot] 2025-08-26 08:27:18 +00:00
11 changed files with 92 additions and 91 deletions

View file

@ -552,8 +552,8 @@ static inline bool matches_pseudo_class(CSS::Selector::SimpleSelector::PseudoCla
case CSS::PseudoClass::FocusVisible:
return element.is_focused() && element.should_indicate_focus();
case CSS::PseudoClass::FocusWithin: {
auto* focused_element = element.document().focused_element();
return focused_element && element.is_inclusive_ancestor_of(*focused_element);
auto focused_area = element.document().focused_area();
return focused_area && element.is_inclusive_ancestor_of(*focused_area);
}
case CSS::PseudoClass::FirstChild:
if (context.collect_per_element_selector_involvement_metadata) {

View file

@ -547,7 +547,7 @@ void Document::visit_edges(Cell::Visitor& visitor)
visitor.visit(m_highlighted_node);
visitor.visit(m_active_favicon);
visitor.visit(m_browsing_context);
visitor.visit(m_focused_element);
visitor.visit(m_focused_area);
visitor.visit(m_active_element);
visitor.visit(m_target_element);
visitor.visit(m_implementation);
@ -2443,7 +2443,7 @@ String const& Document::compat_mode() const
void Document::update_active_element()
{
// 1. Let candidate be this's node document's focused area's DOM anchor.
Node* candidate = focused_element();
Node* candidate = focused_area();
// 2. Set candidate to the result of retargeting candidate against this.
candidate = as<Node>(retarget(candidate, this));
@ -2478,56 +2478,57 @@ void Document::update_active_element()
set_active_element(nullptr);
}
void Document::set_focused_element(GC::Ptr<Element> element)
void Document::set_focused_area(GC::Ptr<Node> node)
{
if (m_focused_element.ptr() == element)
if (m_focused_area == node)
return;
GC::Ptr<Element> old_focused_element = move(m_focused_element);
GC::Ptr old_focused_area = m_focused_area;
if (old_focused_element)
if (auto* old_focused_element = as_if<Element>(old_focused_area.ptr()))
old_focused_element->did_lose_focus();
auto* common_ancestor = find_common_ancestor(old_focused_element, element);
auto* common_ancestor = find_common_ancestor(old_focused_area, node);
GC::Ptr<Node> old_focused_node_root = nullptr;
GC::Ptr<Node> new_focused_node_root = nullptr;
if (old_focused_element)
old_focused_node_root = old_focused_element->root();
if (element)
new_focused_node_root = element->root();
if (old_focused_area)
old_focused_node_root = old_focused_area->root();
if (node)
new_focused_node_root = node->root();
if (old_focused_node_root != new_focused_node_root) {
if (old_focused_node_root) {
invalidate_style_for_elements_affected_by_pseudo_class_change(CSS::PseudoClass::Focus, m_focused_element, *old_focused_node_root, element);
invalidate_style_for_elements_affected_by_pseudo_class_change(CSS::PseudoClass::FocusWithin, m_focused_element, *old_focused_node_root, element);
invalidate_style_for_elements_affected_by_pseudo_class_change(CSS::PseudoClass::FocusVisible, m_focused_element, *old_focused_node_root, element);
invalidate_style_for_elements_affected_by_pseudo_class_change(CSS::PseudoClass::Focus, m_focused_area, *old_focused_node_root, node);
invalidate_style_for_elements_affected_by_pseudo_class_change(CSS::PseudoClass::FocusWithin, m_focused_area, *old_focused_node_root, node);
invalidate_style_for_elements_affected_by_pseudo_class_change(CSS::PseudoClass::FocusVisible, m_focused_area, *old_focused_node_root, node);
}
if (new_focused_node_root) {
invalidate_style_for_elements_affected_by_pseudo_class_change(CSS::PseudoClass::Focus, m_focused_element, *new_focused_node_root, element);
invalidate_style_for_elements_affected_by_pseudo_class_change(CSS::PseudoClass::FocusWithin, m_focused_element, *new_focused_node_root, element);
invalidate_style_for_elements_affected_by_pseudo_class_change(CSS::PseudoClass::FocusVisible, m_focused_element, *new_focused_node_root, element);
invalidate_style_for_elements_affected_by_pseudo_class_change(CSS::PseudoClass::Focus, m_focused_area, *new_focused_node_root, node);
invalidate_style_for_elements_affected_by_pseudo_class_change(CSS::PseudoClass::FocusWithin, m_focused_area, *new_focused_node_root, node);
invalidate_style_for_elements_affected_by_pseudo_class_change(CSS::PseudoClass::FocusVisible, m_focused_area, *new_focused_node_root, node);
}
} else {
invalidate_style_for_elements_affected_by_pseudo_class_change(CSS::PseudoClass::Focus, m_focused_element, *common_ancestor, element);
invalidate_style_for_elements_affected_by_pseudo_class_change(CSS::PseudoClass::FocusWithin, m_focused_element, *common_ancestor, element);
invalidate_style_for_elements_affected_by_pseudo_class_change(CSS::PseudoClass::FocusVisible, m_focused_element, *common_ancestor, element);
invalidate_style_for_elements_affected_by_pseudo_class_change(CSS::PseudoClass::Focus, m_focused_area, *common_ancestor, node);
invalidate_style_for_elements_affected_by_pseudo_class_change(CSS::PseudoClass::FocusWithin, m_focused_area, *common_ancestor, node);
invalidate_style_for_elements_affected_by_pseudo_class_change(CSS::PseudoClass::FocusVisible, m_focused_area, *common_ancestor, node);
}
m_focused_element = element;
m_focused_area = node;
if (m_focused_element)
m_focused_element->did_receive_focus();
auto* new_focused_element = as_if<Element>(node.ptr());
if (new_focused_element)
new_focused_element->did_receive_focus();
if (paintable())
paintable()->set_needs_display();
// Scroll the viewport if necessary to make the newly focused element visible.
if (m_focused_element) {
m_focused_element->queue_an_element_task(HTML::Task::Source::UserInteraction, [&]() {
if (new_focused_element) {
new_focused_element->queue_an_element_task(HTML::Task::Source::UserInteraction, [&] {
ScrollIntoViewOptions scroll_options;
scroll_options.block = Bindings::ScrollLogicalPosition::Nearest;
scroll_options.inline_ = Bindings::ScrollLogicalPosition::Nearest;
(void)m_focused_element->scroll_into_view(scroll_options);
(void)as<Element>(*m_focused_area).scroll_into_view(scroll_options);
});
}
@ -6338,38 +6339,38 @@ GC::Ref<Document> Document::parse_html_unsafe(JS::VM& vm, StringView html)
InputEventsTarget* Document::active_input_events_target()
{
auto* focused_element = this->focused_element();
if (!focused_element)
auto focused_area = this->focused_area();
if (!focused_area)
return {};
if (is<HTML::HTMLInputElement>(*focused_element))
return static_cast<HTML::HTMLInputElement*>(focused_element);
if (is<HTML::HTMLTextAreaElement>(*focused_element))
return static_cast<HTML::HTMLTextAreaElement*>(focused_element);
if (focused_element->is_editable_or_editing_host())
if (auto* input_element = as_if<HTML::HTMLInputElement>(*focused_area))
return input_element;
if (auto* text_area_element = as_if<HTML::HTMLTextAreaElement>(*focused_area))
return text_area_element;
if (focused_area->is_editable_or_editing_host())
return m_editing_host_manager;
return nullptr;
}
GC::Ptr<DOM::Position> Document::cursor_position() const
{
auto const* focused_element = this->focused_element();
if (!focused_element)
auto const focused_area = this->focused_area();
if (!focused_area)
return nullptr;
Optional<HTML::FormAssociatedTextControlElement const&> target {};
if (auto const* input_element = as_if<HTML::HTMLInputElement>(*focused_element)) {
if (auto const* input_element = as_if<HTML::HTMLInputElement>(*focused_area)) {
// Some types of <input> tags shouldn't have a cursor, like buttons
if (!input_element->can_have_text_editing_cursor())
return nullptr;
target = *input_element;
} else if (is<HTML::HTMLTextAreaElement>(*focused_element))
target = static_cast<HTML::HTMLTextAreaElement const&>(*focused_element);
} else if (is<HTML::HTMLTextAreaElement>(*focused_area))
target = static_cast<HTML::HTMLTextAreaElement const&>(*focused_area);
if (target.has_value())
return target->cursor_position();
if (focused_element->is_editable_or_editing_host())
if (focused_area->is_editable_or_editing_host())
return m_selection->cursor_position();
return nullptr;

View file

@ -35,7 +35,6 @@
#include <LibWeb/HTML/Focus.h>
#include <LibWeb/HTML/HTMLScriptElement.h>
#include <LibWeb/HTML/History.h>
#include <LibWeb/HTML/LazyLoadingElement.h>
#include <LibWeb/HTML/NavigationType.h>
#include <LibWeb/HTML/SandboxingFlagSet.h>
#include <LibWeb/HTML/Scripting/Environments.h>
@ -431,10 +430,10 @@ public:
void set_editable(bool editable) { m_editable = editable; }
Element* focused_element() { return m_focused_element.ptr(); }
Element const* focused_element() const { return m_focused_element.ptr(); }
void set_focused_element(GC::Ptr<Element>);
// // https://html.spec.whatwg.org/multipage/interaction.html#focused-area-of-the-document
GC::Ptr<Node> focused_area() { return m_focused_area; }
GC::Ptr<Node const> focused_area() const { return m_focused_area; }
void set_focused_area(GC::Ptr<Node>);
HTML::FocusTrigger last_focus_trigger() const { return m_last_focus_trigger; }
void set_last_focus_trigger(HTML::FocusTrigger trigger) { m_last_focus_trigger = trigger; }
@ -1017,7 +1016,9 @@ private:
bool m_editable { false };
GC::Ptr<Element> m_focused_element;
// https://html.spec.whatwg.org/multipage/interaction.html#focused-area-of-the-document
GC::Ptr<Node> m_focused_area;
HTML::FocusTrigger m_last_focus_trigger { HTML::FocusTrigger::Other };
GC::Ptr<Element> m_active_element;

View file

@ -1077,7 +1077,7 @@ WebIDL::ExceptionOr<String> Element::inner_html() const
bool Element::is_focused() const
{
return document().focused_element() == this;
return document().focused_area() == this;
}
bool Element::is_active() const
@ -3291,8 +3291,8 @@ bool Element::is_relevant_to_the_user()
return true;
// Either the element or its contents are focused, as described in the focus section of the HTML spec.
auto* focused_element = document().focused_element();
if (focused_element && is_inclusive_ancestor_of(*focused_element))
auto focused_area = document().focused_area();
if (focused_area && is_inclusive_ancestor_of(*focused_area))
return true;
// Either the element or its contents are selected, where selection is described in the selection API.

View file

@ -113,9 +113,9 @@ static void run_focus_update_steps(Vector<GC::Root<DOM::Node>> old_chain, Vector
// 1. If entry is a focusable area: designate entry as the focused area of the document.
// FIXME: This isn't entirely right.
if (is<DOM::Element>(*entry))
entry->document().set_focused_element(&static_cast<DOM::Element&>(*entry));
entry->document().set_focused_area(*entry);
else if (is<DOM::Document>(*entry))
entry->document().set_focused_element(static_cast<DOM::Document&>(*entry).document_element());
entry->document().set_focused_area(static_cast<DOM::Document&>(*entry).document_element());
GC::Ptr<DOM::EventTarget> focus_event_target;
if (is<DOM::Element>(*entry)) {

View file

@ -1369,17 +1369,17 @@ GC::Ptr<DOM::Node> TraversableNavigable::currently_focused_area()
// 3. While candidate's focused area is a navigable container with a non-null content navigable:
// set candidate to the active document of that navigable container's content navigable.
while (candidate->focused_element()
&& is<HTML::NavigableContainer>(candidate->focused_element())
&& static_cast<HTML::NavigableContainer&>(*candidate->focused_element()).content_navigable()) {
candidate = static_cast<HTML::NavigableContainer&>(*candidate->focused_element()).content_navigable()->active_document();
while (candidate->focused_area()
&& is<NavigableContainer>(candidate->focused_area().ptr())
&& as<NavigableContainer>(*candidate->focused_area()).content_navigable()) {
candidate = as<NavigableContainer>(*candidate->focused_area()).content_navigable()->active_document();
}
// 4. If candidate's focused area is non-null, set candidate to candidate's focused area.
if (candidate->focused_element()) {
if (candidate->focused_area()) {
// NOTE: We return right away here instead of assigning to candidate,
// since that would require compromising type safety.
return candidate->focused_element();
return candidate->focused_area();
}
// 5. Return candidate.

View file

@ -9,9 +9,7 @@
#include <AK/Badge.h>
#include <AK/RefPtr.h>
#include <AK/TypeCasts.h>
#include <LibGC/Heap.h>
#include <LibURL/URL.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/Bindings/WindowGlobalMixin.h>
#include <LibWeb/DOM/EventTarget.h>
@ -22,6 +20,7 @@
#include <LibWeb/HTML/GlobalEventHandlers.h>
#include <LibWeb/HTML/MimeType.h>
#include <LibWeb/HTML/Navigable.h>
#include <LibWeb/HTML/Navigation.h>
#include <LibWeb/HTML/Plugin.h>
#include <LibWeb/HTML/ScrollOptions.h>
#include <LibWeb/HTML/StructuredSerializeOptions.h>

View file

@ -683,8 +683,8 @@ EventResult EventHandler::handle_mousedown(CSSPixelPoint viewport_position, CSSP
// Spec Note: Note that focusing is not an activation behavior, i.e. calling the click() method on an element or dispatching a synthetic click event on it won't cause the element to get focused.
if (focus_candidate)
HTML::run_focusing_steps(focus_candidate, nullptr, HTML::FocusTrigger::Click);
else if (auto* focused_element = document->focused_element())
HTML::run_unfocusing_steps(focused_element);
else if (auto focused_area = document->focused_area())
HTML::run_unfocusing_steps(focused_area);
// https://drafts.csswg.org/css-ui/#valdef-user-select-none
// Attempting to start a selection in an element where user-select is none, such as by clicking in it or starting
@ -1066,17 +1066,17 @@ EventResult EventHandler::focus_next_element()
return EventResult::Dropped;
};
auto* element = m_navigable->active_document()->focused_element();
if (!element)
auto node = m_navigable->active_document()->focused_area();
if (!node)
return set_focus_to_first_focusable_element();
for (element = element->next_element_in_pre_order(); element && !element->is_focusable(); element = element->next_element_in_pre_order())
for (node = node->next_in_pre_order(); node && !node->is_focusable(); node = node->next_in_pre_order())
;
if (!element)
if (!node)
return set_focus_to_first_focusable_element();
HTML::run_focusing_steps(element, nullptr, HTML::FocusTrigger::Key);
HTML::run_focusing_steps(node, nullptr, HTML::FocusTrigger::Key);
return EventResult::Handled;
}
@ -1101,17 +1101,17 @@ EventResult EventHandler::focus_previous_element()
return EventResult::Dropped;
};
auto* element = m_navigable->active_document()->focused_element();
if (!element)
auto node = m_navigable->active_document()->focused_area();
if (!node)
return set_focus_to_last_focusable_element();
for (element = element->previous_element_in_pre_order(); element && !element->is_focusable(); element = element->previous_element_in_pre_order())
for (node = node->previous_in_pre_order(); node && !node->is_focusable(); node = node->previous_in_pre_order())
;
if (!element)
if (!node)
return set_focus_to_last_focusable_element();
HTML::run_focusing_steps(element, nullptr, HTML::FocusTrigger::Key);
HTML::run_focusing_steps(node, nullptr, HTML::FocusTrigger::Key);
return EventResult::Handled;
}
@ -1132,15 +1132,15 @@ EventResult EventHandler::fire_keyboard_event(FlyString const& event_name, HTML:
if (!document->is_fully_active())
return EventResult::Dropped;
if (GC::Ptr<DOM::Element> focused_element = document->focused_element()) {
if (is<HTML::NavigableContainer>(*focused_element)) {
auto& navigable_container = as<HTML::NavigableContainer>(*focused_element);
if (GC::Ptr focused_area = document->focused_area()) {
if (is<HTML::NavigableContainer>(*focused_area)) {
auto& navigable_container = as<HTML::NavigableContainer>(*focused_area);
if (navigable_container.content_navigable())
return fire_keyboard_event(event_name, *navigable_container.content_navigable(), key, modifiers, code_point, repeat);
}
auto event = UIEvents::KeyboardEvent::create_from_platform_event(document->realm(), event_name, key, modifiers, code_point, repeat);
return focused_element->dispatch_event(event) ? EventResult::Accepted : EventResult::Cancelled;
return focused_area->dispatch_event(event) ? EventResult::Accepted : EventResult::Cancelled;
}
// FIXME: De-duplicate this. This is just to prevent wasting a KeyboardEvent allocation when recursing into an (i)frame.
@ -1207,15 +1207,15 @@ EventResult EventHandler::input_event(FlyString const& event_name, FlyString con
input_event_init.input_type = input_type;
if (auto* focused_element = document->focused_element()) {
if (is<HTML::NavigableContainer>(*focused_element)) {
auto& navigable_container = as<HTML::NavigableContainer>(*focused_element);
if (auto focused_area = document->focused_area()) {
if (is<HTML::NavigableContainer>(*focused_area)) {
auto& navigable_container = as<HTML::NavigableContainer>(*focused_area);
if (navigable_container.content_navigable())
return input_event(event_name, input_type, *navigable_container.content_navigable(), move(code_point_or_string));
}
auto event = UIEvents::InputEvent::create_from_platform_event(document->realm(), event_name, input_event_init, target_ranges_for_input_event(*document));
return focused_element->dispatch_event(event) ? EventResult::Accepted : EventResult::Cancelled;
return focused_area->dispatch_event(event) ? EventResult::Accepted : EventResult::Cancelled;
}
auto event = UIEvents::InputEvent::create_from_platform_event(document->realm(), event_name, input_event_init, target_ranges_for_input_event(*document));
@ -1270,8 +1270,8 @@ EventResult EventHandler::handle_keydown(UIEvents::KeyCode key, u32 modifiers, u
// instead interpret this interaction as some other action, instead of interpreting it as a close request.
}
auto* focused_element = m_navigable->active_document()->focused_element();
if (auto* media_element = as_if<HTML::HTMLMediaElement>(focused_element)) {
auto focused_area = m_navigable->active_document()->focused_area();
if (auto* media_element = as_if<HTML::HTMLMediaElement>(focused_area.ptr())) {
if (media_element->handle_keydown({}, key, modifiers).release_value_but_fixme_should_propagate_errors())
return EventResult::Handled;
}
@ -1347,8 +1347,8 @@ EventResult EventHandler::handle_keydown(UIEvents::KeyCode key, u32 modifiers, u
auto input_type = modifiers == UIEvents::Mod_Shift ? UIEvents::InputTypes::insertLineBreak : UIEvents::InputTypes::insertParagraph;
// If the editing host is contenteditable="plaintext-only", we force a line break.
if (focused_element) {
if (auto editing_host = focused_element->editing_host(); editing_host
if (focused_area) {
if (auto editing_host = focused_area->editing_host(); editing_host
&& as<HTML::HTMLElement>(*editing_host).content_editable_state() == HTML::ContentEditableState::PlaintextOnly)
input_type = UIEvents::InputTypes::insertLineBreak;
}

View file

@ -180,12 +180,12 @@ Gfx::Orientation PaintableFragment::orientation() const
CSSPixelRect PaintableFragment::selection_rect() const
{
if (auto const* focused_element = paintable().document().focused_element(); focused_element && is<HTML::FormAssociatedTextControlElement>(*focused_element)) {
if (auto focused_area = paintable().document().focused_area(); is<HTML::FormAssociatedTextControlElement>(focused_area.ptr())) {
HTML::FormAssociatedTextControlElement const* text_control_element = nullptr;
if (is<HTML::HTMLInputElement>(*focused_element)) {
text_control_element = static_cast<HTML::HTMLInputElement const*>(focused_element);
} else if (is<HTML::HTMLTextAreaElement>(*focused_element)) {
text_control_element = static_cast<HTML::HTMLTextAreaElement const*>(focused_element);
if (auto const* input_element = as_if<HTML::HTMLInputElement>(*focused_area)) {
text_control_element = input_element;
} else if (auto const* text_area_element = as_if<HTML::HTMLTextAreaElement>(*focused_area)) {
text_control_element = text_area_element;
} else {
VERIFY_NOT_REACHED();
}

View file

@ -552,7 +552,7 @@ void Selection::set_range(GC::Ptr<DOM::Range> range)
GC::Ref new_editing_host = *range->start_container()->editing_host();
while (new_editing_host->parent() && new_editing_host->parent()->is_editing_host())
new_editing_host = *new_editing_host->parent();
if (document()->focused_element() != new_editing_host) {
if (document()->focused_area() != new_editing_host) {
// FIXME: Determine and propagate the right focus trigger.
HTML::run_focusing_steps(new_editing_host, nullptr, HTML::FocusTrigger::Other);
}

View file

@ -1961,7 +1961,7 @@ Web::WebDriver::Response WebDriverConnection::element_send_keys_impl(StringView
else if (is<Web::HTML::HTMLElement>(*element) && static_cast<Web::HTML::HTMLElement&>(*element).is_content_editable()) {
// If element does not currently have focus, set the text insertion caret after any child content.
auto* document = current_browsing_context().active_document();
document->set_focused_element(element);
document->set_focused_area(element);
}
// -> otherwise
else if (is<Web::HTML::FormAssociatedTextControlElement>(*element)) {