diff --git a/packages/blitz-dom/src/mutator.rs b/packages/blitz-dom/src/mutator.rs index e0dfd30c7..61089025f 100644 --- a/packages/blitz-dom/src/mutator.rs +++ b/packages/blitz-dom/src/mutator.rs @@ -351,6 +351,18 @@ impl DocumentMutator<'_> { let removed_attr = element.attrs.remove(&name); let had_attr = removed_attr.is_some(); + + // Clearing the checked attribute unchecks the checkbox, even if the + // attribute was not present (checkedness may have been set by a click). + if (&element.name.local, &name.local) == tag_and_attr!("input", "checked") { + if let Some(checked) = element.checkbox_input_checked_mut() { + if *checked { + *checked = false; + self.mutations_occurred |= node_is_in_document; + } + } + } + if !had_attr { return; } diff --git a/tests/blitz-tests/tests/checkbox_checked.rs b/tests/blitz-tests/tests/checkbox_checked.rs new file mode 100644 index 000000000..639273a69 --- /dev/null +++ b/tests/blitz-tests/tests/checkbox_checked.rs @@ -0,0 +1,109 @@ +//! Regression test for checkbox `checked` attribute reactivity. +//! See https://github.com/DioxusLabs/dioxus/issues/5282 +//! +//! When a signal drives the `checked` attribute of an ``, +//! toggling the signal off clears the attribute but must also update the +//! element's checkedness (stored in `SpecialElementData::CheckboxInput`). + +use blitz_test_harness::HarnessOptions; +use dioxus::prelude::*; +use dioxus_core::ScopeId; +use dioxus_native_dom::DioxusDocument; +use std::cell::Cell; +use std::rc::Rc; + +#[derive(Props, Clone)] +struct AppProps { + checked: Rc>, +} + +impl PartialEq for AppProps { + fn eq(&self, other: &Self) -> bool { + Rc::ptr_eq(&self.checked, &other.checked) + } +} + +fn app(props: AppProps) -> Element { + let checked = props.checked.get(); + rsx! { + input { + id: "cb", + type: "checkbox", + checked, + } + } +} + +struct Harness { + inner: blitz_test_harness::Harness, + checked: Rc>, +} + +impl Harness { + fn new(initial: bool) -> Self { + let checked = Rc::new(Cell::new(initial)); + let vdom = VirtualDom::new_with_props( + app, + AppProps { + checked: Rc::clone(&checked), + }, + ); + let inner = blitz_test_harness::Harness::from_vdom(vdom, HarnessOptions::default()); + Self { inner, checked } + } + + fn set_checked(&mut self, value: bool) { + self.checked.set(value); + self.inner.doc.vdom.mark_dirty(ScopeId::APP); + self.inner.pump(); + } + + fn checkbox_state(&self) -> bool { + let node_id = self.inner.node("#cb"); + let doc = self.inner.doc.inner.borrow(); + let node = doc.get_node(node_id).unwrap(); + node.element_data() + .unwrap() + .checkbox_input_checked() + .expect("element is not a checkbox") + } +} + +#[test] +fn checked_attribute_is_reactive() { + let mut harness = Harness::new(false); + assert!( + !harness.checkbox_state(), + "initial state should be unchecked" + ); + + harness.set_checked(true); + assert!( + harness.checkbox_state(), + "should be checked after set to true" + ); + + harness.set_checked(false); + assert!( + !harness.checkbox_state(), + "should be unchecked after set back to false" + ); + + harness.set_checked(true); + assert!( + harness.checkbox_state(), + "should be checked after set to true again" + ); +} + +#[test] +fn checked_attribute_initially_true_is_reactive() { + let mut harness = Harness::new(true); + assert!(harness.checkbox_state(), "initial state should be checked"); + + harness.set_checked(false); + assert!( + !harness.checkbox_state(), + "should be unchecked after set to false" + ); +}