mirror of
https://github.com/lexmount/moli.git
synced 2026-10-19 00:01:26 +00:00
fix(dom): complete tabIndex defaults
This commit is contained in:
@@ -304,6 +304,7 @@ pub(in crate::native_bridge::document) use structure::set_document_body_for_nati
|
||||
pub(crate) const XHTML_NS: &str = "http://www.w3.org/1999/xhtml";
|
||||
pub(crate) const SVG_NS: &str = "http://www.w3.org/2000/svg";
|
||||
pub(crate) const XLINK_NS: &str = "http://www.w3.org/1999/xlink";
|
||||
pub(crate) const MATHML_NS: &str = "http://www.w3.org/1998/Math/MathML";
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[repr(u32)]
|
||||
|
||||
@@ -73,6 +73,58 @@ fn contenteditable_state_from_attr(value: &str) -> Option<bool> {
|
||||
}
|
||||
}
|
||||
|
||||
fn html_area_has_associated_image_map(runtime: &JsContextHost, handle: DomHandle) -> bool {
|
||||
let dom = runtime.dom_host();
|
||||
let Some(area) = dom.node(handle).and_then(Node::as_element) else {
|
||||
return false;
|
||||
};
|
||||
if area.namespace() != document::XHTML_NS
|
||||
|| area.local_name() != "area"
|
||||
|| !area.has_attribute("href")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut ancestor = dom.parent_node(handle);
|
||||
let map_name = loop {
|
||||
let Some(candidate) = ancestor else {
|
||||
return false;
|
||||
};
|
||||
let Some(element) = dom.node(candidate).and_then(Node::as_element) else {
|
||||
ancestor = dom.parent_node(candidate);
|
||||
continue;
|
||||
};
|
||||
if element.namespace() == document::XHTML_NS && element.local_name() == "map" {
|
||||
let Some(name) = element.attribute("name").filter(|name| !name.is_empty()) else {
|
||||
return false;
|
||||
};
|
||||
break name.to_owned();
|
||||
}
|
||||
ancestor = dom.parent_node(candidate);
|
||||
};
|
||||
|
||||
let mut stack = dom
|
||||
.child_handles(runtime.document_handle())
|
||||
.collect::<Vec<_>>();
|
||||
while let Some(candidate) = stack.pop() {
|
||||
let Some(element) = dom.node(candidate).and_then(Node::as_element) else {
|
||||
stack.extend(dom.child_handles(candidate));
|
||||
continue;
|
||||
};
|
||||
if element.namespace() == document::XHTML_NS
|
||||
&& matches!(element.local_name(), "img" | "object")
|
||||
&& element
|
||||
.attribute("usemap")
|
||||
.and_then(|value| value.strip_prefix('#'))
|
||||
.is_some_and(|name| name == map_name)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
stack.extend(dom.child_handles(candidate));
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub(super) fn is_focusable(runtime: &JsContextHost, handle: DomHandle) -> bool {
|
||||
let Some(element) = runtime.dom_host().node(handle).and_then(Node::as_element) else {
|
||||
return false;
|
||||
@@ -85,6 +137,9 @@ pub(super) fn is_focusable(runtime: &JsContextHost, handle: DomHandle) -> bool {
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if element.namespace() == document::XHTML_NS && element.local_name() == "area" {
|
||||
return html_area_has_associated_image_map(runtime, handle);
|
||||
}
|
||||
matches!(
|
||||
element.local_name(),
|
||||
"body" | "input" | "button" | "select" | "textarea" | "a" | "iframe" | "frame"
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::webidl;
|
||||
|
||||
use super::super::{
|
||||
JsContextHost,
|
||||
document::{MATHML_NS, SVG_NS, XHTML_NS},
|
||||
node::{
|
||||
node_is_element, node_runtime_and_handle_from_object_or_detached,
|
||||
throw_incompatible_getter_receiver, throw_incompatible_setter_receiver,
|
||||
@@ -13,6 +14,7 @@ use super::super::{
|
||||
throw_dom_exception,
|
||||
};
|
||||
|
||||
use super::details_dialog::main_summary_child;
|
||||
use super::reflection::{
|
||||
DomStringReflection, ElementReflectionInterface, NullToEmptyDomStringReflection,
|
||||
UnsignedLongReflection, UsvStringReflection, remove_reflected_attribute,
|
||||
@@ -2588,11 +2590,21 @@ fn default_tab_index_for(runtime: &JsContextHost, handle: DomHandle) -> i32 {
|
||||
let Some(element) = runtime.dom_host().node(handle).and_then(Node::as_element) else {
|
||||
return -1;
|
||||
};
|
||||
if element.namespace() != "http://www.w3.org/1999/xhtml" {
|
||||
return -1;
|
||||
}
|
||||
match element.local_name() {
|
||||
"a" | "button" | "input" | "select" | "textarea" => 0,
|
||||
match (element.namespace(), element.local_name()) {
|
||||
(
|
||||
XHTML_NS,
|
||||
"a" | "area" | "button" | "frame" | "iframe" | "input" | "object" | "select"
|
||||
| "textarea",
|
||||
)
|
||||
| (SVG_NS | MATHML_NS, "a") => 0,
|
||||
(XHTML_NS, "summary")
|
||||
if runtime
|
||||
.dom_host()
|
||||
.parent_node(handle)
|
||||
.is_some_and(|parent| main_summary_child(runtime, parent) == Some(handle)) =>
|
||||
{
|
||||
0
|
||||
}
|
||||
_ => -1,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,58 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn image_map_area_focusability_supports_autofocus() {
|
||||
let mut vm = new_storage_test_vm("https://area-autofocus.test/");
|
||||
|
||||
let setup = vm
|
||||
.eval(
|
||||
r##"
|
||||
(() => {
|
||||
const root = document.documentElement ||
|
||||
document.appendChild(document.createElement('html'));
|
||||
const body = document.body || root.appendChild(document.createElement('body'));
|
||||
body.innerHTML = `
|
||||
<area id="outside" href="#" autofocus>
|
||||
<map name="unused"><area id="unreferenced" href="#" autofocus></map>
|
||||
<map name="active">
|
||||
<area id="no-href" autofocus>
|
||||
<area id="target" href="#" autofocus>
|
||||
</map>
|
||||
<img usemap="#active">`;
|
||||
|
||||
const rejected = ['outside', 'unreferenced', 'no-href'].map(id => {
|
||||
const candidate = document.getElementById(id);
|
||||
candidate.focus();
|
||||
return document.activeElement !== candidate;
|
||||
});
|
||||
return rejected.every(Boolean) && document.activeElement === body;
|
||||
})()
|
||||
"##,
|
||||
)
|
||||
.expect("image-map focusability fixture should initialize");
|
||||
assert_eq!(setup, "true");
|
||||
|
||||
vm.with_default_context_scope_and_checkpoint_for_test(|scope, runtime_ptr| {
|
||||
assert!(
|
||||
crate::native_bridge::element::post_parse_autofocus_is_pending(unsafe {
|
||||
&*runtime_ptr
|
||||
})
|
||||
);
|
||||
assert!(crate::native_bridge::element::process_post_parse_autofocus(
|
||||
scope,
|
||||
runtime_ptr
|
||||
));
|
||||
Ok(())
|
||||
})
|
||||
.expect("image-map area autofocus should run");
|
||||
|
||||
assert_eq!(
|
||||
vm.eval("document.activeElement === document.getElementById('target')")
|
||||
.expect("image-map autofocus result should remain observable"),
|
||||
"true"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn focus_prevent_scroll_controls_real_nested_scroll_container_reveal() {
|
||||
let mut vm = new_storage_test_vm("https://focus-prevent-scroll.test/");
|
||||
|
||||
@@ -1045,6 +1045,85 @@ fn htmlelement_standard_accessors_live_on_owner_prototypes() {
|
||||
assert_eq!(result, "ok");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tab_index_getter_uses_the_spec_default_element_set() {
|
||||
let mut vm = new_parsed_test_vm(
|
||||
"https://tab-index-defaults.test/",
|
||||
"<!doctype html><html><head></head><body></body></html>",
|
||||
);
|
||||
|
||||
let result = vm
|
||||
.eval(
|
||||
r#"
|
||||
(() => {
|
||||
const assert = (condition, message) => {
|
||||
if (!condition) throw new Error(message);
|
||||
};
|
||||
const svg = "http://www.w3.org/2000/svg";
|
||||
const mathml = "http://www.w3.org/1998/Math/MathML";
|
||||
const other = "https://tab-index-defaults.test/namespace";
|
||||
const exercise = ownerDocument => {
|
||||
for (const name of [
|
||||
"a", "area", "button", "frame", "iframe", "input", "object", "select", "textarea"
|
||||
]) {
|
||||
const element = ownerDocument.createElement(name);
|
||||
assert(element.tabIndex === 0, `${name} default`);
|
||||
}
|
||||
|
||||
const div = ownerDocument.createElement("div");
|
||||
assert(div.tabIndex === -1, "ordinary HTML element default");
|
||||
|
||||
const svgAnchor = ownerDocument.createElementNS(svg, "a");
|
||||
const svgGroup = ownerDocument.createElementNS(svg, "g");
|
||||
const mathAnchor = ownerDocument.createElementNS(mathml, "a");
|
||||
const mathRow = ownerDocument.createElementNS(mathml, "mrow");
|
||||
const foreignAnchor = ownerDocument.createElementNS(other, "a");
|
||||
assert(svgAnchor.tabIndex === 0, "SVG a default");
|
||||
assert(svgGroup.tabIndex === -1, "other SVG element default");
|
||||
assert(mathAnchor.tabIndex === 0, "MathML a default");
|
||||
assert(mathRow.tabIndex === -1, "other MathML element default");
|
||||
assert(foreignAnchor.tabIndex === undefined, "unrelated namespace has no mixin");
|
||||
|
||||
const details = ownerDocument.createElement("details");
|
||||
const nonSummary = ownerDocument.createElement("span");
|
||||
const firstSummary = ownerDocument.createElement("summary");
|
||||
const secondSummary = ownerDocument.createElement("summary");
|
||||
details.append(nonSummary, firstSummary, secondSummary);
|
||||
assert(firstSummary.tabIndex === 0, "first summary child default");
|
||||
assert(secondSummary.tabIndex === -1, "later summary child default");
|
||||
assert(ownerDocument.createElement("summary").tabIndex === -1,
|
||||
"orphan summary default");
|
||||
|
||||
firstSummary.remove();
|
||||
assert(secondSummary.tabIndex === 0, "summary default follows tree mutation");
|
||||
details.append(firstSummary);
|
||||
assert(secondSummary.tabIndex === 0 && firstSummary.tabIndex === -1,
|
||||
"first summary is based on current child order");
|
||||
|
||||
const wrapper = ownerDocument.createElement("div");
|
||||
const nestedSummary = ownerDocument.createElement("summary");
|
||||
wrapper.append(nestedSummary);
|
||||
details.prepend(wrapper);
|
||||
assert(nestedSummary.tabIndex === -1, "nested summary is not a details summary");
|
||||
|
||||
secondSummary.setAttribute("tabindex", "-7");
|
||||
assert(secondSummary.tabIndex === -7, "explicit valid tabindex wins");
|
||||
secondSummary.setAttribute("tabindex", "invalid");
|
||||
assert(secondSummary.tabIndex === 0, "invalid tabindex uses element default");
|
||||
div.setAttribute("tabindex", "invalid");
|
||||
assert(div.tabIndex === -1, "invalid tabindex uses ordinary default");
|
||||
};
|
||||
|
||||
exercise(document);
|
||||
return "ok";
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("tabIndex default getter behavior should evaluate");
|
||||
|
||||
assert_eq!(result, "ok");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inner_and_outer_text_setters_build_rendered_fragments_and_merge_text_endpoints() {
|
||||
let mut vm = new_parsed_test_vm(
|
||||
|
||||
Reference in New Issue
Block a user