Skip to main content

rdom_core/
radio_group.rs

1//! Radio button groups — HTML §4.10.5.1.18. Names, form owners and tree
2//! shape only; no rendering, no runtime.
3//!
4//! Two `<input type="radio">` elements *a* and *b* are in the same radio
5//! button group when they are in the same tree, have the same form owner
6//! (or both have none — [`Dom::form_owner`], which honours `form=`), and
7//! both carry a non-empty `name` whose values are equal. The comparison
8//! is exact: HTML dropped the old compatibility-caseless match, so
9//! `name="G"` and `name="g"` are two groups. A radio without a `name`
10//! (or with an empty one) is a group of its own.
11//!
12//! Exclusivity, arrow-key movement, the single Tab stop and
13//! `valueMissing` in `rdom-tui` all use this one definition.
14
15use crate::dom::Dom;
16use crate::input_type::InputTypeState;
17use crate::node_id::NodeId;
18
19/// What identifies a named radio's group: tree root, form owner, name.
20struct GroupKey<'a> {
21    root: NodeId,
22    owner: Option<NodeId>,
23    name: &'a str,
24}
25
26impl<Ext> Dom<Ext> {
27    /// Whether `a` and `b` are in the same radio button group (HTML
28    /// §4.10.5.1.18). Every radio is in its own group, so `a == b` is
29    /// `true` for a radio; anything that is not an `<input type="radio">`
30    /// is in no group at all.
31    pub fn in_same_radio_group(&self, a: NodeId, b: NodeId) -> bool {
32        if !self.is_radio(a) || !self.is_radio(b) {
33            return false;
34        }
35        if a == b {
36            return true;
37        }
38        match (self.radio_group_key(a), self.radio_group_key(b)) {
39            (Some(ka), Some(kb)) => {
40                ka.root == kb.root && ka.owner == kb.owner && ka.name == kb.name
41            }
42            _ => false,
43        }
44    }
45
46    /// The radio button group of `id` in tree order, `id` included
47    /// (HTML §4.10.5.1.18). A radio without a non-empty `name` is alone
48    /// in its group; a non-radio has none (empty). One pass over `id`'s
49    /// tree; form owners are resolved only for same-named radios.
50    pub fn radio_group(&self, id: NodeId) -> Vec<NodeId> {
51        if !self.is_radio(id) {
52            return Vec::new();
53        }
54        let Some(key) = self.radio_group_key(id) else {
55            return vec![id];
56        };
57        let mut out = Vec::new();
58        let mut stack = vec![key.root];
59        while let Some(cur) = stack.pop() {
60            let Some(node) = self.get_node(cur) else {
61                continue;
62            };
63            if cur == id
64                || (self.is_radio(cur)
65                    && self.get_attribute(cur, "name") == Some(key.name)
66                    && self.form_owner(cur) == key.owner)
67            {
68                out.push(cur);
69            }
70            // Push children in reverse so they pop in tree order.
71            let first = stack.len();
72            let mut c = node.first_child;
73            while let Some(cid) = c {
74                stack.push(cid);
75                c = self.get_node(cid).and_then(|n| n.next_sibling);
76            }
77            stack[first..].reverse();
78        }
79        out
80    }
81
82    fn is_radio(&self, id: NodeId) -> bool {
83        self.input_type_state(id) == Some(InputTypeState::Radio)
84    }
85
86    /// The group key of a radio with a non-empty `name`; `None` for a
87    /// nameless radio.
88    fn radio_group_key(&self, id: NodeId) -> Option<GroupKey<'_>> {
89        let name = self.get_attribute(id, "name").filter(|n| !n.is_empty())?;
90        Some(GroupKey {
91            root: self.root_of(id)?,
92            owner: self.form_owner(id),
93            name,
94        })
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use crate::Dom;
101    use crate::node_id::NodeId;
102
103    fn el(dom: &mut Dom, parent: NodeId, tag: &str, attrs: &[(&str, &str)]) -> NodeId {
104        let e = dom.create_element(tag);
105        for (k, v) in attrs {
106            dom.set_attribute(e, k, v).unwrap();
107        }
108        dom.append_child(parent, e).unwrap();
109        e
110    }
111
112    fn radio(dom: &mut Dom, parent: NodeId, attrs: &[(&str, &str)]) -> NodeId {
113        let r = el(dom, parent, "input", &[("type", "radio")]);
114        for (k, v) in attrs {
115            dom.set_attribute(r, k, v).unwrap();
116        }
117        r
118    }
119
120    #[test]
121    fn same_named_radios_in_different_forms_are_different_groups() {
122        let mut dom: Dom = Dom::new();
123        let root = dom.root();
124        let fa = el(&mut dom, root, "form", &[]);
125        let fb = el(&mut dom, root, "form", &[]);
126        let a1 = radio(&mut dom, fa, &[("name", "g")]);
127        let a2 = radio(&mut dom, fa, &[("name", "g")]);
128        let b1 = radio(&mut dom, fb, &[("name", "g")]);
129        let loose = radio(&mut dom, root, &[("name", "g")]);
130        assert_eq!(dom.radio_group(a2), vec![a1, a2]);
131        assert_eq!(dom.radio_group(b1), vec![b1]);
132        assert_eq!(dom.radio_group(loose), vec![loose]);
133        assert!(dom.in_same_radio_group(a1, a2));
134        assert!(!dom.in_same_radio_group(a1, b1));
135        assert!(!dom.in_same_radio_group(a1, loose));
136    }
137
138    #[test]
139    fn a_form_attribute_moves_a_radio_into_that_forms_group() {
140        let mut dom: Dom = Dom::new();
141        let root = dom.root();
142        let before = radio(&mut dom, root, &[("name", "g"), ("form", "f")]);
143        let f = el(&mut dom, root, "form", &[("id", "f")]);
144        let inside = radio(&mut dom, f, &[("name", "g")]);
145        let elsewhere = radio(&mut dom, f, &[("name", "g"), ("form", "nope")]);
146        let unowned = radio(&mut dom, root, &[("name", "g")]);
147        assert_eq!(dom.radio_group(inside), vec![before, inside]);
148        assert_eq!(dom.radio_group(elsewhere), vec![elsewhere, unowned]);
149    }
150
151    #[test]
152    fn names_compare_exactly_and_an_empty_name_is_alone() {
153        let mut dom: Dom = Dom::new();
154        let root = dom.root();
155        let upper = radio(&mut dom, root, &[("name", "G")]);
156        let lower = radio(&mut dom, root, &[("name", "g")]);
157        let empty1 = radio(&mut dom, root, &[("name", "")]);
158        let empty2 = radio(&mut dom, root, &[("name", "")]);
159        let none = radio(&mut dom, root, &[]);
160        assert!(!dom.in_same_radio_group(upper, lower));
161        assert!(!dom.in_same_radio_group(empty1, empty2));
162        assert_eq!(dom.radio_group(empty1), vec![empty1]);
163        assert_eq!(dom.radio_group(none), vec![none]);
164        assert!(dom.in_same_radio_group(none, none));
165    }
166
167    #[test]
168    fn non_radios_and_other_trees_are_not_in_the_group() {
169        let mut dom: Dom = Dom::new();
170        let root = dom.root();
171        let r = radio(&mut dom, root, &[("name", "g")]);
172        let cb = el(
173            &mut dom,
174            root,
175            "input",
176            &[("type", "checkbox"), ("name", "g")],
177        );
178        let upper_type = el(&mut dom, root, "input", &[("type", "RADIO"), ("name", "g")]);
179        let detached = dom.create_element("div");
180        let other_tree = radio(&mut dom, detached, &[("name", "g")]);
181        assert_eq!(dom.radio_group(r), vec![r, upper_type]);
182        assert!(dom.radio_group(cb).is_empty());
183        assert!(!dom.in_same_radio_group(r, cb));
184        assert!(!dom.in_same_radio_group(r, other_tree));
185        assert_eq!(dom.radio_group(other_tree), vec![other_tree]);
186    }
187}