fix(forms): expose selectedcontent inner text without a box

This commit is contained in:
ldm0
2026-10-11 18:04:09 +08:00
parent 87a63cd59d
commit 830ae70b88
3 changed files with 54 additions and 1 deletions
@@ -13,6 +13,7 @@ use super::super::{
JsContextHost, document::set_detached_text_replacement_value,
node::node_runtime_and_handle_from_object_or_detached, throw_dom_exception,
};
use super::geometry::observable_client_rects;
use super::{
html_element_getter_receiver, html_element_setter_receiver, observable_sources_with_fragments,
property_string_value,
@@ -817,6 +818,29 @@ fn node_inner_text(
if rendered_state.box_state() == ElementBoxState::NoBox {
return Ok(node.text_content(runtime.dom_host().dom()));
}
// The lightweight rendered-state path derives box presence from computed
// display values. A selectedcontent element can still have no box when
// its native select ancestor suppresses author children. Consult the
// actual layout surface for this control-specific boundary so innerText
// follows its required textContent fallback. Once a customizable select
// produces real child boxes, its non-empty rect list keeps it on the
// normal rendered-text path.
if runtime
.dom_host()
.selectedcontent_nearest_ancestor_select(handle)
.is_some()
&& rendered_state
.target_style()
.is_some_and(|style| style.display != ComputedDisplayKind::Contents)
&& observable_client_rects(
runtime,
handle,
moli_layout::LayoutFlushReason::SynchronousGeometry,
)?
.is_empty()
{
return Ok(node.text_content(runtime.dom_host().dom()));
}
let mut writer = InnerTextWriter::default();
append_inner_text(
@@ -1446,6 +1446,7 @@ const selectedcontent = document.getElementById('selectedcontent');
const sourceSpan = document.querySelector('#one > span');
window.selectedcontentState = [
selectedcontent.textContent.trim(),
selectedcontent.innerText.trim(),
selectedcontent.firstElementChild !== sourceSpan,
];
select.value = 'two';
@@ -1468,7 +1469,7 @@ window.selectedcontentState.push(selectedcontent.textContent.trim());
.expect("selectedcontent parser state should evaluate");
assert_eq!(
result.get("value").and_then(serde_json::Value::as_str),
Some(r#"["one",true,"two","STRONG","updated","one"]"#),
Some(r#"["one","one",true,"two","STRONG","updated","one"]"#),
"parser option completion and select setters must synchronously clone the selected option children"
);
}));
@@ -89,6 +89,34 @@ fn dom_parser_inner_text_is_html_only() {
);
}
#[test]
fn selectedcontent_inner_text_falls_back_when_its_select_child_box_is_suppressed() {
let mut vm = new_parsed_test_vm(
"https://selectedcontent-inner-text.test/",
"<!doctype html><html><body><div id=before>before</div></body></html>",
);
let result = vm
.eval(
r#"
void document.body.innerText;
const select = document.createElement('select');
select.innerHTML = '<button><selectedcontent id="selectedcontent">default</selectedcontent></button><option>one</option>';
select.style.appearance = 'base-select';
document.body.append(select);
const selectedcontent = document.getElementById('selectedcontent');
[
selectedcontent.textContent,
selectedcontent.innerText,
selectedcontent.getClientRects().length
].join('|')
"#,
)
.expect("selectedcontent innerText should evaluate");
assert_eq!(result, "one|one|0");
}
#[test]
fn live_inner_text_applies_inline_text_transform() {
let mut vm = new_rendered_test_vm(