mirror of
https://github.com/lexmount/moli.git
synced 2026-10-09 08:01:05 +00:00
fix(forms): implement option nearest select algorithm
This commit is contained in:
@@ -3139,8 +3139,6 @@ html/semantics/forms/textfieldselection/selection.html
|
||||
html/semantics/forms/the-input-element/show-picker-cross-origin-iframe.html
|
||||
html/semantics/forms/the-select-element/customizable-select/base-appearance-inheritance.html
|
||||
html/semantics/forms/the-select-element/customizable-select/option-disabled-optgroup.html
|
||||
html/semantics/forms/the-select-element/customizable-select/option-form-ancestor-select.html
|
||||
html/semantics/forms/the-select-element/customizable-select/option-list.html
|
||||
html/semantics/forms/the-select-element/customizable-select/picker-icon-animation.html
|
||||
html/semantics/forms/the-select-element/customizable-select/select-base-appearance-computed-style.html
|
||||
html/semantics/forms/the-select-element/customizable-select/select-value-selectedOption.html
|
||||
|
||||
@@ -5847,6 +5847,8 @@ html/semantics/forms/the-select-element/common-HTMLOptionsCollection-namedItem.h
|
||||
html/semantics/forms/the-select-element/common-HTMLOptionsCollection.html
|
||||
html/semantics/forms/the-select-element/customizable-select/option-color-inheritance.html
|
||||
html/semantics/forms/the-select-element/customizable-select/option-disabled-invalid-nesting.html
|
||||
html/semantics/forms/the-select-element/customizable-select/option-form-ancestor-select.html
|
||||
html/semantics/forms/the-select-element/customizable-select/option-list.html
|
||||
html/semantics/forms/the-select-element/customizable-select/select-options-id.html
|
||||
html/semantics/forms/the-select-element/customizable-select/select-parsing.html
|
||||
html/semantics/forms/the-select-element/inserted-or-removed.html
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
mod button_type;
|
||||
mod input_type;
|
||||
mod numeric;
|
||||
mod option;
|
||||
mod text;
|
||||
mod validity;
|
||||
|
||||
@@ -33,6 +34,7 @@ pub use numeric::{
|
||||
parse_html_floating_point_prefix, parse_input_numeric_value, progress_element_values,
|
||||
step_input_value,
|
||||
};
|
||||
pub use option::{OptionNearestSelectStep, OptionNearestSelectTraversal};
|
||||
pub use text::{
|
||||
normalize_custom_validation_message, normalize_form_submission_newlines,
|
||||
parse_non_negative_integer_prefix, parse_non_negative_length_attribute,
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
const HTML_NAMESPACE: &str = "http://www.w3.org/1999/xhtml";
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum OptionNearestSelectStep {
|
||||
Continue,
|
||||
Select,
|
||||
Blocked,
|
||||
}
|
||||
|
||||
/// State for the HTML "option element nearest ancestor select" algorithm.
|
||||
///
|
||||
/// Tree owners perform the actual ancestor walk and feed element names into
|
||||
/// this state machine. Keeping the HTML association rule here lets native DOM
|
||||
/// trees and detached bridge trees share the same barriers.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct OptionNearestSelectTraversal {
|
||||
saw_optgroup: bool,
|
||||
}
|
||||
|
||||
impl OptionNearestSelectTraversal {
|
||||
pub fn visit_ancestor(&mut self, namespace: &str, local_name: &str) -> OptionNearestSelectStep {
|
||||
if namespace != HTML_NAMESPACE {
|
||||
return OptionNearestSelectStep::Continue;
|
||||
}
|
||||
|
||||
match local_name {
|
||||
"datalist" | "hr" | "option" => OptionNearestSelectStep::Blocked,
|
||||
"optgroup" if self.saw_optgroup => OptionNearestSelectStep::Blocked,
|
||||
"optgroup" => {
|
||||
self.saw_optgroup = true;
|
||||
OptionNearestSelectStep::Continue
|
||||
}
|
||||
"select" => OptionNearestSelectStep::Select,
|
||||
_ => OptionNearestSelectStep::Continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,6 +162,10 @@ impl DomHost {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn option_nearest_ancestor_select(&self, handle: DomHandle) -> Option<DomHandle> {
|
||||
self.dom.option_nearest_ancestor_select(handle)
|
||||
}
|
||||
|
||||
pub fn radio_group_members(&self, handle: DomHandle) -> Vec<DomHandle> {
|
||||
let Some(element) = self.node(handle).and_then(Node::as_element) else {
|
||||
return Vec::new();
|
||||
@@ -188,32 +192,8 @@ impl DomHost {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn owner_select_for_option(&self, handle: DomHandle) -> Option<DomHandle> {
|
||||
if !self.is_html_element_named(handle, "option") {
|
||||
return None;
|
||||
}
|
||||
let mut current = self.parent_node(handle);
|
||||
while let Some(parent) = current {
|
||||
let Some(element) = self.node(parent).and_then(Node::as_element) else {
|
||||
current = self.parent_node(parent);
|
||||
continue;
|
||||
};
|
||||
if element.is_html_select() {
|
||||
return Some(parent);
|
||||
}
|
||||
current = self.parent_node(parent);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn select_option_elements(&self, select_handle: DomHandle) -> Vec<DomHandle> {
|
||||
if !self.is_html_element_named(select_handle, "select") {
|
||||
return Vec::new();
|
||||
}
|
||||
self.collect_matching_elements(select_handle, false, |handle| {
|
||||
self.is_html_element_named(handle, "option")
|
||||
&& self.option_belongs_to_select(handle, select_handle)
|
||||
})
|
||||
self.dom.select_option_elements(select_handle)
|
||||
}
|
||||
|
||||
pub fn select_selected_option_elements(&self, select_handle: DomHandle) -> Vec<DomHandle> {
|
||||
@@ -251,28 +231,6 @@ impl DomHost {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn option_belongs_to_select(&self, option: DomHandle, select_handle: DomHandle) -> bool {
|
||||
let mut current = self.parent_node(option);
|
||||
let mut seen_optgroup = false;
|
||||
while let Some(parent) = current {
|
||||
if parent == select_handle {
|
||||
return true;
|
||||
}
|
||||
let Some(element) = self.node(parent).and_then(Node::as_element) else {
|
||||
current = self.parent_node(parent);
|
||||
continue;
|
||||
};
|
||||
match element.local_name() {
|
||||
"option" | "hr" | "select" => return false,
|
||||
"optgroup" if seen_optgroup => return false,
|
||||
"optgroup" => seen_optgroup = true,
|
||||
_ => {}
|
||||
}
|
||||
current = self.parent_node(parent);
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn option_is_disabled(&self, handle: DomHandle) -> bool {
|
||||
let mut current = Some(handle);
|
||||
while let Some(candidate) = current {
|
||||
|
||||
@@ -254,7 +254,7 @@ impl DomHost {
|
||||
if !name.eq_ignore_ascii_case("selected") || !self.is_html_element_named(handle, "option") {
|
||||
return;
|
||||
}
|
||||
let Some(select) = self.owner_select_for_option(handle) else {
|
||||
let Some(select) = self.option_nearest_ancestor_select(handle) else {
|
||||
return;
|
||||
};
|
||||
if self
|
||||
|
||||
@@ -520,7 +520,11 @@ impl DomHost {
|
||||
.node(option)
|
||||
.and_then(Node::as_element)
|
||||
.is_some_and(Element::selected);
|
||||
(option, self.owner_select_for_option(option), selected)
|
||||
(
|
||||
option,
|
||||
self.option_nearest_ancestor_select(option),
|
||||
selected,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -530,7 +534,7 @@ impl DomHost {
|
||||
snapshots: &[(DomHandle, Option<DomHandle>, bool)],
|
||||
) {
|
||||
for &(option, previous_select, was_selected) in snapshots {
|
||||
let Some(select) = self.owner_select_for_option(option) else {
|
||||
let Some(select) = self.option_nearest_ancestor_select(option) else {
|
||||
continue;
|
||||
};
|
||||
if previous_select == Some(select)
|
||||
|
||||
@@ -1529,6 +1529,65 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_option_list_uses_option_nearest_ancestor_select() {
|
||||
let mut host = DomHost::from_dom(NativeDom::new_html(test_url()));
|
||||
let document = host.document_node_id();
|
||||
let parent_select = host.create_element("select");
|
||||
let child_select = host.create_element("select");
|
||||
assert!(host.append_child(document, parent_select));
|
||||
assert!(host.append_child(parent_select, child_select));
|
||||
|
||||
let normal_option = host.create_element("option");
|
||||
let nested_option = host.create_element("option");
|
||||
assert!(host.append_child(child_select, normal_option));
|
||||
assert!(host.append_child(normal_option, nested_option));
|
||||
|
||||
let div = host.create_element("div");
|
||||
let div_option = host.create_element("option");
|
||||
assert!(host.append_child(child_select, div));
|
||||
assert!(host.append_child(div, div_option));
|
||||
|
||||
let hr = host.create_element("hr");
|
||||
let hr_option = host.create_element("option");
|
||||
assert!(host.append_child(child_select, hr));
|
||||
assert!(host.append_child(hr, hr_option));
|
||||
|
||||
let datalist = host.create_element("datalist");
|
||||
let datalist_option = host.create_element("option");
|
||||
assert!(host.append_child(child_select, datalist));
|
||||
assert!(host.append_child(datalist, datalist_option));
|
||||
|
||||
let optgroup = host.create_element("optgroup");
|
||||
let optgroup_option = host.create_element("option");
|
||||
let nested_optgroup = host.create_element("optgroup");
|
||||
let nested_optgroup_option = host.create_element("option");
|
||||
assert!(host.append_child(child_select, optgroup));
|
||||
assert!(host.append_child(optgroup, optgroup_option));
|
||||
assert!(host.append_child(optgroup, nested_optgroup));
|
||||
assert!(host.append_child(nested_optgroup, nested_optgroup_option));
|
||||
|
||||
assert!(host.select_option_elements(parent_select).is_empty());
|
||||
assert_eq!(
|
||||
host.select_option_elements(child_select),
|
||||
vec![normal_option, div_option, optgroup_option]
|
||||
);
|
||||
for option in [normal_option, div_option, optgroup_option] {
|
||||
assert_eq!(
|
||||
host.option_nearest_ancestor_select(option),
|
||||
Some(child_select)
|
||||
);
|
||||
}
|
||||
for option in [
|
||||
nested_option,
|
||||
hr_option,
|
||||
datalist_option,
|
||||
nested_optgroup_option,
|
||||
] {
|
||||
assert_eq!(host.option_nearest_ancestor_select(option), None);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selected_option_insertion_deselects_single_select_peers_without_dirtying_them() {
|
||||
let mut host = DomHost::from_dom(NativeDom::new_html(test_url()));
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use super::NativeDom;
|
||||
use super::element::Element;
|
||||
use super::node::{NativeNodeId, Node};
|
||||
use crate::forms::parse_non_negative_integer_prefix;
|
||||
use crate::forms::{
|
||||
OptionNearestSelectStep, OptionNearestSelectTraversal, parse_non_negative_integer_prefix,
|
||||
};
|
||||
|
||||
impl NativeDom {
|
||||
pub fn is_html_element_named(&self, node_id: NativeNodeId, local_name: &str) -> bool {
|
||||
@@ -40,7 +42,7 @@ impl NativeDom {
|
||||
if !option.is_html_option() {
|
||||
return false;
|
||||
}
|
||||
if let Some(select_id) = self.owner_select_for_option(option_id) {
|
||||
if let Some(select_id) = self.option_nearest_ancestor_select(option_id) {
|
||||
return self
|
||||
.select_selected_option_elements(select_id)
|
||||
.contains(&option_id);
|
||||
@@ -58,10 +60,36 @@ impl NativeDom {
|
||||
}
|
||||
self.elements_by_tag_name(select_id, "option", false)
|
||||
.into_iter()
|
||||
.filter(|option_id| self.option_belongs_to_select(*option_id, select_id))
|
||||
.filter(|option_id| self.option_nearest_ancestor_select(*option_id) == Some(select_id))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn option_nearest_ancestor_select(&self, option_id: NativeNodeId) -> Option<NativeNodeId> {
|
||||
if !self
|
||||
.node(option_id)
|
||||
.and_then(Node::as_element)
|
||||
.is_some_and(Element::is_html_option)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut traversal = OptionNearestSelectTraversal::default();
|
||||
let mut current = self.parent_node(option_id);
|
||||
while let Some(parent) = current {
|
||||
let Some(element) = self.node(parent).and_then(Node::as_element) else {
|
||||
current = self.parent_node(parent);
|
||||
continue;
|
||||
};
|
||||
match traversal.visit_ancestor(element.namespace(), element.local_name()) {
|
||||
OptionNearestSelectStep::Continue => {}
|
||||
OptionNearestSelectStep::Select => return Some(parent),
|
||||
OptionNearestSelectStep::Blocked => return None,
|
||||
}
|
||||
current = self.parent_node(parent);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn select_selected_option_elements(&self, select_id: NativeNodeId) -> Vec<NativeNodeId> {
|
||||
let options = self.select_option_elements(select_id);
|
||||
let Some(select) = self.node(select_id).and_then(Node::as_element) else {
|
||||
@@ -181,46 +209,6 @@ impl NativeDom {
|
||||
out
|
||||
}
|
||||
|
||||
fn option_belongs_to_select(&self, option_id: NativeNodeId, select_id: NativeNodeId) -> bool {
|
||||
let mut current = self.parent_node(option_id);
|
||||
let mut seen_optgroup = false;
|
||||
while let Some(parent) = current {
|
||||
if parent == select_id {
|
||||
return true;
|
||||
}
|
||||
let Some(element) = self.node(parent).and_then(Node::as_element) else {
|
||||
current = self.parent_node(parent);
|
||||
continue;
|
||||
};
|
||||
match element.local_name() {
|
||||
"option" | "hr" | "select" => return false,
|
||||
"optgroup" if seen_optgroup => return false,
|
||||
"optgroup" => seen_optgroup = true,
|
||||
_ => {}
|
||||
}
|
||||
current = self.parent_node(parent);
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn owner_select_for_option(&self, option_id: NativeNodeId) -> Option<NativeNodeId> {
|
||||
let mut current = self.parent_node(option_id);
|
||||
while let Some(parent) = current {
|
||||
let Some(element) = self.node(parent).and_then(Node::as_element) else {
|
||||
current = self.parent_node(parent);
|
||||
continue;
|
||||
};
|
||||
if element.is_html_select() {
|
||||
return Some(parent);
|
||||
}
|
||||
if matches!(element.local_name(), "option" | "hr" | "select") {
|
||||
return None;
|
||||
}
|
||||
current = self.parent_node(parent);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn option_is_disabled(&self, option_id: NativeNodeId) -> bool {
|
||||
let mut current = Some(option_id);
|
||||
while let Some(candidate) = current {
|
||||
|
||||
@@ -28,7 +28,7 @@ impl DocumentRuntime {
|
||||
if !element.is_html_option() {
|
||||
return false;
|
||||
}
|
||||
if let Some(select) = self.owner_select_for_option(handle) {
|
||||
if let Some(select) = self.dom_host.option_nearest_ancestor_select(handle) {
|
||||
return self
|
||||
.dom_host
|
||||
.select_selected_option_elements(select)
|
||||
@@ -37,17 +37,6 @@ impl DocumentRuntime {
|
||||
element.selected()
|
||||
}
|
||||
|
||||
fn owner_select_for_option(&self, handle: DomHandle) -> Option<DomHandle> {
|
||||
let mut current = self.dom_host.parent_node(handle);
|
||||
while let Some(parent) = current {
|
||||
if self.dom_host.is_html_element_named(parent, "select") {
|
||||
return Some(parent);
|
||||
}
|
||||
current = self.dom_host.parent_node(parent);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn preserve_selectedness_for_options_removed_from_select(
|
||||
&mut self,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
@@ -55,7 +44,12 @@ impl DocumentRuntime {
|
||||
selectedness: &[(DomHandle, bool)],
|
||||
) {
|
||||
for &(option, was_selected) in selectedness {
|
||||
if was_selected && self.owner_select_for_option(option).is_none() {
|
||||
if was_selected
|
||||
&& self
|
||||
.dom_host
|
||||
.option_nearest_ancestor_select(option)
|
||||
.is_none()
|
||||
{
|
||||
let _ = self.set_selected_state_with_dirty(scope, host_ptr, option, true, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1464,7 +1464,7 @@ fn perform_option_click_default_action(
|
||||
handle: DomHandle,
|
||||
) -> bool {
|
||||
let runtime = unsafe { &*runtime_ptr };
|
||||
let Some(select_handle) = owner_select_for_option(runtime, handle) else {
|
||||
let Some(select_handle) = runtime.dom_host().option_nearest_ancestor_select(handle) else {
|
||||
return false;
|
||||
};
|
||||
if is_disabled_form_control(runtime, handle) || is_disabled_form_control(runtime, select_handle)
|
||||
@@ -1499,25 +1499,6 @@ fn perform_option_click_default_action(
|
||||
true
|
||||
}
|
||||
|
||||
fn owner_select_for_option(runtime: &JsContextHost, handle: DomHandle) -> Option<DomHandle> {
|
||||
if !is_html_option_element(runtime, handle) {
|
||||
return None;
|
||||
}
|
||||
let mut current = runtime.dom_host().parent_node(handle);
|
||||
while let Some(parent) = current {
|
||||
if runtime
|
||||
.dom_host()
|
||||
.node(parent)
|
||||
.and_then(Node::as_element)
|
||||
.is_some_and(Element::is_html_select)
|
||||
{
|
||||
return Some(parent);
|
||||
}
|
||||
current = runtime.dom_host().parent_node(parent);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn is_html_option_element(runtime: &JsContextHost, handle: DomHandle) -> bool {
|
||||
runtime
|
||||
.dom_host()
|
||||
|
||||
@@ -25,20 +25,11 @@ pub(super) fn effective_option_selected(runtime: &JsContextHost, handle: DomHand
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut current = runtime.dom_host().parent_node(handle);
|
||||
while let Some(parent) = current {
|
||||
let Some(parent_element) = runtime.dom_host().node(parent).and_then(Node::as_element)
|
||||
else {
|
||||
current = runtime.dom_host().parent_node(parent);
|
||||
continue;
|
||||
};
|
||||
if parent_element.is_html_select() {
|
||||
return runtime
|
||||
.dom_host()
|
||||
.select_selected_option_elements(parent)
|
||||
.contains(&handle);
|
||||
}
|
||||
current = runtime.dom_host().parent_node(parent);
|
||||
if let Some(select) = runtime.dom_host().option_nearest_ancestor_select(handle) {
|
||||
return runtime
|
||||
.dom_host()
|
||||
.select_selected_option_elements(select)
|
||||
.contains(&handle);
|
||||
}
|
||||
element.selected()
|
||||
}
|
||||
|
||||
@@ -2,10 +2,11 @@ use super::helpers::{
|
||||
effective_option_selected, element_option_value, select_is_multiple, select_option_handles,
|
||||
};
|
||||
use super::*;
|
||||
use crate::dom::forms::{OptionNearestSelectStep, OptionNearestSelectTraversal};
|
||||
use crate::native_bridge::{
|
||||
document::{
|
||||
detached_element_local_name, detached_form_owner_object, detached_parent_node_object,
|
||||
set_detached_text_replacement_value,
|
||||
detached_element_local_name, detached_element_namespace_uri, detached_form_owner_object,
|
||||
detached_parent_node_object, set_detached_text_replacement_value,
|
||||
},
|
||||
element::{html_element_getter_receiver, html_element_setter_receiver},
|
||||
};
|
||||
@@ -86,11 +87,19 @@ fn detached_option_form_owner_object<'s>(
|
||||
option: v8::Local<'s, v8::Object>,
|
||||
) -> Option<Option<v8::Local<'s, v8::Object>>> {
|
||||
let mut current = detached_parent_node_object(scope, option)?;
|
||||
let mut traversal = OptionNearestSelectTraversal::default();
|
||||
loop {
|
||||
if detached_element_local_name(scope, current)
|
||||
.is_some_and(|name| name.eq_ignore_ascii_case("select"))
|
||||
{
|
||||
return Some(detached_form_owner_object(scope, current));
|
||||
if let (Some(namespace), Some(local_name)) = (
|
||||
detached_element_namespace_uri(scope, current),
|
||||
detached_element_local_name(scope, current),
|
||||
) {
|
||||
match traversal.visit_ancestor(&namespace, &local_name) {
|
||||
OptionNearestSelectStep::Continue => {}
|
||||
OptionNearestSelectStep::Select => {
|
||||
return Some(detached_form_owner_object(scope, current));
|
||||
}
|
||||
OptionNearestSelectStep::Blocked => return Some(None),
|
||||
}
|
||||
}
|
||||
let Some(parent) = detached_parent_node_object(scope, current) else {
|
||||
return Some(None);
|
||||
@@ -203,19 +212,7 @@ pub(in crate::native_bridge) fn option_selected_setter_function<'s>(
|
||||
};
|
||||
let selected = args.get(0).boolean_value(scope);
|
||||
let runtime = unsafe { &mut *runtime_ptr };
|
||||
let mut owner_select = runtime.dom_host().parent_node(handle);
|
||||
while let Some(parent) = owner_select {
|
||||
if runtime
|
||||
.dom_host()
|
||||
.node(parent)
|
||||
.and_then(Node::as_element)
|
||||
.is_some_and(Element::is_html_select)
|
||||
{
|
||||
owner_select = Some(parent);
|
||||
break;
|
||||
}
|
||||
owner_select = runtime.dom_host().parent_node(parent);
|
||||
}
|
||||
let owner_select = runtime.dom_host().option_nearest_ancestor_select(handle);
|
||||
if let Some(select_handle) = owner_select
|
||||
&& selected
|
||||
&& !select_is_multiple(runtime, select_handle)
|
||||
@@ -252,21 +249,11 @@ pub(in crate::native_bridge) fn option_form_getter_function<'s>(
|
||||
return;
|
||||
}
|
||||
let runtime = unsafe { &*runtime_ptr };
|
||||
let mut current = runtime.dom_host().parent_node(handle);
|
||||
while let Some(parent) = current {
|
||||
if runtime
|
||||
.dom_host()
|
||||
.node(parent)
|
||||
.and_then(Node::as_element)
|
||||
.is_some_and(Element::is_html_select)
|
||||
{
|
||||
let form = form_associated_form_owner(runtime, parent);
|
||||
set_wrapped_node_or_null(scope, &mut rv, runtime_ptr, form);
|
||||
return;
|
||||
}
|
||||
current = runtime.dom_host().parent_node(parent);
|
||||
}
|
||||
rv.set_null();
|
||||
let form = runtime
|
||||
.dom_host()
|
||||
.option_nearest_ancestor_select(handle)
|
||||
.and_then(|select| form_associated_form_owner(runtime, select));
|
||||
set_wrapped_node_or_null(scope, &mut rv, runtime_ptr, form);
|
||||
}
|
||||
|
||||
pub(in crate::native_bridge) fn option_index_getter_function<'s>(
|
||||
@@ -279,33 +266,16 @@ pub(in crate::native_bridge) fn option_index_getter_function<'s>(
|
||||
return;
|
||||
};
|
||||
let runtime = unsafe { &*runtime_ptr };
|
||||
let mut current = runtime.dom_host().parent_node(handle);
|
||||
while let Some(parent) = current {
|
||||
if runtime
|
||||
.dom_host()
|
||||
.node(parent)
|
||||
.and_then(Node::as_element)
|
||||
.is_some_and(Element::is_html_select)
|
||||
{
|
||||
let index = select_option_handles(runtime, parent)
|
||||
let index = runtime
|
||||
.dom_host()
|
||||
.option_nearest_ancestor_select(handle)
|
||||
.and_then(|select| {
|
||||
select_option_handles(runtime, select)
|
||||
.into_iter()
|
||||
.position(|option| option == handle)
|
||||
.unwrap_or(0) as i32;
|
||||
rv.set_int32(index);
|
||||
return;
|
||||
}
|
||||
if runtime
|
||||
.dom_host()
|
||||
.node(parent)
|
||||
.and_then(Node::as_element)
|
||||
.is_some_and(|element| element.local_name() == "datalist")
|
||||
{
|
||||
rv.set_int32(0);
|
||||
return;
|
||||
}
|
||||
current = runtime.dom_host().parent_node(parent);
|
||||
}
|
||||
rv.set_int32(0);
|
||||
})
|
||||
.unwrap_or(0) as i32;
|
||||
rv.set_int32(index);
|
||||
}
|
||||
|
||||
pub(in crate::native_bridge) fn option_label_getter_function<'s>(
|
||||
|
||||
@@ -563,6 +563,80 @@ fn detached_select_and_option_track_selection_state() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn option_nearest_ancestor_select_controls_list_form_index_and_selectedness() {
|
||||
let mut vm = new_storage_test_vm("https://option-nearest-select.test/");
|
||||
|
||||
let result = vm
|
||||
.eval(
|
||||
r#"
|
||||
(() => {
|
||||
const root = document.documentElement || document.appendChild(document.createElement('html'));
|
||||
const body = document.body || root.appendChild(document.createElement('body'));
|
||||
const form = document.createElement('form');
|
||||
const parentSelect = document.createElement('select');
|
||||
const childSelect = document.createElement('select');
|
||||
form.append(parentSelect);
|
||||
parentSelect.append(childSelect);
|
||||
body.append(form);
|
||||
|
||||
const normalOption = document.createElement('option');
|
||||
childSelect.append(normalOption);
|
||||
const nestedOption = document.createElement('option');
|
||||
normalOption.append(nestedOption);
|
||||
|
||||
const div = document.createElement('div');
|
||||
childSelect.append(div);
|
||||
const divOption = document.createElement('option');
|
||||
div.append(divOption);
|
||||
|
||||
const hr = document.createElement('hr');
|
||||
childSelect.append(hr);
|
||||
const hrOption = document.createElement('option');
|
||||
hr.append(hrOption);
|
||||
|
||||
const datalist = document.createElement('datalist');
|
||||
childSelect.append(datalist);
|
||||
const datalistOption = document.createElement('option');
|
||||
datalist.append(datalistOption);
|
||||
|
||||
const optgroup = document.createElement('optgroup');
|
||||
childSelect.append(optgroup);
|
||||
const optgroupOption = document.createElement('option');
|
||||
optgroup.append(optgroupOption);
|
||||
const nestedOptgroup = document.createElement('optgroup');
|
||||
optgroup.append(nestedOptgroup);
|
||||
const nestedOptgroupOption = document.createElement('option');
|
||||
nestedOptgroup.append(nestedOptgroupOption);
|
||||
|
||||
normalOption.selected = true;
|
||||
datalistOption.selected = true;
|
||||
|
||||
return JSON.stringify({
|
||||
parentLength: parentSelect.length,
|
||||
childLength: childSelect.length,
|
||||
options: Array.from(childSelect.options, option =>
|
||||
option === normalOption ? 'normal' :
|
||||
option === divOption ? 'div' :
|
||||
option === optgroupOption ? 'optgroup' : 'unexpected'),
|
||||
validForms: [normalOption, divOption, optgroupOption].map(option => option.form === form),
|
||||
invalidForms: [nestedOption, hrOption, datalistOption, nestedOptgroupOption]
|
||||
.map(option => option.form === null),
|
||||
indices: [normalOption, divOption, optgroupOption, nestedOption, hrOption,
|
||||
datalistOption, nestedOptgroupOption].map(option => option.index),
|
||||
normalStillSelected: normalOption.selected
|
||||
});
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("option nearest ancestor select behavior should evaluate");
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
r#"{"parentLength":0,"childLength":3,"options":["normal","div","optgroup"],"validForms":[true,true,true],"invalidForms":[true,true,true,true],"indices":[0,1,2,0,0,0,0],"normalStillSelected":true}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn child_document_select_exposes_detached_mutation_surface() {
|
||||
let mut vm = new_storage_test_vm("https://child-select-options-surface.test/");
|
||||
|
||||
Reference in New Issue
Block a user