mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 12:21:58 +00:00
feat(linter/jsx-a11y): add fixer for anchor-has-content (#4852)
Add a conditional fix that removes `aria-hidden` from an anchor's child if there is only a single child. This PR also fixes a false positive on hidden anchors. It should report visible anchors with hidden content, not hidden anchors.
This commit is contained in:
parent
a81ce3a3c7
commit
a6195a6a77
3 changed files with 97 additions and 11 deletions
|
|
@ -145,6 +145,16 @@ macro_rules! impl_from {
|
||||||
// but this breaks when implementing `From<RuleFix<'a>> for CompositeFix<'a>`.
|
// but this breaks when implementing `From<RuleFix<'a>> for CompositeFix<'a>`.
|
||||||
impl_from!(CompositeFix<'a>, Fix<'a>, Option<Fix<'a>>, Vec<Fix<'a>>);
|
impl_from!(CompositeFix<'a>, Fix<'a>, Option<Fix<'a>>, Vec<Fix<'a>>);
|
||||||
|
|
||||||
|
impl<'a> FromIterator<Fix<'a>> for RuleFix<'a> {
|
||||||
|
fn from_iter<T: IntoIterator<Item = Fix<'a>>>(iter: T) -> Self {
|
||||||
|
Self {
|
||||||
|
kind: FixKind::SafeFix,
|
||||||
|
message: None,
|
||||||
|
fix: iter.into_iter().collect::<Vec<_>>().into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a> From<RuleFix<'a>> for CompositeFix<'a> {
|
impl<'a> From<RuleFix<'a>> for CompositeFix<'a> {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn from(val: RuleFix<'a>) -> Self {
|
fn from(val: RuleFix<'a>) -> Self {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
use oxc_ast::AstKind;
|
use oxc_ast::{
|
||||||
|
ast::{JSXAttributeItem, JSXChild, JSXElement},
|
||||||
|
AstKind,
|
||||||
|
};
|
||||||
use oxc_diagnostics::OxcDiagnostic;
|
use oxc_diagnostics::OxcDiagnostic;
|
||||||
use oxc_macros::declare_oxc_lint;
|
use oxc_macros::declare_oxc_lint;
|
||||||
use oxc_span::Span;
|
use oxc_span::Span;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
context::LintContext,
|
context::LintContext,
|
||||||
|
fixer::{Fix, RuleFix},
|
||||||
rule::Rule,
|
rule::Rule,
|
||||||
utils::{
|
utils::{
|
||||||
get_element_type, has_jsx_prop_ignore_case, is_hidden_from_screen_reader,
|
get_element_type, has_jsx_prop_ignore_case, is_hidden_from_screen_reader,
|
||||||
|
|
@ -19,12 +23,6 @@ fn missing_content(span0: Span) -> OxcDiagnostic {
|
||||||
.with_label(span0)
|
.with_label(span0)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_aria_hidden(span0: Span) -> OxcDiagnostic {
|
|
||||||
OxcDiagnostic::warn("Missing accessible content when using `a` elements.")
|
|
||||||
.with_help("Remove the `aria-hidden` attribute to allow the anchor element and its content visible to assistive technologies.")
|
|
||||||
.with_label(span0)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default, Clone)]
|
||||||
pub struct AnchorHasContent;
|
pub struct AnchorHasContent;
|
||||||
|
|
||||||
|
|
@ -59,7 +57,8 @@ declare_oxc_lint!(
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
AnchorHasContent,
|
AnchorHasContent,
|
||||||
correctness
|
correctness,
|
||||||
|
conditional_suggestion
|
||||||
);
|
);
|
||||||
|
|
||||||
impl Rule for AnchorHasContent {
|
impl Rule for AnchorHasContent {
|
||||||
|
|
@ -70,7 +69,6 @@ impl Rule for AnchorHasContent {
|
||||||
};
|
};
|
||||||
if name == "a" {
|
if name == "a" {
|
||||||
if is_hidden_from_screen_reader(ctx, &jsx_el.opening_element) {
|
if is_hidden_from_screen_reader(ctx, &jsx_el.opening_element) {
|
||||||
ctx.diagnostic(remove_aria_hidden(jsx_el.span));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -84,12 +82,43 @@ impl Rule for AnchorHasContent {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.diagnostic(missing_content(jsx_el.span));
|
let diagnostic = missing_content(jsx_el.span);
|
||||||
|
if jsx_el.children.len() == 1 {
|
||||||
|
let child = &jsx_el.children[0];
|
||||||
|
if let JSXChild::Element(child) = child {
|
||||||
|
ctx.diagnostic_with_suggestion(diagnostic, |_fixer| {
|
||||||
|
remove_hidden_attributes(child)
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.diagnostic(diagnostic);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn remove_hidden_attributes<'a>(element: &JSXElement<'a>) -> RuleFix<'a> {
|
||||||
|
element
|
||||||
|
.opening_element
|
||||||
|
.attributes
|
||||||
|
.iter()
|
||||||
|
.filter_map(JSXAttributeItem::as_attribute)
|
||||||
|
.filter_map(|attr| {
|
||||||
|
attr.name.as_identifier().and_then(|name| {
|
||||||
|
if name.name.eq_ignore_ascii_case("aria-hidden")
|
||||||
|
|| name.name.eq_ignore_ascii_case("hidden")
|
||||||
|
{
|
||||||
|
Some(Fix::delete(attr.span))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test() {
|
fn test() {
|
||||||
use crate::tester::Tester;
|
use crate::tester::Tester;
|
||||||
|
|
@ -114,12 +143,28 @@ fn test() {
|
||||||
(r"<a title={title} />", None, None),
|
(r"<a title={title} />", None, None),
|
||||||
(r"<a aria-label={ariaLabel} />", None, None),
|
(r"<a aria-label={ariaLabel} />", None, None),
|
||||||
(r"<a title={title} aria-label={ariaLabel} />", None, None),
|
(r"<a title={title} aria-label={ariaLabel} />", None, None),
|
||||||
|
(r#"<a><Bar aria-hidden="false" /></a>"#, None, None),
|
||||||
|
// anchors can be hidden
|
||||||
|
(r"<a aria-hidden>Foo</a>", None, None),
|
||||||
|
(r#"<a aria-hidden="true">Foo</a>"#, None, None),
|
||||||
|
(r"<a hidden>Foo</a>", None, None),
|
||||||
|
(r"<a aria-hidden><span aria-hidden>Foo</span></a>", None, None),
|
||||||
|
(r#"<a hidden="true">Foo</a>"#, None, None),
|
||||||
|
(r#"<a hidden="">Foo</a>"#, None, None),
|
||||||
|
// TODO: should these be failing?
|
||||||
|
(r"<a><div hidden /></a>", None, None),
|
||||||
|
(r"<a><Bar hidden /></a>", None, None),
|
||||||
|
(r#"<a><Bar hidden="" /></a>"#, None, None),
|
||||||
|
(r#"<a><Bar hidden="until-hidden" /></a>"#, None, None),
|
||||||
];
|
];
|
||||||
|
|
||||||
let fail = vec![
|
let fail = vec![
|
||||||
(r"<a />", None, None),
|
(r"<a />", None, None),
|
||||||
(r"<a><Bar aria-hidden /></a>", None, None),
|
(r"<a><Bar aria-hidden /></a>", None, None),
|
||||||
|
(r#"<a><Bar aria-hidden="true" /></a>"#, None, None),
|
||||||
|
(r#"<a><input type="hidden" /></a>"#, None, None),
|
||||||
(r"<a>{undefined}</a>", None, None),
|
(r"<a>{undefined}</a>", None, None),
|
||||||
|
(r"<a>{null}</a>", None, None),
|
||||||
(
|
(
|
||||||
r"<Link />",
|
r"<Link />",
|
||||||
None,
|
None,
|
||||||
|
|
@ -129,5 +174,15 @@ fn test() {
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
Tester::new(AnchorHasContent::NAME, pass, fail).test_and_snapshot();
|
let fix = vec![
|
||||||
|
(r"<a><Bar aria-hidden /></a>", "<a><Bar /></a>"),
|
||||||
|
(r"<a><Bar aria-hidden>Can't see me</Bar></a>", r"<a><Bar >Can't see me</Bar></a>"),
|
||||||
|
(r"<a><Bar aria-hidden={true}>Can't see me</Bar></a>", r"<a><Bar >Can't see me</Bar></a>"),
|
||||||
|
(
|
||||||
|
r#"<a><Bar aria-hidden="true">Can't see me</Bar></a>"#,
|
||||||
|
r"<a><Bar >Can't see me</Bar></a>",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
Tester::new(AnchorHasContent::NAME, pass, fail).expect_fix(fix).test_and_snapshot();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,20 @@ source: crates/oxc_linter/src/tester.rs
|
||||||
╰────
|
╰────
|
||||||
help: Provide screen reader accessible content when using `a` elements.
|
help: Provide screen reader accessible content when using `a` elements.
|
||||||
|
|
||||||
|
⚠ eslint-plugin-jsx-a11y(anchor-has-content): Missing accessible content when using `a` elements.
|
||||||
|
╭─[anchor_has_content.tsx:1:1]
|
||||||
|
1 │ <a><Bar aria-hidden="true" /></a>
|
||||||
|
· ─────────────────────────────────
|
||||||
|
╰────
|
||||||
|
help: Provide screen reader accessible content when using `a` elements.
|
||||||
|
|
||||||
|
⚠ eslint-plugin-jsx-a11y(anchor-has-content): Missing accessible content when using `a` elements.
|
||||||
|
╭─[anchor_has_content.tsx:1:1]
|
||||||
|
1 │ <a><input type="hidden" /></a>
|
||||||
|
· ──────────────────────────────
|
||||||
|
╰────
|
||||||
|
help: Provide screen reader accessible content when using `a` elements.
|
||||||
|
|
||||||
⚠ eslint-plugin-jsx-a11y(anchor-has-content): Missing accessible content when using `a` elements.
|
⚠ eslint-plugin-jsx-a11y(anchor-has-content): Missing accessible content when using `a` elements.
|
||||||
╭─[anchor_has_content.tsx:1:1]
|
╭─[anchor_has_content.tsx:1:1]
|
||||||
1 │ <a>{undefined}</a>
|
1 │ <a>{undefined}</a>
|
||||||
|
|
@ -22,6 +36,13 @@ source: crates/oxc_linter/src/tester.rs
|
||||||
╰────
|
╰────
|
||||||
help: Provide screen reader accessible content when using `a` elements.
|
help: Provide screen reader accessible content when using `a` elements.
|
||||||
|
|
||||||
|
⚠ eslint-plugin-jsx-a11y(anchor-has-content): Missing accessible content when using `a` elements.
|
||||||
|
╭─[anchor_has_content.tsx:1:1]
|
||||||
|
1 │ <a>{null}</a>
|
||||||
|
· ─────────────
|
||||||
|
╰────
|
||||||
|
help: Provide screen reader accessible content when using `a` elements.
|
||||||
|
|
||||||
⚠ eslint-plugin-jsx-a11y(anchor-has-content): Missing accessible content when using `a` elements.
|
⚠ eslint-plugin-jsx-a11y(anchor-has-content): Missing accessible content when using `a` elements.
|
||||||
╭─[anchor_has_content.tsx:1:1]
|
╭─[anchor_has_content.tsx:1:1]
|
||||||
1 │ <Link />
|
1 │ <Link />
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue