use crate::context::LintContext;
use crate::diagnostic::Severity;
use crate::rule::{Rule, RuleCategory, RuleMeta};
use vize_relief::ast::{ElementNode, ElementType, ExpressionNode, PropNode};
static META: RuleMeta = RuleMeta {
name: "a11y/click-events-have-key-events",
description: "Require keyboard event handlers with click events",
category: RuleCategory::Accessibility,
fixable: false,
default_severity: Severity::Warning,
};
#[derive(Default)]
pub struct ClickEventsHaveKeyEvents;
impl ClickEventsHaveKeyEvents {
fn is_interactive_element(tag: &str) -> bool {
matches!(
tag,
"a" | "button"
| "input"
| "select"
| "textarea"
| "details"
| "summary"
| "video"
| "audio"
)
}
fn has_interactive_role(element: &ElementNode) -> bool {
for prop in &element.props {
if let PropNode::Attribute(attr) = prop {
if attr.name == "role" {
if let Some(value) = &attr.value {
return matches!(
value.content.as_ref(),
"button"
| "link"
| "checkbox"
| "menuitem"
| "menuitemcheckbox"
| "menuitemradio"
| "option"
| "radio"
| "searchbox"
| "switch"
| "textbox"
| "tab"
| "treeitem"
| "gridcell"
);
}
}
}
}
false
}
fn has_click_handler(element: &ElementNode) -> bool {
for prop in &element.props {
if let PropNode::Directive(dir) = prop {
if dir.name == "on" {
if let Some(ExpressionNode::Simple(arg)) = &dir.arg {
if arg.content == "click" {
return true;
}
}
}
}
}
false
}
fn has_keyboard_handler(element: &ElementNode) -> bool {
for prop in &element.props {
if let PropNode::Directive(dir) = prop {
if dir.name == "on" {
if let Some(ExpressionNode::Simple(arg)) = &dir.arg {
if matches!(arg.content.as_ref(), "keydown" | "keyup" | "keypress") {
return true;
}
}
}
}
}
false
}
}
impl Rule for ClickEventsHaveKeyEvents {
fn meta(&self) -> &'static RuleMeta {
&META
}
fn enter_element<'a>(&self, ctx: &mut LintContext<'a>, element: &ElementNode<'a>) {
if element.tag_type == ElementType::Component {
return;
}
if Self::is_interactive_element(&element.tag) {
return;
}
if Self::has_interactive_role(element) {
return;
}
if Self::has_click_handler(element) && !Self::has_keyboard_handler(element) {
ctx.warn_with_help(
ctx.t("a11y/click-events-have-key-events.message"),
&element.loc,
ctx.t("a11y/click-events-have-key-events.help"),
);
}
}
}
#[cfg(test)]
mod tests {
use super::ClickEventsHaveKeyEvents;
use crate::linter::Linter;
use crate::rule::RuleRegistry;
fn create_linter() -> Linter {
let mut registry = RuleRegistry::new();
registry.register(Box::new(ClickEventsHaveKeyEvents));
Linter::with_registry(registry)
}
#[test]
fn test_valid_button() {
let linter = create_linter();
let result =
linter.lint_template(r#"<button @click="handleClick">Click</button>"#, "test.vue");
assert_eq!(result.warning_count, 0);
}
#[test]
fn test_valid_div_with_both() {
let linter = create_linter();
let result = linter.lint_template(
r#"<div @click="handleClick" @keydown="handleKeydown">Click</div>"#,
"test.vue",
);
assert_eq!(result.warning_count, 0);
}
#[test]
fn test_valid_div_with_role_button() {
let linter = create_linter();
let result = linter.lint_template(
r#"<div role="button" @click="handleClick">Click</div>"#,
"test.vue",
);
assert_eq!(result.warning_count, 0);
}
#[test]
fn test_invalid_div_click_only() {
let linter = create_linter();
let result = linter.lint_template(r#"<div @click="handleClick">Click</div>"#, "test.vue");
assert_eq!(result.warning_count, 1);
}
#[test]
fn test_invalid_span_click_only() {
let linter = create_linter();
let result = linter.lint_template(r#"<span @click="toggle">Toggle</span>"#, "test.vue");
assert_eq!(result.warning_count, 1);
}
#[test]
fn test_valid_vue_component_with_click() {
let linter = create_linter();
let result = linter.lint_template(
r#"<MkButton @click="handleClick">Click</MkButton>"#,
"test.vue",
);
assert_eq!(
result.warning_count, 0,
"Should not flag Vue components with @click"
);
}
#[test]
fn test_valid_nuxt_link_with_click() {
let linter = create_linter();
let result = linter.lint_template(r#"<NuxtLink @click.stop>Link</NuxtLink>"#, "test.vue");
assert_eq!(
result.warning_count, 0,
"Should not flag NuxtLink component"
);
}
}