LibWeb: Implement autofocus candidate processing

This change implements the algorithms necessary to focus elements with
the autofocus attribute on page load.
This commit is contained in:
Tim Ledbetter 2026-04-21 18:17:38 +01:00 committed by Shannon Booth
parent baefb51902
commit e5d615cb11
19 changed files with 345 additions and 4 deletions

View file

@ -597,6 +597,7 @@ void Document::visit_edges(Cell::Visitor& visitor)
visitor.visit(m_focused_area);
visitor.visit(m_active_element);
visitor.visit(m_target_element);
visitor.visit(m_autofocus_candidates);
visitor.visit(m_implementation);
visitor.visit(m_current_script);
visitor.visit(m_associated_inert_template_document);
@ -2928,6 +2929,106 @@ void Document::set_target_element(GC::Ptr<Element> element)
set_needs_repaint();
}
// https://html.spec.whatwg.org/multipage/interaction.html#flush-autofocus-candidates
void Document::flush_autofocus_candidates()
{
// 1. If topDocument's autofocus processed flag is true, then return.
if (m_autofocus_processed_flag)
return;
// 2. Let candidates be topDocument's autofocus candidates.
auto& candidates = m_autofocus_candidates;
// 3. If candidates is empty, then return.
if (candidates.is_empty())
return;
// 4. If topDocument's focused area is not topDocument itself, or topDocument has non-null target element, then:
if ((m_focused_area && m_focused_area.ptr() != this) || m_target_element) {
// 1. Empty candidates.
candidates.clear();
// 2. Set topDocument's autofocus processed flag to true.
m_autofocus_processed_flag = true;
// 3. Return.
return;
}
// 5. While candidates is not empty:
while (!candidates.is_empty()) {
// 1. Let element be candidates[0].
GC::Ref<Element> element = candidates.first();
// 2. Let doc be element's node document.
auto& doc = element->document();
// 3. If doc is not fully active, then remove element from candidates, and continue.
if (!doc.is_fully_active()) {
candidates.take_first();
continue;
}
// 4. If doc's node navigable's top-level traversable is not the same as topDocument's node navigable, then
// remove element from candidates, and continue.
auto doc_navigable = doc.navigable();
if (!doc_navigable || doc_navigable->top_level_traversable() != navigable()) {
candidates.take_first();
continue;
}
// 5. If doc's script-blocking style sheet set is not empty, then return.
if (!doc.script_blocking_style_sheet_set().is_empty())
return;
// 6. Remove element from candidates.
candidates.take_first();
// 7. Let inclusiveAncestorDocuments be a list consisting of the active document of doc's inclusive ancestor navigables.
GC::RootVector<GC::Ref<Document>> inclusive_ancestor_documents(heap());
inclusive_ancestor_documents.append(doc);
auto ancestor_navigable = doc_navigable->parent();
while (ancestor_navigable) {
if (auto active = ancestor_navigable->active_document())
inclusive_ancestor_documents.append(*active);
ancestor_navigable = ancestor_navigable->parent();
}
// 8. If any Document in inclusiveAncestorDocuments has non-null target element, then continue.
auto any_ancestor_has_target = false;
for (auto& ancestor : inclusive_ancestor_documents) {
if (ancestor->target_element()) {
any_ancestor_has_target = true;
break;
}
}
if (any_ancestor_has_target)
continue;
// 9. Let target be element.
GC::Ptr<Element> target = element;
// FIXME: 10. If target is not a focusable area, then set target to the result of getting the
// focusable area for target.
// AD-HOC: We don't implement "get the focusable area" so for now, treat unconnected and non-focusable elements
// as having no focusable area.
if (!target->is_connected() || !target->is_focusable())
target = nullptr;
// 11. If target is not null, then:
if (target) {
// 1. Empty candidates.
candidates.clear();
// 2. Set topDocument's autofocus processed flag to true.
m_autofocus_processed_flag = true;
// 3. Run the focusing steps for target.
HTML::run_focusing_steps(target.ptr());
}
}
}
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#the-indicated-part-of-the-document
Document::IndicatedPart Document::determine_the_indicated_part() const
{

View file

@ -499,6 +499,11 @@ public:
Element const* target_element() const { return m_target_element.ptr(); }
void set_target_element(GC::Ptr<Element>);
Vector<GC::Ref<Element>>& autofocus_candidates() { return m_autofocus_candidates; }
bool autofocus_processed_flag() const { return m_autofocus_processed_flag; }
void set_autofocus_processed_flag(bool value) { m_autofocus_processed_flag = value; }
void flush_autofocus_candidates();
void try_to_scroll_to_the_fragment();
void scroll_to_the_fragment();
void scroll_to_the_beginning_of_the_document();
@ -1202,6 +1207,12 @@ private:
GC::Ptr<Element> m_active_element;
GC::Ptr<Element> m_target_element;
// https://html.spec.whatwg.org/multipage/interaction.html#autofocus-candidates
Vector<GC::Ref<Element>> m_autofocus_candidates;
// https://html.spec.whatwg.org/multipage/interaction.html#autofocus-processed-flag
bool m_autofocus_processed_flag { false };
bool m_created_for_appropriate_template_contents { false };
GC::Ptr<Document> m_associated_inert_template_document;
GC::Ptr<Document> m_appropriate_template_contents_owner_document;

View file

@ -365,7 +365,12 @@ void EventLoop::update_the_rendering()
// FIXME: 6. For each doc of docs, reveal doc.
// FIXME: 7. For each doc of docs, flush autofocus candidates for doc if its node navigable is a top-level traversable.
// 7. For each doc of docs, flush autofocus candidates for doc if its node navigable is a top-level traversable.
for (auto& document : docs) {
auto navigable = document->navigable();
if (navigable && navigable->is_top_level_traversable())
document->flush_autofocus_candidates();
}
// 8. For each doc of docs, run the resize steps for doc. [CSSOMVIEW]
for (auto& document : docs) {

View file

@ -9,7 +9,10 @@
#include <LibWeb/HTML/Focus.h>
#include <LibWeb/HTML/HTMLElement.h>
#include <LibWeb/HTML/HTMLOrSVGElement.h>
#include <LibWeb/HTML/Navigable.h>
#include <LibWeb/HTML/PolicyContainers.h>
#include <LibWeb/HTML/SandboxingFlagSet.h>
#include <LibWeb/HTML/TraversableNavigable.h>
#include <LibWeb/MathML/MathMLElement.h>
#include <LibWeb/SVG/SVGElement.h>
@ -108,6 +111,41 @@ void HTMLOrSVGElement<ElementBase>::inserted()
// 2.3. Set element's [[CryptographicNonce]] to nonce.
m_cryptographic_nonce = nonce;
}
// https://html.spec.whatwg.org/multipage/interaction.html#the-autofocus-attribute
if (element.has_attribute(HTML::AttributeNames::autofocus)) {
// When an element with the autofocus attribute specified is inserted into a document, run the following steps:
// FIXME: 1. If the user has indicated (for example, by starting to type in a form control) that they do not
// wish focus to be changed, then optionally return.
// 2. Let target be the element's node document.
auto& target = element.document();
// 3. If target is not fully active, then return.
if (!target.is_fully_active())
return;
// 4. If target's active sandboxing flag set has the sandboxed automatic features browsing
// context flag, then return.
if (has_flag(target.active_sandboxing_flag_set(), HTML::SandboxingFlagSet::SandboxedAutomaticFeatures))
return;
// 5. If the allow focus steps given target return false, then return.
if (!target.allow_focus())
return;
// 6. Let topDocument be target's node navigable's top-level traversable's active document.
auto top_document = target.navigable()->top_level_traversable()->active_document();
// 7. If topDocument's autofocus processed flag is false, then remove the element from topDocument's autofocus
// candidates, and append the element to topDocument's autofocus candidates.
if (!top_document->autofocus_processed_flag()) {
auto& candidates = top_document->autofocus_candidates();
candidates.remove_first_matching([&element](auto const& other) { return other.ptr() == &element; });
candidates.append(GC::Ref { element });
}
}
}
template<typename ElementBase>

