|
1 | | -use std::sync::LazyLock; |
| 1 | +use std::{collections::HashMap, sync::LazyLock}; |
2 | 2 |
|
3 | 3 | use aria_query::{ |
4 | 4 | AriaRoleDefinitionKey, AriaRoleRelationConcept, AriaRoleRelationConceptAttributeConstraint, |
5 | 5 | ELEMENT_ROLES, |
6 | 6 | }; |
| 7 | +use dom_accessibility_api::{ |
| 8 | + compute_accessible_description, compute_accessible_name, ComputeTextAlternativeOptions, |
| 9 | +}; |
7 | 10 | use wasm_bindgen::JsCast; |
8 | | -use web_sys::{Element, HtmlElement, HtmlInputElement, HtmlOptionElement, Node}; |
| 11 | +use web_sys::{Element, HtmlElement, HtmlInputElement, HtmlOptionElement}; |
9 | 12 |
|
10 | | -use crate::types::ByRoleOptionsCurrent; |
| 13 | +use crate::{pretty_dom, types::ByRoleOptionsCurrent, util::html_collection_to_vec}; |
11 | 14 |
|
12 | 15 | struct ElementRole { |
13 | 16 | r#match: Box<dyn Fn(&Element) -> bool + Send + Sync>, |
@@ -179,26 +182,120 @@ pub fn get_implicit_aria_roles(current_node: &Element) -> Vec<AriaRoleDefinition |
179 | 182 | vec![] |
180 | 183 | } |
181 | 184 |
|
182 | | -pub fn get_roles(_container: Node) -> Vec<String> { |
183 | | - fn _flatten_dom(node: Node) -> Vec<Node> { |
184 | | - let nodes = vec![node.clone()]; |
185 | | - if let Some(_element) = node.dyn_ref::<Element>() { |
186 | | - todo!() |
187 | | - // nodes.extend(element.children()); |
188 | | - } |
189 | | - nodes |
| 185 | +#[derive(Clone, Default)] |
| 186 | +pub struct GetRolesOptions { |
| 187 | + pub hidden: Option<bool>, |
| 188 | +} |
| 189 | + |
| 190 | +pub fn get_roles( |
| 191 | + container: Element, |
| 192 | + options: GetRolesOptions, |
| 193 | +) -> HashMap<AriaRoleDefinitionKey, Vec<Element>> { |
| 194 | + fn flatten_dom(element: Element) -> Vec<Element> { |
| 195 | + let mut elements = vec![element.clone()]; |
| 196 | + elements.extend( |
| 197 | + html_collection_to_vec::<Element>(element.children()) |
| 198 | + .into_iter() |
| 199 | + .flat_map(flatten_dom) |
| 200 | + .collect::<Vec<_>>(), |
| 201 | + ); |
| 202 | + elements |
190 | 203 | } |
191 | 204 |
|
192 | | - todo!() |
193 | | - // flatten_dom(contaier).into_iter().filter(|_element| { |
194 | | - // // TODO |
195 | | - // false |
196 | | - // }) |
197 | | - //.fold(init, f) |
| 205 | + let hidden = options.hidden.unwrap_or(false); |
| 206 | + |
| 207 | + flatten_dom(container) |
| 208 | + .into_iter() |
| 209 | + .filter(|element| hidden || !is_inaccessible(element)) |
| 210 | + .fold(HashMap::new(), |mut acc, element| { |
| 211 | + // TODO: This violates html-aria which does not allow any role on every element. |
| 212 | + let roles = if element.has_attribute("role") { |
| 213 | + element |
| 214 | + .get_attribute("role") |
| 215 | + .expect("Attribute should exist.") |
| 216 | + .split(' ') |
| 217 | + .filter_map(|role| role.parse::<AriaRoleDefinitionKey>().ok()) |
| 218 | + .take(1) |
| 219 | + .collect::<Vec<_>>() |
| 220 | + } else { |
| 221 | + get_implicit_aria_roles(&element) |
| 222 | + }; |
| 223 | + |
| 224 | + for role in roles { |
| 225 | + acc.entry(role) |
| 226 | + .and_modify(|entry| entry.push(element.clone())) |
| 227 | + .or_insert_with(|| vec![element.clone()]); |
| 228 | + } |
| 229 | + |
| 230 | + acc |
| 231 | + }) |
| 232 | +} |
| 233 | + |
| 234 | +#[derive(Clone, Default)] |
| 235 | +pub struct PrettyRolesOptions { |
| 236 | + pub hidden: Option<bool>, |
| 237 | + pub include_description: Option<bool>, |
| 238 | +} |
| 239 | + |
| 240 | +fn pretty_roles(dom: Element, options: PrettyRolesOptions) -> String { |
| 241 | + let roles = get_roles( |
| 242 | + dom, |
| 243 | + GetRolesOptions { |
| 244 | + hidden: options.hidden, |
| 245 | + }, |
| 246 | + ); |
| 247 | + |
| 248 | + roles |
| 249 | + .into_iter() |
| 250 | + // We prefer to skip generic role, we don't recommend it. |
| 251 | + .filter(|(role, _)| *role != AriaRoleDefinitionKey::Generic) |
| 252 | + .map(|(role, elements)| { |
| 253 | + let delimiter_bar = "-".repeat(50); |
| 254 | + let elements_string = elements |
| 255 | + .iter() |
| 256 | + .map(|element| { |
| 257 | + let name_string = format!( |
| 258 | + "Name \"{}\":\n", |
| 259 | + compute_accessible_name(element, ComputeTextAlternativeOptions::default()) |
| 260 | + ); |
| 261 | + |
| 262 | + let dom_string = pretty_dom( |
| 263 | + Some( |
| 264 | + element |
| 265 | + .clone_node_with_deep(false) |
| 266 | + .expect("Node should be cloned.") |
| 267 | + .dyn_into::<Element>() |
| 268 | + .expect("Cloned node should be an Element.") |
| 269 | + .into(), |
| 270 | + ), |
| 271 | + None, |
| 272 | + ); |
| 273 | + |
| 274 | + if options.include_description.unwrap_or(false) { |
| 275 | + let description_string = format!( |
| 276 | + "Description \"{}\":", |
| 277 | + compute_accessible_description( |
| 278 | + element, |
| 279 | + ComputeTextAlternativeOptions::default() |
| 280 | + ) |
| 281 | + ); |
| 282 | + |
| 283 | + format!("{name_string}{description_string}{dom_string}") |
| 284 | + } else { |
| 285 | + format!("{name_string}{dom_string}") |
| 286 | + } |
| 287 | + }) |
| 288 | + .collect::<Vec<_>>() |
| 289 | + .join("\n\n"); |
| 290 | + |
| 291 | + format!("{role}:\n\n{elements_string}\n\n{delimiter_bar}") |
| 292 | + }) |
| 293 | + .collect::<Vec<_>>() |
| 294 | + .join("\n") |
198 | 295 | } |
199 | 296 |
|
200 | | -pub fn log_roles() { |
201 | | - todo!() |
| 297 | +pub fn log_roles(dom: Element, options: PrettyRolesOptions) { |
| 298 | + log::info!("{}", pretty_roles(dom, options)); |
202 | 299 | } |
203 | 300 |
|
204 | 301 | pub fn compute_aria_selected(element: &Element) -> Option<bool> { |
|
0 commit comments