Fix :enabled/:disabled/:checked pseudo-classes matching real DOM state
These were bundled in with the truly dynamic, interaction-only pseudo-classes (:hover/:active/:focus) and always resolved to false. That's right for :hover et al (there's no live cursor against a static snapshot) but wrong for :enabled/:disabled/:checked: those reflect real, static DOM state (the disabled/checked attributes), not user interaction, and modern component systems (Codex, Material, Bootstrap) lean on :enabled constantly for their base button/input styling. With it always false, every such base rule was silently inert, which is why the Wikipedia TOC toggle button (a <button> with no disabled attribute, so :enabled should be true) kept its default black border instead of the transparent one its :enabled.cdx-button--weight-quiet rule sets. :enabled now matches a form control (input/button/select/textarea/ optgroup/option/fieldset) without a disabled attribute, :disabled the opposite, and :checked matches the checked/selected attribute.
This commit is contained in:
1 parent
ca4a4ef135
commit
55e6787b0b
1 file changed
+65
-8
@@ -221,6 +221,31 @@ impl<'a> DomElement<'a> {
|
|||||||
pub fn new(tree: &'a DomTree, node_id: NodeId) -> Self {
|
pub fn new(tree: &'a DomTree, node_id: NodeId) -> Self {
|
||||||
DomElement { tree, node_id }
|
DomElement { tree, node_id }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Is this a form control element that `:enabled`/`:disabled` apply to?
|
||||||
|
fn is_form_control(&self) -> bool {
|
||||||
|
self.tree
|
||||||
|
.with_node(self.node_id, |n| {
|
||||||
|
n.as_element()
|
||||||
|
.map(|name| {
|
||||||
|
matches!(
|
||||||
|
name.local.as_ref(),
|
||||||
|
"input" | "button" | "select" | "textarea" | "optgroup" | "option" | "fieldset"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap_or(false)
|
||||||
|
})
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A boolean HTML attribute's presence (its value does not matter: per
|
||||||
|
/// HTML, `disabled=""`, `disabled="disabled"`, and bare `disabled` are
|
||||||
|
/// all equally "set").
|
||||||
|
fn has_boolean_attr(&self, name: &str) -> bool {
|
||||||
|
self.tree
|
||||||
|
.with_node(self.node_id, |n| n.get_attribute(name).is_some())
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> std::fmt::Debug for DomElement<'a> {
|
impl<'a> std::fmt::Debug for DomElement<'a> {
|
||||||
@@ -413,14 +438,20 @@ impl<'a> Element for DomElement<'a> {
|
|||||||
match pc {
|
match pc {
|
||||||
PseudoClass::Link => self.is_link(),
|
PseudoClass::Link => self.is_link(),
|
||||||
PseudoClass::Visited => false,
|
PseudoClass::Visited => false,
|
||||||
// Dynamic user-interaction and form-state pseudo-classes have no
|
// :enabled/:disabled/:checked reflect real, static DOM state (the
|
||||||
// meaning against a static DOM snapshot with no live user input.
|
// disabled/checked attributes), not live user interaction, so
|
||||||
PseudoClass::Hover
|
// they resolve the same way against a static snapshot as they
|
||||||
| PseudoClass::Active
|
// would in a browser that never received an input event. Modern
|
||||||
| PseudoClass::Focus
|
// component systems (Codex, Material, Bootstrap, ...) lean on
|
||||||
| PseudoClass::Enabled
|
// :enabled constantly for their default/base styling, so
|
||||||
| PseudoClass::Disabled
|
// treating it as unconditionally false (as :hover/:active
|
||||||
| PseudoClass::Checked => false,
|
// correctly are) made every such base rule silently inert.
|
||||||
|
PseudoClass::Enabled => self.is_form_control() && !self.has_boolean_attr("disabled"),
|
||||||
|
PseudoClass::Disabled => self.is_form_control() && self.has_boolean_attr("disabled"),
|
||||||
|
PseudoClass::Checked => self.has_boolean_attr("checked") || self.has_boolean_attr("selected"),
|
||||||
|
// Dynamic user-interaction pseudo-classes have no meaning against
|
||||||
|
// a static DOM snapshot with no live user input.
|
||||||
|
PseudoClass::Hover | PseudoClass::Active | PseudoClass::Focus => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -922,6 +953,32 @@ mod tests {
|
|||||||
assert_eq!(none.len(), 0, "a:has(span.bt) should match nothing");
|
assert_eq!(none.len(), 0, "a:has(span.bt) should match nothing");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_enabled_matches_form_control_without_disabled_attr() {
|
||||||
|
let tree = parse_html(r#"<button>Click</button><button disabled>Nope</button>"#);
|
||||||
|
let enabled = tree.query_selector_all("button:enabled").unwrap();
|
||||||
|
assert_eq!(enabled.len(), 1, ":enabled should match only the non-disabled button");
|
||||||
|
let disabled = tree.query_selector_all("button:disabled").unwrap();
|
||||||
|
assert_eq!(disabled.len(), 1, ":disabled should match only the disabled button");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_enabled_does_not_match_non_form_elements() {
|
||||||
|
// :enabled/:disabled only apply to form controls; a plain div should
|
||||||
|
// never match either, disabled attribute or not.
|
||||||
|
let tree = parse_html(r#"<div disabled>x</div>"#);
|
||||||
|
assert_eq!(tree.query_selector_all("div:enabled").unwrap().len(), 0);
|
||||||
|
assert_eq!(tree.query_selector_all("div:disabled").unwrap().len(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_checked_matches_checked_attribute() {
|
||||||
|
let tree = parse_html(
|
||||||
|
r#"<input type="checkbox" checked><input type="checkbox">"#,
|
||||||
|
);
|
||||||
|
assert_eq!(tree.query_selector_all("input:checked").unwrap().len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_query_selector_descendant() {
|
fn test_query_selector_descendant() {
|
||||||
let tree =
|
let tree =
|
||||||
|
|||||||
Reference in new issue
Block a user