1use crate::dom::Dom;
10use crate::node_id::NodeId;
11
12fn is_disableable_control(tag: &str) -> bool {
16 matches!(tag, "button" | "input" | "select" | "textarea" | "fieldset")
17}
18
19impl<Ext> Dom<Ext> {
20 pub fn is_actually_disabled(&self, id: NodeId) -> bool {
36 let Some(tag) = self.get_node(id).and_then(|n| n.tag_name()) else {
37 return false;
38 };
39 match tag {
40 "option" => {
41 self.has_attribute(id, "disabled")
42 || self.parent_element_id(id).is_some_and(|p| {
43 self.tag_is(p, "optgroup") && self.has_attribute(p, "disabled")
44 })
45 }
46 "optgroup" => self.has_attribute(id, "disabled"),
47 t if is_disableable_control(t) => {
48 self.has_attribute(id, "disabled") || self.in_disabled_fieldset(id)
49 }
50 _ => false,
51 }
52 }
53
54 pub fn is_enabled_control(&self, id: NodeId) -> bool {
59 let Some(tag) = self.get_node(id).and_then(|n| n.tag_name()) else {
60 return false;
61 };
62 (is_disableable_control(tag) || matches!(tag, "optgroup" | "option"))
63 && !self.is_actually_disabled(id)
64 }
65
66 fn in_disabled_fieldset(&self, id: NodeId) -> bool {
71 let mut via = id;
72 let mut cur = self.parent_element_id(id);
73 while let Some(anc) = cur {
74 if self.tag_is(anc, "fieldset")
75 && self.has_attribute(anc, "disabled")
76 && self.first_legend_child(anc) != Some(via)
77 {
78 return true;
79 }
80 via = anc;
81 cur = self.parent_element_id(anc);
82 }
83 false
84 }
85
86 fn first_legend_child(&self, fieldset: NodeId) -> Option<NodeId> {
88 let mut c = self.get_node(fieldset)?.first_child;
89 while let Some(cid) = c {
90 let n = self.get_node(cid)?;
91 if n.tag_name() == Some("legend") {
92 return Some(cid);
93 }
94 c = n.next_sibling;
95 }
96 None
97 }
98
99 pub(crate) fn parent_element_id(&self, id: NodeId) -> Option<NodeId> {
100 self.get_node(id).and_then(|n| n.parent)
101 }
102
103 pub(crate) fn tag_is(&self, id: NodeId, tag: &str) -> bool {
104 self.get_node(id).and_then(|n| n.tag_name()) == Some(tag)
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use crate::Dom;
111 use crate::node_id::NodeId;
112
113 fn el(dom: &mut Dom, parent: NodeId, tag: &str, attrs: &[(&str, &str)]) -> NodeId {
114 let e = dom.create_element(tag);
115 for (k, v) in attrs {
116 dom.set_attribute(e, k, v).unwrap();
117 }
118 dom.append_child(parent, e).unwrap();
119 e
120 }
121
122 #[test]
123 fn own_disabled_attribute_disables_a_control_but_not_a_div() {
124 let mut dom: Dom = Dom::new();
125 let root = dom.root();
126 let input = el(&mut dom, root, "input", &[("disabled", "")]);
127 let div = el(&mut dom, root, "div", &[("disabled", "")]);
128 let plain = el(&mut dom, root, "button", &[]);
129 assert!(dom.is_actually_disabled(input));
130 assert!(!dom.is_actually_disabled(div));
131 assert!(!dom.is_actually_disabled(plain));
132 }
133
134 #[test]
136 fn control_inside_disabled_fieldset_is_disabled() {
137 let mut dom: Dom = Dom::new();
138 let root = dom.root();
139 let fs = el(&mut dom, root, "fieldset", &[("disabled", "")]);
140 let div = el(&mut dom, fs, "div", &[]);
141 let input = el(&mut dom, div, "input", &[]);
142 let button = el(&mut dom, fs, "button", &[]);
143 let select = el(&mut dom, fs, "select", &[]);
144 let textarea = el(&mut dom, fs, "textarea", &[]);
145 for c in [input, button, select, textarea] {
146 assert!(dom.is_actually_disabled(c));
147 }
148 assert!(!dom.is_actually_disabled(div), "a div is never disabled");
149 }
150
151 #[test]
153 fn control_in_first_legend_stays_enabled_but_not_in_a_later_one() {
154 let mut dom: Dom = Dom::new();
155 let root = dom.root();
156 let fs = el(&mut dom, root, "fieldset", &[("disabled", "")]);
157 let legend = el(&mut dom, fs, "legend", &[]);
158 let in_legend = el(&mut dom, legend, "input", &[("type", "checkbox")]);
159 let legend2 = el(&mut dom, fs, "legend", &[]);
160 let in_legend2 = el(&mut dom, legend2, "input", &[]);
161 assert!(!dom.is_actually_disabled(in_legend));
162 assert!(dom.is_actually_disabled(in_legend2));
163 }
164
165 #[test]
167 fn nested_legend_that_is_not_a_child_is_no_exemption() {
168 let mut dom: Dom = Dom::new();
169 let root = dom.root();
170 let fs = el(&mut dom, root, "fieldset", &[("disabled", "")]);
171 let div = el(&mut dom, fs, "div", &[]);
172 let legend = el(&mut dom, div, "legend", &[]);
173 let input = el(&mut dom, legend, "input", &[]);
174 assert!(dom.is_actually_disabled(input));
175 }
176
177 #[test]
181 fn nested_fieldset_in_a_disabled_fieldset_disables_its_content() {
182 let mut dom: Dom = Dom::new();
183 let root = dom.root();
184 let outer = el(&mut dom, root, "fieldset", &[("disabled", "")]);
185 let inner = el(&mut dom, outer, "fieldset", &[]);
186 let inner_legend = el(&mut dom, inner, "legend", &[]);
187 let in_inner_legend = el(&mut dom, inner_legend, "input", &[]);
188 let input = el(&mut dom, inner, "input", &[]);
189 assert!(dom.is_actually_disabled(outer));
190 assert!(dom.is_actually_disabled(inner));
191 assert!(dom.is_actually_disabled(input));
192 assert!(dom.is_actually_disabled(in_inner_legend));
193 }
194
195 #[test]
199 fn inner_disabled_fieldset_and_legend_exempt_fieldset() {
200 let mut dom: Dom = Dom::new();
201 let root = dom.root();
202 let outer = el(&mut dom, root, "fieldset", &[]);
203 let inner = el(&mut dom, outer, "fieldset", &[("disabled", "")]);
204 let input = el(&mut dom, inner, "input", &[]);
205 assert!(!dom.is_actually_disabled(outer));
206 assert!(dom.is_actually_disabled(input));
207
208 let off = el(&mut dom, root, "fieldset", &[("disabled", "")]);
209 let legend = el(&mut dom, off, "legend", &[]);
210 let fs_in_legend = el(&mut dom, legend, "fieldset", &[]);
211 let deep = el(&mut dom, fs_in_legend, "input", &[]);
212 assert!(!dom.is_actually_disabled(fs_in_legend));
213 assert!(!dom.is_actually_disabled(deep));
214 }
215
216 #[test]
220 fn option_and_optgroup_rules() {
221 let mut dom: Dom = Dom::new();
222 let root = dom.root();
223 let fs = el(&mut dom, root, "fieldset", &[("disabled", "")]);
224 let select = el(&mut dom, fs, "select", &[]);
225 let og = el(&mut dom, select, "optgroup", &[("disabled", "")]);
226 let in_og = el(&mut dom, og, "option", &[]);
227 let own = el(&mut dom, select, "option", &[("disabled", "")]);
228 let plain = el(&mut dom, select, "option", &[]);
229 assert!(dom.is_actually_disabled(select));
230 assert!(dom.is_actually_disabled(og));
231 assert!(dom.is_actually_disabled(in_og));
232 assert!(dom.is_actually_disabled(own));
233 assert!(!dom.is_actually_disabled(plain));
234 }
235
236 #[test]
237 fn disabled_and_enabled_selectors_follow_the_predicate() {
238 let mut dom: Dom = Dom::new();
239 let root = dom.root();
240 let fs = el(&mut dom, root, "fieldset", &[("disabled", "")]);
241 let legend = el(&mut dom, fs, "legend", &[]);
242 let in_legend = el(&mut dom, legend, "input", &[]);
243 let input = el(&mut dom, fs, "input", &[]);
244 let div = el(&mut dom, root, "div", &[("disabled", "")]);
245
246 assert!(dom.matches(fs, ":disabled").unwrap());
247 assert!(dom.matches(input, ":disabled").unwrap());
248 assert!(dom.matches(input, "input:disabled").unwrap());
249 assert!(!dom.matches(input, ":enabled").unwrap());
250 assert!(!dom.matches(in_legend, ":disabled").unwrap());
251 assert!(dom.matches(in_legend, ":enabled").unwrap());
252 assert!(!dom.matches(div, ":disabled").unwrap());
254 assert!(!dom.matches(div, ":enabled").unwrap());
255 assert!(!dom.matches(legend, ":enabled").unwrap());
256 assert_eq!(
257 dom.query_selector_all_in(root, ":disabled").unwrap(),
258 vec![fs, input]
259 );
260 }
261}