Repository: biomejs/biome (language: rust)

Issue:
🐛 `lint/a11y/noAutofocus` contradicts with MDN recommendations for <dialog>
### Environment information

```block
CLI:
  Version:                      1.9.4
  Color support:                true

Platform:
  CPU Architecture:             x86_64
  OS:                           linux

Environment:
  BIOME_LOG_PATH:               unset
  BIOME_LOG_PREFIX_NAME:        unset
  BIOME_CONFIG_PATH:            unset
  NO_COLOR:                     unset
  TERM:                         "xterm-256color"
  JS_RUNTIME_VERSION:           "v20.11.1"
  JS_RUNTIME_NAME:              "node"
  NODE_PACKAGE_MANAGER:         unset

Biome Configuration:
  Status:                       Loaded successfully
  Formatter disabled:           false
  Linter disabled:              false
  Organize imports disabled:    false
  VCS disabled:                 false

Workspace:
  Open Documents:               0
```


### What happened?

A `lint/a11y/noAutofocus` contradicts with MDN recommendations for <dialog>:

> The [autofocus](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/autofocus) attribute should be added to the element the user is expected to interact with immediately upon opening a modal dialog. If no other element involves more immediate interaction, it is recommended to add autofocus to the close button inside the dialog, or the dialog itself if the user is expected to click/activate it to dismiss.

https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog

[Code example](https://biomejs.dev/playground/?indentStyle=space&code=ZQB4AHAAbwByAHQAIABjAG8AbgBzAHQAIABBAHMAawBQAGEAcwBzAHcAbwByAGQARABpAGEAbABvAGcAIAA9ACAAKAApACAAPQA%2BACAAewAKACAAIABjAG8AbgBzAHQAIABpAGQAIAA9ACAAdQBzAGUASQBkACgAKQA7AAoAIAAgAGMAbwBuAHMAdAAgAFsAcABhAHMAcwB3AG8AcgBkACwAIABzAGUAdABQAGEAcwBzAHcAbwByAGQAXQAgAD0AIAB1AHMAZQBTAHQAYQB0AGUAKAAiACIAKQA7AAoACgAgACAAYwBvAG4AcwB0ACAAaABhAG4AZABsAGUAQwBoAGEAbgBnAGUAIAA9ACAAdQBzAGUAQwBhAGwAbABiAGEAYwBrACgAKABlAHYAZQBuAHQAKQAgAD0APgAgAHsACgAgACAAIAAgAHMAZQB0AFAAYQBzAHMAdwBvAHIAZAAoAGUAdgBlAG4AdAAuAHQAYQByAGcAZQB0AC4AdgBhAGwAdQBlACkAOwAKACAAIAB9ACwAIABbAF0AKQA7AAoACgAgACAAcgBlAHQAdQByAG4AIAAoAAoAIAAgACAAIAA8AGQAaQBhAGwAbwBnACAAbwBwAGUAbgA%2BAAoAIAAgACAAIAAgACAAPABmAG8AcgBtAD4ACgAgACAAIAAgACAAIAAgACAAPABsAGEAYgBlAGwAIABoAHQAbQBsAEYAbwByAD0AewBpAGQAfQA%2BAAoAIAAgACAAIAAgACAAIAAgACAAIABFAG4AdABlAHIAIABwAGEAcwBzAHcAbwByAGQACgAgACAAIAAgACAAIAAgACAAPAAvAGwAYQBiAGUAbAA%2BAAoAIAAgACAAIAAgACAAIAAgADwAaQBuAHAAdQB0AAoAIAAgACAAIAAgACAAIAAgACAAIAB0AHkAcABlAD0AIgBwAGEAcwBzAHcAbwByAGQAIgAKACAAIAAgACAAIAAgACAAIAAgACAAdgBhAGwAdQBlAD0AewBwAGEAcwBzAHcAbwByAGQAfQAKACAAIAAgACAAIAAgACAAIAAgACAAbwBuAEMAaABhAG4AZwBlAD0AewBoAGEAbgBkAGwAZQBDAGgAYQBuAGcAZQB9AAoAIAAgACAAIAAgACAAIAAgACAAIABhAHUAdABvAEYAbwBjAHUAcwAKACAAIAAgACAAIAAgACAAIAAgACAAYQB1AHQAbwBDAG8AbQBwAGwAZQB0AGUAPQAiAG8AZgBmACIACgAgACAAIAAgACAAIAAgACAAIAAgAGkAZAA9AHsAaQBkAH0ACgAgACAAIAAgACAAIAAgACAALwA%2BAAoAIAAgACAAIAAgACAAPAAvAGYAbwByAG0APgAKACAAIAAgACAAPAAvAGQAaQBhAGwAbwBnAD4ACgAgACAAKQA7AAoAfQA7AAoA)


### Expected result

It should permit `autoFocus` when it is used inside a dialog.

### Code of Conduct

- [X] I agree to follow Biome's Code of Conduct


Discussion / hints:
`autoFocus` should be valid also for elements that use the attribute `popover`.

Once we fix the issue, we should update the rule to be inspired and provide examples.

Fix patch:
diff --git a/crates/biome_js_analyze/src/lint/a11y/no_autofocus.rs b/crates/biome_js_analyze/src/lint/a11y/no_autofocus.rs
index 9b52a095cfa6..6867213b97cc 100644
--- a/crates/biome_js_analyze/src/lint/a11y/no_autofocus.rs
+++ b/crates/biome_js_analyze/src/lint/a11y/no_autofocus.rs
@@ -1,15 +1,19 @@
 use crate::JsRuleAction;
 use biome_analyze::{
-    context::RuleContext, declare_lint_rule, Ast, FixKind, Rule, RuleDiagnostic, RuleSource,
+    context::RuleContext, declare_lint_rule, FixKind, Phases, QueryMatch, Queryable, Rule,
+    RuleDiagnostic, RuleSource, Visitor,
 };
 use biome_console::markup;
-use biome_js_syntax::{jsx_ext::AnyJsxElement, JsxAttribute};
-use biome_rowan::{AstNode, BatchMutationExt};
+use biome_js_syntax::{jsx_ext::AnyJsxElement, JsLanguage, JsSyntaxKind, JsxAttribute};
+use biome_rowan::{AstNode, BatchMutationExt, WalkEvent};
+use biome_string_case::StrOnlyExtension;
 
 declare_lint_rule! {
     /// Enforce that autoFocus prop is not used on elements.
     ///
     /// Autofocusing elements can cause usability issues for sighted and non-sighted users, alike.
+    /// But the autofocus attribute should be added to the element the user is expected to
+    /// interact with immediately upon opening a modal dialog or popover.
     ///
     /// ## Examples
     ///
@@ -50,10 +54,21 @@ declare_lint_rule! {
     /// <MyComponent autoFocus={true} />
     ///```
     ///
+    /// ```jsx
+    /// // `autoFocus` prop in element has `popover` attribute is valid
+    /// <div popover><input autoFocus /></div>
+    /// ```
+    ///
+    /// ```jsx
+    /// // `autoFocus` prop in `dialog` is valid
+    /// <dialog><input autoFocus /></dialog>
+    /// ```
+    ///
     /// ## Resources
     ///
     /// - [WHATWG HTML Standard, The autofocus attribute](https://html.spec.whatwg.org/multipage/interaction.html#attr-fe-autofocus)
     /// - [The accessibility of HTML 5 autofocus](https://brucelawson.co.uk/2009/the-accessibility-of-html-5-autofocus/)
+    /// - [MDN Web Docs, HTMLElement: autofocus property](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/autofocus)
     ///
     pub NoAutofocus {
         version: "1.0.0",
@@ -65,8 +80,113 @@ declare_lint_rule! {
     }
 }
 
+fn find_kept_autofocus_mark(element: &AnyJsxElement) -> bool {
+    // the check for no_autofocus can be ignored
+    // 1. inside the `dialog` element
+    // 2. inside the element with the popover attribute
+
+    let is_dialog_element = match element.name_value_token() {
+        Some(syntax_token) => {
+            let tag_name = String::from(syntax_token.text_trimmed());
+            tag_name.to_lowercase_cow() == "dialog"
+        }
+        None => false,
+    };
+
+    let has_popover_attr = element.has_truthy_attribute("popover");
+
+    is_dialog_element || has_popover_attr
+}
+
+#[derive(Default)]
+struct ValidAutofocusVisitor {
+    stack: Vec<(AnyJsxElement, bool)>,
+}
+
+impl Visitor for ValidAutofocusVisitor {
+    type Language = JsLanguage;
+
+    fn visit(
+        &mut self,
+        event: &biome_rowan::WalkEvent<biome_rowan::SyntaxNode<Self::Language>>,
+        mut ctx: biome_analyze::VisitorContext<Self::Language>,
+    ) {
+        match event {
+            WalkEvent::Enter(node) => {
+                let kind = node.kind();
+
+                match kind {
+                    JsSyntaxKind::JSX_OPENING_ELEMENT => {
+                        let element = AnyJsxElement::unwrap_cast(node.clone());
+
+                        let is_hold = match self.stack.last() {
+                            None => false,
+                            Some((_, value)) => *value,
+                        };
+
+                        if is_hold {
+                            self.stack.push((element.clone(), true));
+                        } else {
+                            let next_hold = find_kept_autofocus_mark(&element);
+                            self.stack.push((element.clone(), next_hold));
+                        }
+
+                        ctx.match_query(ValidAutofocus(element));
+                    }
+                    JsSyntaxKind::JSX_SELF_CLOSING_ELEMENT => {
+                        let element = AnyJsxElement::unwrap_cast(node.clone());
+
+                        let is_hold = match self.stack.last() {
+                            None => false,
+                            Some((_, value)) => *value,
+                        };
+
+                        if !is_hold {
+                            ctx.match_query(ValidAutofocus(element));
+                        }
+                    }
+                    JsSyntaxKind::JSX_CLOSING_ELEMENT => {
+                        self.stack.pop();
+                    }
+                    _ => {}
+                }
+            }
+            WalkEvent::Leave(_) => {}
+        };
+    }
+}
+
+pub struct ValidAutofocus(AnyJsxElement);
+
+impl QueryMatch for ValidAutofocus {
+    fn text_range(&self) -> biome_rowan::TextRange {
+        self.0.range()
+    }
+}
+
+impl Queryable for ValidAutofocus {
+    type Input = Self;
+
+    type Output = AnyJsxElement;
+
+    type Language = JsLanguage;
+
+    type Services = ();
+
+    fn build_visitor(
+        analyzer: &mut impl biome_analyze::AddVisitor<Self::Language>,
+        _: &<Self::Language as biome_rowan::Language>::Root,
+    ) {
+        analyzer.add_visitor(Phases::Syntax, ValidAutofocusVisitor::default);
+    }
+
+    fn unwrap_match(_: &biome_analyze::ServiceBag, query: &Self::Input) -> Self::Output {
+        query.0.clone()
+    }
+}
+
 impl Rule for NoAutofocus {
-    type Query = Ast<AnyJsxElement>;
+    type Query = ValidAutofocus;
     type State = JsxAttribute;
     type Signals = Option<Self::State>;
     type Options = ();


Test patch:
diff --git a/crates/biome_js_analyze/tests/quick_test.rs b/crates/biome_js_analyze/tests/quick_test.rs
index 5ff05a2ad6cb..901aa8a2885c 100644
--- a/crates/biome_js_analyze/tests/quick_test.rs
+++ b/crates/biome_js_analyze/tests/quick_test.rs
@@ -14,7 +14,7 @@ use std::{ffi::OsStr, fs::read_to_string, path::Path, slice};
 #[ignore]
 #[test]
 fn quick_test() {
-    let input_file = Path::new("tests/specs/nursery/useSortedClasses/issue.jsx");
+    let input_file = Path::new("tests/specs/a11y/noAutofocus/invalid.jsx");
     let file_name = input_file.file_name().and_then(OsStr::to_str).unwrap();
 
     let (group, rule) = parse_test_path(input_file);
diff --git a/crates/biome_js_analyze/tests/specs/a11y/noAutofocus/invalid.jsx b/crates/biome_js_analyze/tests/specs/a11y/noAutofocus/invalid.jsx
index f4e2b6cb22e6..1cf6f211aa22 100644
--- a/crates/biome_js_analyze/tests/specs/a11y/noAutofocus/invalid.jsx
+++ b/crates/biome_js_analyze/tests/specs/a11y/noAutofocus/invalid.jsx
@@ -11,4 +11,5 @@
     <div autoFocus />
     <div autoFocus={true} />
     <div autoFocus={false} />
-</>
+    <div popover={false}><input autoFocus /></div>
+</>
\ No newline at end of file
diff --git a/crates/biome_js_analyze/tests/specs/a11y/noAutofocus/invalid.jsx.snap b/crates/biome_js_analyze/tests/specs/a11y/noAutofocus/invalid.jsx.snap
index ae200810008c..ee01682a964a 100644
--- a/crates/biome_js_analyze/tests/specs/a11y/noAutofocus/invalid.jsx.snap
+++ b/crates/biome_js_analyze/tests/specs/a11y/noAutofocus/invalid.jsx.snap
@@ -17,8 +17,8 @@ expression: invalid.jsx
     <div autoFocus />
     <div autoFocus={true} />
     <div autoFocus={false} />
+    <div popover={false}><input autoFocus /></div>
 </>
-
 ```
 
 # Diagnostics
@@ -231,7 +231,7 @@ invalid.jsx:12:10 lint/a11y/noAutofocus  FIXABLE  ━━━━━━━━━━
   > 12 │     <div autoFocus={true} />
        │          ^^^^^^^^^^^^^^^^
     13 │     <div autoFocus={false} />
-    14 │ </>
+    14 │     <div popover={false}><input autoFocus /></div>
   
   i Unsafe fix: Remove the autoFocus attribute.
   
@@ -249,8 +249,8 @@ invalid.jsx:13:10 lint/a11y/noAutofocus  FIXABLE  ━━━━━━━━━━
     12 │     <div autoFocus={true} />
   > 13 │     <div autoFocus={false} />
        │          ^^^^^^^^^^^^^^^^^
-    14 │ </>
-    15 │ 
+    14 │     <div popover={false}><input autoFocus /></div>
+    15 │ </>
   
   i Unsafe fix: Remove the autoFocus attribute.
   
@@ -259,4 +259,20 @@ invalid.jsx:13:10 lint/a11y/noAutofocus  FIXABLE  ━━━━━━━━━━
 
 ```
 
+```
+invalid.jsx:14:33 lint/a11y/noAutofocus  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 
+  ! Avoid the autoFocus attribute.
+  
+    12 │     <div autoFocus={true} />
+    13 │     <div autoFocus={false} />
+  > 14 │     <div popover={false}><input autoFocus /></div>
+       │                                 ^^^^^^^^^
+    15 │ </>
+  
+  i Unsafe fix: Remove the autoFocus attribute.
+  
+    14 │ ····<div·popover={false}><input·autoFocus·/></div>
+       │                                 ----------        
+
+```
diff --git a/crates/biome_js_analyze/tests/specs/a11y/noAutofocus/valid.jsx b/crates/biome_js_analyze/tests/specs/a11y/noAutofocus/valid.jsx
index 9fcd23f52ff0..2ce51413d803 100644
--- a/crates/biome_js_analyze/tests/specs/a11y/noAutofocus/valid.jsx
+++ b/crates/biome_js_analyze/tests/specs/a11y/noAutofocus/valid.jsx
@@ -5,4 +5,6 @@
     <div autofocus />
     <input autofocus="true" />
     <MyComponent autoFocus={true} />
-</>
+    <div popover><input autoFocus />></div>
+    <dialog><input autoFocus />></dialog>
+</>
\ No newline at end of file
diff --git a/crates/biome_js_analyze/tests/specs/a11y/noAutofocus/valid.jsx.snap b/crates/biome_js_analyze/tests/specs/a11y/noAutofocus/valid.jsx.snap
index b8edc246d709..f99a8f94fe9d 100644
--- a/crates/biome_js_analyze/tests/specs/a11y/noAutofocus/valid.jsx.snap
+++ b/crates/biome_js_analyze/tests/specs/a11y/noAutofocus/valid.jsx.snap
@@ -11,8 +11,7 @@ expression: valid.jsx
     <div autofocus />
     <input autofocus="true" />
     <MyComponent autoFocus={true} />
+    <div popover><input autoFocus />></div>
+    <dialog><input autoFocus />></dialog>
 </>
-
 ```
-
-