View file

@ -0,0 +1 @@
activeElement.id=second

View file

@ -2,7 +2,6 @@ Harness status: OK
Found 2 tests
1 Pass
1 Fail
2 Pass
Pass ":focus-visible" should be a valid selector
Fail :focus-visible doesn't match on ShadowRoot
Pass :focus-visible doesn't match on ShadowRoot

View file

@ -0,0 +1,6 @@
Harness status: OK
Found 1 tests
1 Pass
Pass The second autofocus element wins if the first autofocus element was disconnected and reconnected before flushing the autofocus candidates.

View file

@ -0,0 +1,6 @@
Harness status: OK
Found 1 tests
1 Pass
Pass The temporally first autofocus in the document wins, even if an element is inserted later that is previous in the document tree.

View file

@ -0,0 +1,6 @@
Harness status: OK
Found 1 tests
1 Pass
Pass The first autofocus in the document wins, even if elements are inserted later.

View file

@ -0,0 +1,6 @@
Harness status: OK
Found 1 tests
1 Pass
Pass The first autofocus element in the document should win.

View file

@ -0,0 +1,6 @@
Harness status: OK
Found 1 tests
1 Pass
Pass If the first autofocus element is not focusable, but becomes focusable before a frame, it should be focused.

View file

@ -0,0 +1,6 @@
Harness status: OK
Found 1 tests
1 Pass
Pass Non-focusable autofocus element is skipped.

