99
1010use biome_aria:: event_handlers:: matches_event_handler;
1111use biome_html_syntax:: element_ext:: AnyHtmlTagElement ;
12- use biome_html_syntax:: { AnyHtmlAttribute , AnyVueDirective , HtmlAttribute } ;
12+ use biome_html_syntax:: { AnyHtmlAttribute , AnyVueDirective } ;
1313use biome_rowan:: AstNodeList ;
1414
1515// ============================================================================
@@ -23,38 +23,52 @@ use biome_rowan::AstNodeList;
2323/// Missing values, empty strings, and whitespace-only values are considered falsy.
2424///
2525/// Ref: <https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-hidden>
26- fn is_aria_hidden_value_truthy ( attribute : & HtmlAttribute ) -> bool {
27- attribute. value ( ) . is_some_and ( |value| {
28- let trimmed = value. trim ( ) ;
26+ fn is_aria_hidden_value_truthy ( attribute : & AnyHtmlAttribute ) -> bool {
27+ attribute. as_static_value ( ) . is_some_and ( |value| {
28+ let trimmed = value. text ( ) . trim ( ) ;
2929 !trimmed. is_empty ( ) && !trimmed. eq_ignore_ascii_case ( "false" )
3030 } )
3131}
3232
3333/// Checks if an attribute value equals `"true"` exactly (case-sensitive).
34- fn is_strict_true_value ( attribute : & HtmlAttribute ) -> bool {
35- attribute. value ( ) . is_some_and ( |value| value == "true" )
34+ fn is_strict_true_value ( attribute : & AnyHtmlAttribute ) -> bool {
35+ attribute
36+ . as_static_value ( )
37+ . is_some_and ( |value| value. text ( ) == "true" )
3638}
3739
3840/// Checks if an attribute has a non-empty value after trimming whitespace.
3941///
4042/// Returns `false` for attributes with no value, empty strings, or whitespace-only values.
41- fn has_non_empty_value ( attribute : & HtmlAttribute ) -> bool {
42- attribute
43- . value ( )
44- . is_some_and ( |value| !value. trim ( ) . is_empty ( ) )
43+ fn has_non_empty_value ( attribute : & AnyHtmlAttribute ) -> bool {
44+ match attribute {
45+ AnyHtmlAttribute :: HtmlAttribute ( html_attribute) => {
46+ if html_attribute. initializer ( ) . is_none ( ) {
47+ return false ;
48+ }
49+ if let Some ( static_value) = html_attribute. as_static_value ( )
50+ && let Some ( static_value) = static_value. as_string_constant ( )
51+ {
52+ return !static_value. trim ( ) . is_empty ( ) ;
53+ }
54+ true
55+ }
56+ // Vue directives always have a bound expression, so always non-empty
57+ _ => true ,
58+ }
4559}
4660
4761/// Checks if an attribute value matches the expected string (case-insensitive).
4862///
4963/// Useful for checking HTML attribute values like `type="hidden"` or `role="button"`
5064/// where the comparison should be case-insensitive per HTML spec.
5165pub ( crate ) fn attribute_value_equals_ignore_case (
52- attribute : & HtmlAttribute ,
66+ attribute : & AnyHtmlAttribute ,
5367 expected : & str ,
5468) -> bool {
5569 attribute
56- . value ( )
57- . is_some_and ( |value| value. eq_ignore_ascii_case ( expected) )
70+ . as_static_value ( )
71+ . is_some_and ( |value| value. text ( ) . eq_ignore_ascii_case ( expected) )
5872}
5973
6074// ============================================================================
@@ -72,17 +86,22 @@ pub(crate) fn attribute_value_equals_ignore_case(
7286/// - <https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/hidden>
7387/// - <https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/v6.10.0/src/util/isHiddenFromScreenReader.js>
7488pub ( crate ) fn is_hidden_from_screen_reader ( element : & AnyHtmlTagElement ) -> bool {
75- if element
76- . find_attribute_by_name ( "aria-hidden" )
77- . is_some_and ( |attr| is_aria_hidden_value_truthy ( & attr) )
89+ if let Some ( attr) = element. find_attribute_or_vue_binding ( "aria-hidden" )
90+ && is_aria_hidden_value_truthy ( & attr)
7891 {
7992 return true ;
8093 }
8194
8295 match element. name_value_token ( ) {
83- Some ( name) if name. text_trimmed ( ) == "input" => element
84- . find_attribute_by_name ( "type" )
85- . is_some_and ( |attr| attribute_value_equals_ignore_case ( & attr, "hidden" ) ) ,
96+ Some ( name) if name. text_trimmed ( ) == "input" => {
97+ if let Some ( attr) = element. find_attribute_or_vue_binding ( "type" )
98+ && attribute_value_equals_ignore_case ( & attr, "hidden" )
99+ {
100+ return true ;
101+ }
102+
103+ false
104+ }
86105 _ => false ,
87106 }
88107}
@@ -93,9 +112,13 @@ pub(crate) fn is_hidden_from_screen_reader(element: &AnyHtmlTagElement) -> bool
93112///
94113/// Ref: <https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-hidden>
95114pub ( crate ) fn is_aria_hidden_true ( element : & AnyHtmlTagElement ) -> bool {
96- element
97- . find_attribute_by_name ( "aria-hidden" )
98- . is_some_and ( |attr| is_strict_true_value ( & attr) )
115+ if let Some ( attr) = element. find_attribute_or_vue_binding ( "aria-hidden" )
116+ && is_strict_true_value ( & attr)
117+ {
118+ return true ;
119+ }
120+
121+ false
99122}
100123
101124/// Returns the `aria-hidden` attribute if it has a truthy value.
@@ -106,8 +129,8 @@ pub(crate) fn is_aria_hidden_true(element: &AnyHtmlTagElement) -> bool {
106129/// Ref: <https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-hidden>
107130pub ( crate ) fn get_truthy_aria_hidden_attribute (
108131 element : & AnyHtmlTagElement ,
109- ) -> Option < HtmlAttribute > {
110- let attribute = element. find_attribute_by_name ( "aria-hidden" ) ?;
132+ ) -> Option < AnyHtmlAttribute > {
133+ let attribute = element. find_attribute_or_vue_binding ( "aria-hidden" ) ?;
111134 if is_aria_hidden_value_truthy ( & attribute) {
112135 Some ( attribute)
113136 } else {
@@ -119,9 +142,14 @@ pub(crate) fn get_truthy_aria_hidden_attribute(
119142///
120143/// Whitespace-only values are considered empty.
121144pub ( crate ) fn has_non_empty_attribute ( element : & AnyHtmlTagElement , name : & str ) -> bool {
122- element
123- . find_attribute_by_name ( name)
124- . is_some_and ( |attr| has_non_empty_value ( & attr) )
145+ let Some ( attr) = element. find_attribute_or_vue_binding ( name) else {
146+ return false ;
147+ } ;
148+
149+ match attr {
150+ AnyHtmlAttribute :: HtmlAttribute ( _) => has_non_empty_value ( & attr) ,
151+ _ => true ,
152+ }
125153}
126154
127155/// Returns `true` if the element has all of the named attributes with non-empty values.
@@ -163,9 +191,13 @@ pub(crate) fn has_accessible_name(element: &AnyHtmlTagElement) -> bool {
163191pub ( crate ) fn html_element_has_truthy_aria_hidden (
164192 element : & biome_html_syntax:: HtmlElement ,
165193) -> bool {
166- element
167- . find_attribute_by_name ( "aria-hidden" )
168- . is_some_and ( |attr| is_aria_hidden_value_truthy ( & attr) )
194+ if let Some ( attr) = element. find_attribute_or_vue_binding ( "aria-hidden" )
195+ && is_aria_hidden_value_truthy ( & attr)
196+ {
197+ return true ;
198+ }
199+
200+ false
169201}
170202
171203/// Checks if an [`HtmlSelfClosingElement`] has a truthy `aria-hidden` attribute.
@@ -174,9 +206,13 @@ pub(crate) fn html_element_has_truthy_aria_hidden(
174206pub ( crate ) fn html_self_closing_element_has_truthy_aria_hidden (
175207 element : & biome_html_syntax:: HtmlSelfClosingElement ,
176208) -> bool {
177- element
178- . find_attribute_by_name ( "aria-hidden" )
179- . is_some_and ( |attr| is_aria_hidden_value_truthy ( & attr) )
209+ if let Some ( attr) = element. find_attribute_or_vue_binding ( "aria-hidden" )
210+ && is_aria_hidden_value_truthy ( & attr)
211+ {
212+ return true ;
213+ }
214+
215+ false
180216}
181217
182218/// Checks if an [`HtmlSelfClosingElement`] has an accessible name via `aria-label`,
@@ -186,16 +222,25 @@ pub(crate) fn html_self_closing_element_has_truthy_aria_hidden(
186222pub ( crate ) fn html_self_closing_element_has_accessible_name (
187223 element : & biome_html_syntax:: HtmlSelfClosingElement ,
188224) -> bool {
189- let has_aria_label = element
190- . find_attribute_by_name ( "aria-label" )
191- . is_some_and ( |attr| has_non_empty_value ( & attr) ) ;
192- let has_aria_labelledby = element
193- . find_attribute_by_name ( "aria-labelledby" )
194- . is_some_and ( |attr| has_non_empty_value ( & attr) ) ;
195- let has_title = element
196- . find_attribute_by_name ( "title" )
197- . is_some_and ( |attr| has_non_empty_value ( & attr) ) ;
198- has_aria_label || has_aria_labelledby || has_title
225+ if let Some ( attr) = element. find_attribute_or_vue_binding ( "aria-label" )
226+ && has_non_empty_value ( & attr)
227+ {
228+ return true ;
229+ }
230+
231+ if let Some ( attr) = element. find_attribute_or_vue_binding ( "aria-labelledby" )
232+ && has_non_empty_value ( & attr)
233+ {
234+ return true ;
235+ }
236+
237+ if let Some ( attr) = element. find_attribute_or_vue_binding ( "title" )
238+ && has_non_empty_value ( & attr)
239+ {
240+ return true ;
241+ }
242+
243+ false
199244}
200245
201246/// Checks if an [`HtmlSelfClosingElement`] has the named attribute with a non-empty value.
@@ -205,9 +250,13 @@ pub(crate) fn html_self_closing_element_has_non_empty_attribute(
205250 element : & biome_html_syntax:: HtmlSelfClosingElement ,
206251 name : & str ,
207252) -> bool {
208- element
209- . find_attribute_by_name ( name)
210- . is_some_and ( |attr| has_non_empty_value ( & attr) )
253+ if let Some ( attr) = element. find_attribute_or_vue_binding ( name)
254+ && has_non_empty_value ( & attr)
255+ {
256+ return true ;
257+ }
258+
259+ false
211260}
212261
213262/// Check if the element is `contentEditable`
@@ -216,9 +265,13 @@ pub(crate) fn html_self_closing_element_has_non_empty_attribute(
216265/// - https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/contenteditable
217266/// - https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/v6.10.0/src/util/isContentEditable.js
218267pub ( crate ) fn is_content_editable ( element : & AnyHtmlTagElement ) -> bool {
219- element
220- . find_attribute_by_name ( "contenteditable" )
221- . is_some_and ( |attribute| is_strict_true_value ( & attribute) )
268+ if let Some ( attr) = element. find_attribute_or_vue_binding ( "contenteditable" )
269+ && is_strict_true_value ( & attr)
270+ {
271+ return true ;
272+ }
273+
274+ false
222275}
223276
224277/// Check if the element contains event handler
0 commit comments