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:
parent
baefb51902
commit
e5d615cb11
19 changed files with 345 additions and 4 deletions
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
activeElement.id=second
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
Harness status: OK
|
||||
|
||||
Found 1 tests
|
||||
|
||||
1 Pass
|
||||
Pass The first autofocus element in the document should win.
|
||||
|
|
@ -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.
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
Harness status: OK
|
||||
|
||||
Found 1 tests
|
||||
|
||||
1 Pass
|
||||
Pass Non-focusable autofocus element is skipped.
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
Loading…
Reference in a new issue