use chromiumoxide::Page;
use crate::cli::WcagLevel;
use crate::wcag::types::{RuleMetadata, Severity, Violation};
pub(super) const CLICK_HANDLERS_RULE: RuleMetadata = RuleMetadata {
id: "2.1.1",
name: "Keyboard",
level: WcagLevel::A,
severity: Severity::High,
description: "Click handlers on non-interactive elements must be keyboard-accessible",
help_url: "https://www.w3.org/WAI/WCAG22/Understanding/keyboard.html",
axe_id: "click-events-have-key-events",
tags: &["wcag2a", "wcag211", "cat.keyboard"],
};
const CLICK_HANDLERS_JS: &str = r#"
(function() {
/*CSS_SELECTOR*/
const NON_INTERACTIVE = ['div', 'span', 'p', 'li', 'section', 'article'];
const INTERACTIVE_ROLES = new Set([
'button', 'link', 'checkbox', 'radio', 'switch',
'menuitem', 'menuitemcheckbox', 'menuitemradio',
'tab', 'option', 'treeitem'
]);
const findings = [];
try {
const all = document.querySelectorAll('[onclick]');
for (const el of Array.from(all)) {
const tag = el.tagName.toLowerCase();
if (!NON_INTERACTIVE.includes(tag)) continue;
const role = (el.getAttribute('role') || '').toLowerCase();
if (INTERACTIVE_ROLES.has(role)) continue;
const tabindex = el.getAttribute('tabindex');
const focusable = tabindex !== null && parseInt(tabindex, 10) >= 0;
if (focusable) continue;
__amsPush(findings, el, __amsCssSelector(el), 10);
if (__amsReal(findings) >= 10) break;
}
} catch(e) {}
return { count: findings.length, selectors: findings };
})()
"#;
pub async fn check_click_handlers_with_page(page: &Page) -> Vec<Violation> {
let val = match crate::wcag::types::evaluate_or_fail(
page,
&CLICK_HANDLERS_RULE,
&CLICK_HANDLERS_JS.replace(
"/*CSS_SELECTOR*/",
crate::accessibility::js_helpers::CSS_SELECTOR_JS,
),
)
.await
{
Ok(v) => v,
Err(violations) => return violations,
};
let count = val.get("count").and_then(|v| v.as_u64()).unwrap_or(0);
if count == 0 {
return vec![];
}
let selectors: Vec<String> = val
.get("selectors")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|s| s.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
selectors
.into_iter()
.map(|sel| {
Violation::new(
CLICK_HANDLERS_RULE.id,
CLICK_HANDLERS_RULE.name,
CLICK_HANDLERS_RULE.level,
Severity::High,
format!(
"Element with onclick handler on non-interactive tag has no keyboard equivalent ({sel})."
),
&sel,
)
.with_selector(&sel)
.with_fix(
"Replace with a native interactive element (`<button>`, `<a>`), or add `role=\"button\"` + `tabindex=\"0\"` + a keyboard handler (`onkeydown`/`onkeyup`).",
)
.with_rule_id(CLICK_HANDLERS_RULE.axe_id)
.with_help_url(CLICK_HANDLERS_RULE.help_url)
})
.collect()
}