View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<script src="../include.js"></script>
<input id="first" autofocus>
<input id="second" autofocus>
<script>
document.getElementById("first").remove();
asyncTest(done => {
requestAnimationFrame(() => {
println(`activeElement.id=${document.activeElement.id}`);
done();
});
});
</script>

View file

@ -0,0 +1,22 @@
<!DOCTYPE html>
<meta charset="utf-8">
<script src="../../../../resources/testharness.js"></script>
<script src="../../../../resources/testharnessreport.js"></script>
<script src="resources/utils.js"></script>
<input autofocus id="i1">
<input autofocus id="i2">
<script>
"use strict";
promise_test(async () => {
const input1 = document.querySelector("#i1");
const input2 = document.querySelector("#i2");
input1.remove();
input2.parentNode.insertBefore(input1, input2);
await waitUntilStableAutofocusState();
assert_equals(document.activeElement, input2);
}, 'The second autofocus element wins if the first autofocus element was ' +
'disconnected and reconnected before flushing the autofocus candidates.');
</script>

View file

@ -0,0 +1,26 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>The temporally first autofocus in the document wins, even if an element is inserted later that is previous in the document tree</title>
<link rel="help" href="https://html.spec.whatwg.org/multipage/#autofocusing-a-form-control:-the-autofocus-attribute">
<link rel="author" title="Domenic Denicola" href="d@domenic.me">
<script src="../../../../resources/testharness.js"></script>
<script src="../../../../resources/testharnessreport.js"></script>
<script src="resources/utils.js"></script>
<input autofocus>
<script>
"use strict";
promise_test(async () => {
const input1 = document.querySelector("input");
const input2 = document.createElement("input");
input2.autofocus = true;
document.body.prepend(input2);
await waitUntilStableAutofocusState();
assert_equals(document.activeElement, input1);
assert_not_equals(document.activeElement, input2);
}, 'The temporally first autofocus in the document wins, even if an element is inserted later that is previous in the document tree.');
</script>

View file

@ -0,0 +1,26 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>The first autofocus in the document wins, even if elements are inserted later</title>
<link rel="help" href="https://html.spec.whatwg.org/multipage/#autofocusing-a-form-control:-the-autofocus-attribute">
<link rel="author" title="Domenic Denicola" href="d@domenic.me">
<script src="../../../../resources/testharness.js"></script>
<script src="../../../../resources/testharnessreport.js"></script>
<script src="resources/utils.js"></script>
<input autofocus>
<script>
"use strict";
promise_test(async () => {
const input1 = document.querySelector("input");
const input2 = document.createElement("input");
input2.autofocus = true;
document.body.appendChild(input2);
await waitUntilStableAutofocusState();
assert_equals(document.activeElement, input1);
assert_not_equals(document.activeElement, input2);
}, 'The first autofocus in the document wins, even if elements are inserted later.');
</script>

View file

@ -0,0 +1,24 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>The first autofocus in the document wins</title>
<link rel="help" href="https://html.spec.whatwg.org/multipage/#autofocusing-a-form-control:-the-autofocus-attribute">
<link rel="author" title="Domenic Denicola" href="d@domenic.me">
<script src="../../../../resources/testharness.js"></script>
<script src="../../../../resources/testharnessreport.js"></script>
<script src="resources/utils.js"></script>
<input autofocus>
<input autofocus>
<script>
"use strict";
promise_test(async () => {
const [input1, input2] = document.querySelectorAll("input");
await waitUntilStableAutofocusState();
assert_equals(document.activeElement, input1);
assert_not_equals(document.activeElement, input2);
}, 'The first autofocus element in the document should win.');
</script>

View file

@ -0,0 +1,20 @@
<!DOCTYPE html>
<script src="../../../../resources/testharness.js"></script>
<script src="../../../../resources/testharnessreport.js"></script>
<script src="resources/utils.js"></script>
<textarea autofocus disabled></textarea>
<select autofocus></select>
<script>
'use strict';
promise_test(async () => {
const [textarea, select] = document.querySelectorAll('[autofocus]');
textarea.disabled = false;
await waitUntilStableAutofocusState();
assert_equals(document.activeElement, textarea);
assert_not_equals(document.activeElement, select);
}, 'If the first autofocus element is not focusable, but becomes focusable before a frame, it should be focused.');
</script>

View file

@ -0,0 +1,19 @@
<!DOCTYPE html>
<script src="../../../../resources/testharness.js"></script>
<script src="../../../../resources/testharnessreport.js"></script>
<script src="resources/utils.js"></script>
<textarea autofocus disabled></textarea>
<select autofocus></select>
<script>
'use strict';
promise_test(async () => {
const [textarea, select] = document.querySelectorAll('[autofocus]');
await waitUntilStableAutofocusState();
assert_not_equals(document.activeElement, textarea);
assert_equals(document.activeElement, select);
}, 'Non-focusable autofocus element is skipped.');
</script>