use crate::accessibility::AXTree;
use crate::cli::WcagLevel;
use crate::wcag::types::{RuleMetadata, Severity, Violation, WcagResults};
pub const FOCUS_ORDER_RULE: RuleMetadata = RuleMetadata {
id: "2.4.3",
name: "Focus Order",
level: WcagLevel::A,
severity: Severity::High,
description: "Focusable components receive focus in an order that preserves meaning",
help_url: "https://www.w3.org/WAI/WCAG21/Understanding/focus-order.html",
axe_id: "focus-order-semantics",
tags: &["wcag2a", "wcag243", "cat.keyboard"],
};
pub fn check_focus_order(tree: &AXTree) -> WcagResults {
let mut results = WcagResults::new();
for node in tree.iter() {
if node.ignored {
continue;
}
let is_aria_hidden = node.get_property_bool("hidden").unwrap_or(false);
if is_aria_hidden && node.is_focusable() {
let violation = Violation::new(
FOCUS_ORDER_RULE.id,
FOCUS_ORDER_RULE.name,
FOCUS_ORDER_RULE.level,
Severity::Critical,
"Focusable element inside aria-hidden context",
node.node_id.clone(),
)
.with_role(node.role.clone())
.with_name(node.name.clone())
.with_fix(
"Either remove aria-hidden or make the element not focusable with tabindex=\"-1\"",
)
.with_help_url(FOCUS_ORDER_RULE.help_url)
.with_rule_id(FOCUS_ORDER_RULE.axe_id);
results.add_violation(violation);
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
use crate::accessibility::{AXNode, AXProperty, AXTree, AXValue};
fn node_with_hidden_and_focusable(
id: &str,
role: &str,
hidden: bool,
focusable: bool,
) -> AXNode {
AXNode {
node_id: id.to_string(),
ignored: false,
ignored_reasons: vec![],
role: Some(role.to_string()),
name: Some("Test".to_string()),
name_source: None,
description: None,
value: None,
properties: vec![
AXProperty {
name: "hidden".to_string(),
value: AXValue::Bool(hidden),
},
AXProperty {
name: "focusable".to_string(),
value: AXValue::Bool(focusable),
},
],
child_ids: vec![],
parent_id: None,
backend_dom_node_id: None,
}
}
#[test]
fn test_focusable_in_hidden_context_flagged() {
let tree = AXTree::from_nodes(vec![node_with_hidden_and_focusable(
"1", "button", true, true,
)]);
let results = check_focus_order(&tree);
assert_eq!(results.violations.len(), 1);
}
#[test]
fn test_focusable_not_hidden_passes() {
let tree = AXTree::from_nodes(vec![node_with_hidden_and_focusable(
"1", "button", false, true,
)]);
let results = check_focus_order(&tree);
assert_eq!(results.violations.len(), 0);
}
#[test]
fn test_hidden_not_focusable_passes() {
let tree = AXTree::from_nodes(vec![node_with_hidden_and_focusable(
"1", "button", true, false,
)]);
let results = check_focus_order(&tree);
assert_eq!(results.violations.len(), 0);
}
}