mirror of
https://github.com/lexmount/moli.git
synced 2026-09-24 00:01:27 +00:00
fix(forms): unify option disabledness
This commit is contained in:
@@ -3087,7 +3087,6 @@ html/semantics/forms/form-submission-target/rel-input-target.html
|
||||
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/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
|
||||
|
||||
@@ -6442,6 +6442,7 @@ html/semantics/forms/the-select-element/common-HTMLOptionsCollection.html
|
||||
html/semantics/forms/the-select-element/customizable-select/nested-select-crash.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-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/select-highlight-crash.html
|
||||
|
||||
@@ -34,7 +34,10 @@ 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 option::{
|
||||
OptionDisabledAncestorStep, OptionNearestSelectStep, OptionNearestSelectTraversal,
|
||||
option_disabled_ancestor_step,
|
||||
};
|
||||
pub use text::{
|
||||
normalize_custom_validation_message, normalize_form_submission_newlines,
|
||||
parse_non_negative_integer_prefix, parse_non_negative_length_attribute,
|
||||
|
||||
@@ -7,6 +7,28 @@ pub enum OptionNearestSelectStep {
|
||||
Blocked,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum OptionDisabledAncestorStep {
|
||||
Continue,
|
||||
Disabled(bool),
|
||||
}
|
||||
|
||||
pub fn option_disabled_ancestor_step(
|
||||
namespace: &str,
|
||||
local_name: &str,
|
||||
has_disabled_attribute: bool,
|
||||
) -> OptionDisabledAncestorStep {
|
||||
if namespace != HTML_NAMESPACE {
|
||||
return OptionDisabledAncestorStep::Continue;
|
||||
}
|
||||
|
||||
match local_name {
|
||||
"select" | "hr" | "datalist" | "option" => OptionDisabledAncestorStep::Disabled(false),
|
||||
"optgroup" => OptionDisabledAncestorStep::Disabled(has_disabled_attribute),
|
||||
_ => OptionDisabledAncestorStep::Continue,
|
||||
}
|
||||
}
|
||||
|
||||
/// State for the HTML "option element nearest ancestor select" algorithm.
|
||||
///
|
||||
/// Tree owners perform the actual ancestor walk and feed element names into
|
||||
@@ -18,6 +40,10 @@ pub struct OptionNearestSelectTraversal {
|
||||
}
|
||||
|
||||
impl OptionNearestSelectTraversal {
|
||||
pub fn starting_at_optgroup() -> Self {
|
||||
Self { saw_optgroup: true }
|
||||
}
|
||||
|
||||
pub fn visit_ancestor(&mut self, namespace: &str, local_name: &str) -> OptionNearestSelectStep {
|
||||
if namespace != HTML_NAMESPACE {
|
||||
return OptionNearestSelectStep::Continue;
|
||||
|
||||
@@ -166,6 +166,14 @@ impl DomHost {
|
||||
self.dom.option_nearest_ancestor_select(handle)
|
||||
}
|
||||
|
||||
pub fn optgroup_nearest_ancestor_select(&self, handle: DomHandle) -> Option<DomHandle> {
|
||||
self.dom.optgroup_nearest_ancestor_select(handle)
|
||||
}
|
||||
|
||||
pub fn option_is_disabled(&self, handle: DomHandle) -> bool {
|
||||
self.dom.option_is_disabled(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();
|
||||
@@ -230,26 +238,6 @@ impl DomHost {
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn option_is_disabled(&self, handle: DomHandle) -> bool {
|
||||
let mut current = Some(handle);
|
||||
while let Some(candidate) = current {
|
||||
let Some(element) = self.node(candidate).and_then(Node::as_element) else {
|
||||
current = self.parent_node(candidate);
|
||||
continue;
|
||||
};
|
||||
if matches!(element.local_name(), "option" | "optgroup")
|
||||
&& element.has_attribute("disabled")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if element.is_html_select() {
|
||||
return false;
|
||||
}
|
||||
current = self.parent_node(candidate);
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn select_display_size(select: &Element) -> i32 {
|
||||
|
||||
@@ -1560,11 +1560,13 @@ mod tests {
|
||||
assert!(host.append_child(datalist, datalist_option));
|
||||
|
||||
let optgroup = host.create_element("optgroup");
|
||||
let optgroup_div = host.create_element("div");
|
||||
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, optgroup_div));
|
||||
assert!(host.append_child(optgroup_div, optgroup_option));
|
||||
assert!(host.append_child(optgroup, nested_optgroup));
|
||||
assert!(host.append_child(nested_optgroup, nested_optgroup_option));
|
||||
|
||||
@@ -1587,6 +1589,18 @@ mod tests {
|
||||
] {
|
||||
assert_eq!(host.option_nearest_ancestor_select(option), None);
|
||||
}
|
||||
|
||||
assert!(host.set_attribute(optgroup, "disabled", ""));
|
||||
assert!(host.option_is_disabled(optgroup_option));
|
||||
for option in [
|
||||
normal_option,
|
||||
nested_option,
|
||||
hr_option,
|
||||
datalist_option,
|
||||
nested_optgroup_option,
|
||||
] {
|
||||
assert!(!host.option_is_disabled(option));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -2,7 +2,8 @@ use super::NativeDom;
|
||||
use super::element::Element;
|
||||
use super::node::{NativeNodeId, Node};
|
||||
use crate::forms::{
|
||||
OptionNearestSelectStep, OptionNearestSelectTraversal, parse_non_negative_integer_prefix,
|
||||
OptionDisabledAncestorStep, OptionNearestSelectStep, OptionNearestSelectTraversal,
|
||||
option_disabled_ancestor_step, parse_non_negative_integer_prefix,
|
||||
};
|
||||
|
||||
impl NativeDom {
|
||||
@@ -73,8 +74,33 @@ impl NativeDom {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut traversal = OptionNearestSelectTraversal::default();
|
||||
let mut current = self.parent_node(option_id);
|
||||
self.nearest_ancestor_select(option_id, OptionNearestSelectTraversal::default())
|
||||
}
|
||||
|
||||
pub fn optgroup_nearest_ancestor_select(
|
||||
&self,
|
||||
optgroup_id: NativeNodeId,
|
||||
) -> Option<NativeNodeId> {
|
||||
if !self
|
||||
.node(optgroup_id)
|
||||
.and_then(Node::as_element)
|
||||
.is_some_and(|element| element.is_html_element("optgroup"))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
self.nearest_ancestor_select(
|
||||
optgroup_id,
|
||||
OptionNearestSelectTraversal::starting_at_optgroup(),
|
||||
)
|
||||
}
|
||||
|
||||
fn nearest_ancestor_select(
|
||||
&self,
|
||||
element_id: NativeNodeId,
|
||||
mut traversal: OptionNearestSelectTraversal,
|
||||
) -> Option<NativeNodeId> {
|
||||
let mut current = self.parent_node(element_id);
|
||||
while let Some(parent) = current {
|
||||
let Some(element) = self.node(parent).and_then(Node::as_element) else {
|
||||
current = self.parent_node(parent);
|
||||
@@ -90,6 +116,36 @@ impl NativeDom {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn option_is_disabled(&self, option_id: NativeNodeId) -> bool {
|
||||
let Some(option) = self.node(option_id).and_then(Node::as_element) else {
|
||||
return false;
|
||||
};
|
||||
if !option.is_html_option() {
|
||||
return false;
|
||||
}
|
||||
if option.has_attribute("disabled") {
|
||||
return true;
|
||||
}
|
||||
|
||||
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 option_disabled_ancestor_step(
|
||||
element.namespace(),
|
||||
element.local_name(),
|
||||
element.has_attribute("disabled"),
|
||||
) {
|
||||
OptionDisabledAncestorStep::Continue => {}
|
||||
OptionDisabledAncestorStep::Disabled(disabled) => return disabled,
|
||||
}
|
||||
current = self.parent_node(parent);
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -208,26 +264,6 @@ impl NativeDom {
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn option_is_disabled(&self, option_id: NativeNodeId) -> bool {
|
||||
let mut current = Some(option_id);
|
||||
while let Some(candidate) = current {
|
||||
let Some(element) = self.node(candidate).and_then(Node::as_element) else {
|
||||
current = self.parent_node(candidate);
|
||||
continue;
|
||||
};
|
||||
if matches!(element.local_name(), "option" | "optgroup")
|
||||
&& element.has_attribute("disabled")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if element.is_html_select() {
|
||||
return false;
|
||||
}
|
||||
current = self.parent_node(candidate);
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn select_display_size(select: &Element) -> i32 {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use super::storage::{form_data_entries, form_data_is_object, push_form_data_entry};
|
||||
use super::*;
|
||||
use crate::custom_elements::is_form_associated_custom_element_handle;
|
||||
use crate::dom::{forms::InputType, native::Node};
|
||||
use crate::dom::{
|
||||
forms::{InputType, OptionDisabledAncestorStep, option_disabled_ancestor_step},
|
||||
native::Node,
|
||||
};
|
||||
use crate::native_bridge::{
|
||||
element::{
|
||||
element_attribute_for_object, element_internals_form_value_for_target,
|
||||
@@ -504,18 +507,21 @@ fn control_has_datalist_ancestor<'s>(
|
||||
}
|
||||
|
||||
fn option_is_disabled(scope: &mut v8::PinScope<'_, '_>, option: v8::Local<'_, v8::Object>) -> bool {
|
||||
let mut current = Some(option);
|
||||
if object_bool_property(scope, option, "disabled").unwrap_or(false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let mut current = object_property_as_object(scope, option, "parentElement");
|
||||
while let Some(element) = current {
|
||||
let tag = object_string_property_defined(scope, element, "tagName")
|
||||
.map(|tag| tag.to_ascii_lowercase())
|
||||
.unwrap_or_default();
|
||||
if matches!(tag.as_str(), "option" | "optgroup")
|
||||
&& object_bool_property(scope, element, "disabled").unwrap_or(false)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if tag == "select" {
|
||||
return false;
|
||||
let namespace =
|
||||
object_string_property_defined(scope, element, "namespaceURI").unwrap_or_default();
|
||||
let local_name =
|
||||
object_string_property_defined(scope, element, "localName").unwrap_or_default();
|
||||
let has_disabled_attribute =
|
||||
object_bool_property(scope, element, "disabled").unwrap_or(false);
|
||||
match option_disabled_ancestor_step(&namespace, &local_name, has_disabled_attribute) {
|
||||
OptionDisabledAncestorStep::Continue => {}
|
||||
OptionDisabledAncestorStep::Disabled(disabled) => return disabled,
|
||||
}
|
||||
current = object_property_as_object(scope, element, "parentElement");
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ pub(crate) fn form_control_is_effectively_disabled(
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if option_is_disabled_by_optgroup(runtime, handle) {
|
||||
if runtime.dom_host().option_is_disabled(handle) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -161,33 +161,6 @@ fn disabled_attribute_applies_to_control(
|
||||
))
|
||||
}
|
||||
|
||||
fn option_is_disabled_by_optgroup(runtime: &JsContextHost, handle: DomHandle) -> bool {
|
||||
if !runtime
|
||||
.dom_host()
|
||||
.node(handle)
|
||||
.and_then(Node::as_element)
|
||||
.is_some_and(|element| element.is_html_element("option"))
|
||||
{
|
||||
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;
|
||||
};
|
||||
match parent_element.local_name() {
|
||||
"optgroup" => return parent_element.has_attribute("disabled"),
|
||||
"select" | "hr" | "datalist" | "option" => return false,
|
||||
_ => {}
|
||||
}
|
||||
current = runtime.dom_host().parent_node(parent);
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn control_is_in_first_legend(
|
||||
runtime: &JsContextHost,
|
||||
control: DomHandle,
|
||||
|
||||
@@ -150,28 +150,5 @@ pub(in crate::script_vm) fn current_selection_state(
|
||||
}
|
||||
|
||||
pub(in crate::script_vm) fn option_is_disabled(runtime: &JsContextHost, handle: DomHandle) -> bool {
|
||||
if runtime
|
||||
.dom_host()
|
||||
.node(handle)
|
||||
.and_then(Node::as_element)
|
||||
.is_some_and(|element| element.has_attribute("disabled"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
match parent_element.local_name() {
|
||||
"optgroup" => return parent_element.has_attribute("disabled"),
|
||||
"select" | "hr" | "datalist" | "option" => return false,
|
||||
_ => {}
|
||||
}
|
||||
current = runtime.dom_host().parent_node(parent);
|
||||
}
|
||||
false
|
||||
runtime.dom_host().option_is_disabled(handle)
|
||||
}
|
||||
|
||||
@@ -267,6 +267,42 @@ fn has_pseudo_class_invalidation_updates_derived_form_states() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optgroup_disabled_invalidation_updates_nested_option_computed_style() {
|
||||
let mut vm = new_storage_test_vm("https://nested-option-disabled-style.test/");
|
||||
|
||||
let result = vm
|
||||
.eval(
|
||||
r#"
|
||||
(() => {
|
||||
const root = document.documentElement || document.appendChild(document.createElement('html'));
|
||||
const head = document.head || root.appendChild(document.createElement('head'));
|
||||
const body = document.body || root.appendChild(document.createElement('body'));
|
||||
const style = document.createElement('style');
|
||||
style.textContent = 'option { color: black; } option:disabled { color: gray; }';
|
||||
head.append(style);
|
||||
|
||||
const select = document.createElement('select');
|
||||
const optgroup = document.createElement('optgroup');
|
||||
const div = document.createElement('div');
|
||||
const option = document.createElement('option');
|
||||
div.append(option);
|
||||
optgroup.append(div);
|
||||
select.append(optgroup);
|
||||
body.append(select);
|
||||
|
||||
const computed = getComputedStyle(option);
|
||||
const before = computed.color;
|
||||
optgroup.disabled = true;
|
||||
return [before, computed.color, option.matches(':disabled')].join('|');
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("nested option disabled style should evaluate");
|
||||
|
||||
assert_eq!(result, "rgb(0, 0, 0)|rgb(128, 128, 128)|true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn focus_selector_invalidation_preserves_unrelated_cache_entries() {
|
||||
let mut vm = new_storage_test_vm("https://focus-style-cache-targeted.test/");
|
||||
|
||||
@@ -1931,6 +1931,70 @@ mod tests {
|
||||
assert!(!engine.matches_host(&host, option, ":disabled").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dom_api_selectors_option_disabledness_respects_association_boundaries() {
|
||||
let url = url::Url::parse("https://example.test/").unwrap();
|
||||
let mut host = DomHost::from_dom(NativeDom::new_html(url));
|
||||
host.reset_html_document_shell();
|
||||
let body = host.document_body_handle().unwrap();
|
||||
let engine = QueryEngine;
|
||||
|
||||
let select = host.create_element("select");
|
||||
let optgroup = host.create_element("optgroup");
|
||||
let div = host.create_element("div");
|
||||
let option = host.create_element("option");
|
||||
assert!(host.append_child(body, select));
|
||||
assert!(host.append_child(select, optgroup));
|
||||
assert!(host.append_child(optgroup, div));
|
||||
assert!(host.append_child(div, option));
|
||||
assert!(host.set_attribute(optgroup, "disabled", ""));
|
||||
assert!(engine.matches_host(&host, option, ":disabled").unwrap());
|
||||
|
||||
let disabled_select = host.create_element("select");
|
||||
let valid_optgroup = host.create_element("optgroup");
|
||||
let valid_option = host.create_element("option");
|
||||
let nested_optgroup = host.create_element("optgroup");
|
||||
let nested_optgroup_option = host.create_element("option");
|
||||
let parent_option = host.create_element("option");
|
||||
let nested_option = host.create_element("option");
|
||||
let hr = host.create_element("hr");
|
||||
let hr_option = host.create_element("option");
|
||||
let datalist = host.create_element("datalist");
|
||||
let datalist_option = host.create_element("option");
|
||||
assert!(host.set_attribute(disabled_select, "disabled", ""));
|
||||
assert!(host.append_child(body, disabled_select));
|
||||
assert!(host.append_child(disabled_select, valid_optgroup));
|
||||
assert!(host.append_child(valid_optgroup, valid_option));
|
||||
assert!(host.append_child(valid_optgroup, nested_optgroup));
|
||||
assert!(host.append_child(nested_optgroup, nested_optgroup_option));
|
||||
assert!(host.append_child(disabled_select, parent_option));
|
||||
assert!(host.append_child(parent_option, nested_option));
|
||||
assert!(host.append_child(disabled_select, hr));
|
||||
assert!(host.append_child(hr, hr_option));
|
||||
assert!(host.append_child(disabled_select, datalist));
|
||||
assert!(host.append_child(datalist, datalist_option));
|
||||
|
||||
assert!(
|
||||
engine
|
||||
.matches_host(&host, valid_optgroup, ":disabled")
|
||||
.unwrap()
|
||||
);
|
||||
assert!(
|
||||
engine
|
||||
.matches_host(&host, valid_option, ":disabled")
|
||||
.unwrap()
|
||||
);
|
||||
for handle in [
|
||||
nested_optgroup,
|
||||
nested_optgroup_option,
|
||||
nested_option,
|
||||
hr_option,
|
||||
datalist_option,
|
||||
] {
|
||||
assert!(!engine.matches_host(&host, handle, ":disabled").unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dom_api_selectors_disabled_fieldset_disables_option_descendants() {
|
||||
let url = url::Url::parse("https://example.test/").unwrap();
|
||||
|
||||
@@ -239,27 +239,17 @@ impl<'a> QueryElement<'a> {
|
||||
if self.element().has_attribute("disabled") {
|
||||
return true;
|
||||
}
|
||||
if self.element().local_name() == "option"
|
||||
&& self.parent_element().is_some_and(|(_, parent)| {
|
||||
parent.local_name() == "optgroup" && parent.has_attribute("disabled")
|
||||
})
|
||||
{
|
||||
if self.element().local_name() == "option" && self.host.option_is_disabled(self.handle) {
|
||||
return true;
|
||||
}
|
||||
if matches!(self.element().local_name(), "option" | "optgroup")
|
||||
&& self.disabled_select_ancestor().is_some()
|
||||
&& self.disabled_associated_select().is_some()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
self.disabled_fieldset_ancestor().is_some()
|
||||
}
|
||||
|
||||
pub(super) fn parent_element(self) -> Option<(NodeId, &'a Element)> {
|
||||
let parent = self.node().parent_node()?;
|
||||
let element = self.host.node(parent)?.as_element()?;
|
||||
Some((parent, element))
|
||||
}
|
||||
|
||||
pub(super) fn disabled_fieldset_ancestor(self) -> Option<NodeId> {
|
||||
let mut current = self.node().parent_node();
|
||||
while let Some(parent) = current {
|
||||
@@ -275,22 +265,17 @@ impl<'a> QueryElement<'a> {
|
||||
None
|
||||
}
|
||||
|
||||
pub(super) fn disabled_select_ancestor(self) -> Option<NodeId> {
|
||||
let mut current = self.node().parent_node();
|
||||
while let Some(parent) = current {
|
||||
let Some(element) = self.host.node(parent).and_then(Node::as_element) else {
|
||||
current = self.host.node(parent).and_then(Node::parent_node);
|
||||
continue;
|
||||
};
|
||||
match element.local_name() {
|
||||
"select" if element.has_attribute("disabled") => return Some(parent),
|
||||
"select" | "option" => return None,
|
||||
"optgroup" if self.element().local_name() == "optgroup" => return None,
|
||||
_ => {}
|
||||
}
|
||||
current = self.host.node(parent).and_then(Node::parent_node);
|
||||
}
|
||||
None
|
||||
pub(super) fn disabled_associated_select(self) -> Option<NodeId> {
|
||||
let select = match self.element().local_name() {
|
||||
"option" => self.host.option_nearest_ancestor_select(self.handle),
|
||||
"optgroup" => self.host.optgroup_nearest_ancestor_select(self.handle),
|
||||
_ => None,
|
||||
}?;
|
||||
self.host
|
||||
.node(select)
|
||||
.and_then(Node::as_element)
|
||||
.is_some_and(|element| element.has_attribute("disabled"))
|
||||
.then_some(select)
|
||||
}
|
||||
|
||||
pub(super) fn is_inside_first_legend_child(self, fieldset: NodeId) -> bool {
|
||||
|
||||
Reference in New Issue
Block a user