-
-
Notifications
You must be signed in to change notification settings - Fork 472
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(linter): eslint-plugin-jsx-a11y click-events-have-key-events (#1976
) ( #1974 was lost due to mishandling. 🙈 ) Part of #1141 ### Refs - https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/4c7e7815c12a797587bb8e3cdced7f3003848964/docs/rules/click-events-have-key-events.md - https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/4c7e7815c12a797587bb8e3cdced7f3003848964/src/rules/click-events-have-key-events.js - https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/4c7e7815c12a797587bb8e3cdced7f3003848964/__tests__/src/rules/click-events-have-key-events-test.js --------- Co-authored-by: Boshen <boshenc@gmail.com>
- Loading branch information
Showing
4 changed files
with
283 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
150 changes: 150 additions & 0 deletions
150
crates/oxc_linter/src/rules/jsx_a11y/click_events_have_key_events.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
use oxc_ast::AstKind; | ||
use oxc_diagnostics::{ | ||
miette::{self, Diagnostic}, | ||
thiserror::Error, | ||
}; | ||
use oxc_macros::declare_oxc_lint; | ||
use oxc_span::Span; | ||
|
||
use crate::{ | ||
context::LintContext, | ||
globals::HTML_TAG, | ||
rule::Rule, | ||
utils::{ | ||
get_element_type, has_jsx_prop, is_hidden_from_screen_reader, is_interactive_element, | ||
is_presentation_role, | ||
}, | ||
AstNode, | ||
}; | ||
|
||
#[derive(Debug, Error, Diagnostic)] | ||
#[error("eslint-plugin-jsx-a11y(click-events-have-key-events): Enforce a clickable non-interactive element has at least one keyboard event listener.")] | ||
#[diagnostic(severity(warning), help("Visible, non-interactive elements with click handlers must have one of keyup, keydown, or keypress listener."))] | ||
struct ClickEventsHaveKeyEventsDiagnostic(#[label] pub Span); | ||
|
||
#[derive(Debug, Default, Clone)] | ||
pub struct ClickEventsHaveKeyEvents; | ||
|
||
declare_oxc_lint!( | ||
/// ### What it does | ||
/// | ||
/// Enforce onClick is accompanied by at least one of the following: onKeyUp, onKeyDown, onKeyPress. | ||
/// | ||
/// ### Why is this bad? | ||
/// | ||
/// Coding for the keyboard is important for users with physical disabilities who cannot use a mouse, AT compatibility, and screenreader users. | ||
/// This does not apply for interactive or hidden elements. | ||
/// | ||
/// ### Example | ||
/// ```jsx | ||
/// // Good | ||
/// <div onClick={() => void 0} onKeyDown={() => void 0} /> | ||
/// | ||
/// // Bad | ||
/// <div onClick={() => void 0} /> | ||
/// ``` | ||
ClickEventsHaveKeyEvents, | ||
correctness | ||
); | ||
|
||
impl Rule for ClickEventsHaveKeyEvents { | ||
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { | ||
let AstKind::JSXOpeningElement(jsx_opening_el) = node.kind() else { | ||
return; | ||
}; | ||
|
||
if has_jsx_prop(jsx_opening_el, "onClick").is_none() { | ||
return; | ||
}; | ||
|
||
// Check only native DOM elements or custom component via settings | ||
let Some(element_type) = get_element_type(ctx, jsx_opening_el) else { | ||
return; | ||
}; | ||
if !HTML_TAG.contains(&element_type) { | ||
return; | ||
}; | ||
|
||
if is_hidden_from_screen_reader(jsx_opening_el) || is_presentation_role(jsx_opening_el) { | ||
return; | ||
} | ||
|
||
if is_interactive_element(&element_type, jsx_opening_el) { | ||
return; | ||
} | ||
|
||
if ["onKeyUp", "onKeyDown", "onKeyPress"] | ||
.iter() | ||
.find_map(|prop| has_jsx_prop(jsx_opening_el, prop)) | ||
.is_some() | ||
{ | ||
return; | ||
} | ||
|
||
ctx.diagnostic(ClickEventsHaveKeyEventsDiagnostic(jsx_opening_el.span)); | ||
} | ||
} | ||
|
||
#[test] | ||
fn test() { | ||
use crate::tester::Tester; | ||
|
||
let pass = vec![ | ||
(r"<div onClick={() => void 0} onKeyDown={foo}/>;", None, None, None), | ||
(r"<div onClick={() => void 0} onKeyUp={foo} />;", None, None, None), | ||
(r"<div onClick={() => void 0} onKeyPress={foo}/>;", None, None, None), | ||
(r"<div onClick={() => void 0} onKeyDown={foo} onKeyUp={bar} />;", None, None, None), | ||
(r"<div onClick={() => void 0} onKeyDown={foo} {...props} />;", None, None, None), | ||
(r#"<div className="foo" />;"#, None, None, None), | ||
(r"<div onClick={() => void 0} aria-hidden />;", None, None, None), | ||
(r"<div onClick={() => void 0} aria-hidden={true} />;", None, None, None), | ||
(r"<div onClick={() => void 0} aria-hidden={false} onKeyDown={foo} />;", None, None, None), | ||
( | ||
r"<div onClick={() => void 0} onKeyDown={foo} aria-hidden={undefined} />;", | ||
None, | ||
None, | ||
None, | ||
), | ||
(r#"<input type="text" onClick={() => void 0} />"#, None, None, None), | ||
(r"<input onClick={() => void 0} />", None, None, None), | ||
(r#"<button onClick={() => void 0} className="foo" />"#, None, None, None), | ||
(r#"<select onClick={() => void 0} className="foo" />"#, None, None, None), | ||
(r#"<textarea onClick={() => void 0} className="foo" />"#, None, None, None), | ||
(r#"<a onClick={() => void 0} href="http://x.y.z" />"#, None, None, None), | ||
(r#"<a onClick={() => void 0} href="http://x.y.z" tabIndex="0" />"#, None, None, None), | ||
(r#"<input onClick={() => void 0} type="hidden" />;"#, None, None, None), | ||
(r#"<div onClick={() => void 0} role="presentation" />;"#, None, None, None), | ||
(r#"<div onClick={() => void 0} role="none" />;"#, None, None, None), | ||
(r"<TestComponent onClick={doFoo} />", None, None, None), | ||
(r"<Button onClick={doFoo} />", None, None, None), | ||
(r"<Footer onClick={doFoo} />", None, None, None), | ||
]; | ||
|
||
let fail = vec![ | ||
(r"<div onClick={() => void 0} />;", None, None, None), | ||
(r"<div onClick={() => void 0} role={undefined} />;", None, None, None), | ||
(r"<div onClick={() => void 0} {...props} />;", None, None, None), | ||
(r"<section onClick={() => void 0} />;", None, None, None), | ||
(r"<main onClick={() => void 0} />;", None, None, None), | ||
(r"<article onClick={() => void 0} />;", None, None, None), | ||
(r"<header onClick={() => void 0} />;", None, None, None), | ||
(r"<footer onClick={() => void 0} />;", None, None, None), | ||
(r"<div onClick={() => void 0} aria-hidden={false} />;", None, None, None), | ||
(r"<a onClick={() => void 0} />", None, None, None), | ||
(r#"<a tabIndex="0" onClick={() => void 0} />"#, None, None, None), | ||
( | ||
r"<Footer onClick={doFoo} />", | ||
None, | ||
Some(serde_json::json!({ | ||
"jsx-a11y": { | ||
"components": { | ||
"Footer": "footer", | ||
} | ||
} | ||
})), | ||
None, | ||
), | ||
]; | ||
|
||
Tester::new(ClickEventsHaveKeyEvents::NAME, pass, fail).test_and_snapshot(); | ||
} |
89 changes: 89 additions & 0 deletions
89
crates/oxc_linter/src/snapshots/click_events_have_key_events.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
--- | ||
source: crates/oxc_linter/src/tester.rs | ||
expression: click_events_have_key_events | ||
--- | ||
⚠ eslint-plugin-jsx-a11y(click-events-have-key-events): Enforce a clickable non-interactive element has at least one keyboard event listener. | ||
╭─[click_events_have_key_events.tsx:1:1] | ||
1 │ <div onClick={() => void 0} />; | ||
· ────────────────────────────── | ||
╰──── | ||
help: Visible, non-interactive elements with click handlers must have one of keyup, keydown, or keypress listener. | ||
|
||
⚠ eslint-plugin-jsx-a11y(click-events-have-key-events): Enforce a clickable non-interactive element has at least one keyboard event listener. | ||
╭─[click_events_have_key_events.tsx:1:1] | ||
1 │ <div onClick={() => void 0} role={undefined} />; | ||
· ─────────────────────────────────────────────── | ||
╰──── | ||
help: Visible, non-interactive elements with click handlers must have one of keyup, keydown, or keypress listener. | ||
|
||
⚠ eslint-plugin-jsx-a11y(click-events-have-key-events): Enforce a clickable non-interactive element has at least one keyboard event listener. | ||
╭─[click_events_have_key_events.tsx:1:1] | ||
1 │ <div onClick={() => void 0} {...props} />; | ||
· ───────────────────────────────────────── | ||
╰──── | ||
help: Visible, non-interactive elements with click handlers must have one of keyup, keydown, or keypress listener. | ||
|
||
⚠ eslint-plugin-jsx-a11y(click-events-have-key-events): Enforce a clickable non-interactive element has at least one keyboard event listener. | ||
╭─[click_events_have_key_events.tsx:1:1] | ||
1 │ <section onClick={() => void 0} />; | ||
· ────────────────────────────────── | ||
╰──── | ||
help: Visible, non-interactive elements with click handlers must have one of keyup, keydown, or keypress listener. | ||
|
||
⚠ eslint-plugin-jsx-a11y(click-events-have-key-events): Enforce a clickable non-interactive element has at least one keyboard event listener. | ||
╭─[click_events_have_key_events.tsx:1:1] | ||
1 │ <main onClick={() => void 0} />; | ||
· ─────────────────────────────── | ||
╰──── | ||
help: Visible, non-interactive elements with click handlers must have one of keyup, keydown, or keypress listener. | ||
|
||
⚠ eslint-plugin-jsx-a11y(click-events-have-key-events): Enforce a clickable non-interactive element has at least one keyboard event listener. | ||
╭─[click_events_have_key_events.tsx:1:1] | ||
1 │ <article onClick={() => void 0} />; | ||
· ────────────────────────────────── | ||
╰──── | ||
help: Visible, non-interactive elements with click handlers must have one of keyup, keydown, or keypress listener. | ||
|
||
⚠ eslint-plugin-jsx-a11y(click-events-have-key-events): Enforce a clickable non-interactive element has at least one keyboard event listener. | ||
╭─[click_events_have_key_events.tsx:1:1] | ||
1 │ <header onClick={() => void 0} />; | ||
· ───────────────────────────────── | ||
╰──── | ||
help: Visible, non-interactive elements with click handlers must have one of keyup, keydown, or keypress listener. | ||
|
||
⚠ eslint-plugin-jsx-a11y(click-events-have-key-events): Enforce a clickable non-interactive element has at least one keyboard event listener. | ||
╭─[click_events_have_key_events.tsx:1:1] | ||
1 │ <footer onClick={() => void 0} />; | ||
· ───────────────────────────────── | ||
╰──── | ||
help: Visible, non-interactive elements with click handlers must have one of keyup, keydown, or keypress listener. | ||
|
||
⚠ eslint-plugin-jsx-a11y(click-events-have-key-events): Enforce a clickable non-interactive element has at least one keyboard event listener. | ||
╭─[click_events_have_key_events.tsx:1:1] | ||
1 │ <div onClick={() => void 0} aria-hidden={false} />; | ||
· ────────────────────────────────────────────────── | ||
╰──── | ||
help: Visible, non-interactive elements with click handlers must have one of keyup, keydown, or keypress listener. | ||
|
||
⚠ eslint-plugin-jsx-a11y(click-events-have-key-events): Enforce a clickable non-interactive element has at least one keyboard event listener. | ||
╭─[click_events_have_key_events.tsx:1:1] | ||
1 │ <a onClick={() => void 0} /> | ||
· ──────────────────────────── | ||
╰──── | ||
help: Visible, non-interactive elements with click handlers must have one of keyup, keydown, or keypress listener. | ||
|
||
⚠ eslint-plugin-jsx-a11y(click-events-have-key-events): Enforce a clickable non-interactive element has at least one keyboard event listener. | ||
╭─[click_events_have_key_events.tsx:1:1] | ||
1 │ <a tabIndex="0" onClick={() => void 0} /> | ||
· ───────────────────────────────────────── | ||
╰──── | ||
help: Visible, non-interactive elements with click handlers must have one of keyup, keydown, or keypress listener. | ||
|
||
⚠ eslint-plugin-jsx-a11y(click-events-have-key-events): Enforce a clickable non-interactive element has at least one keyboard event listener. | ||
╭─[click_events_have_key_events.tsx:1:1] | ||
1 │ <Footer onClick={doFoo} /> | ||
· ────────────────────────── | ||
╰──── | ||
help: Visible, non-interactive elements with click handlers must have one of keyup, keydown, or keypress listener. | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters