Skip to content

Commit

Permalink
refactor(aria): partially migrate roles from aria to aria-metadata
Browse files Browse the repository at this point in the history
  • Loading branch information
Conaclos committed Nov 7, 2024
1 parent 820d063 commit e3d9726
Show file tree
Hide file tree
Showing 16 changed files with 172 additions and 502 deletions.
20 changes: 0 additions & 20 deletions crates/biome_aria/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,3 @@ pub use roles::AriaRoles;
pub fn is_aria_property_valid(property: &str) -> bool {
AriaAttribute::from_str(property).is_ok()
}

#[cfg(test)]
mod test {
use crate::roles::AriaRoles;

#[test]
fn property_is_required() {
let roles = AriaRoles;

let role = roles.get_role("checkbox");

assert!(role.is_some());

let role = role.unwrap();

assert!(role.is_property_required("aria-checked"));
assert!(!role.is_property_required("aria-sort"));
assert!(!role.is_property_required("aria-bnlabla"));
}
}
172 changes: 82 additions & 90 deletions crates/biome_aria/src/roles.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::{define_role, is_aria_property_valid};
use biome_aria_metadata::AriaAttribute;
use biome_aria_metadata::{AriaAttribute, AriaRole};
use rustc_hash::FxHashMap;
use std::fmt::Debug;
use std::slice::Iter;
Expand Down Expand Up @@ -1054,112 +1054,109 @@ impl<'a> AriaRoles {
element: &str,
// To generate `attributes`, you can use `biome_js_analyze::services::aria::AriaServices::extract_defined_attributes`
attributes: &FxHashMap<String, Vec<String>>,
) -> Option<&'static dyn AriaRoleDefinition> {
let result = match element {
"article" => &ArticleRole as &dyn AriaRoleDefinition,
"aside" => &ComplementaryRole as &dyn AriaRoleDefinition,
"blockquote" => &BlockQuoteRole as &dyn AriaRoleDefinition,
"button" => &ButtonRole as &dyn AriaRoleDefinition,
"caption" => &CaptionRole as &dyn AriaRoleDefinition,
"code" => &CodeRole as &dyn AriaRoleDefinition,
"datalist" => &ListBoxRole as &dyn AriaRoleDefinition,
"del" => &DeletionRole as &dyn AriaRoleDefinition,
"dd" => &DefinitionRole as &dyn AriaRoleDefinition,
"dt" => &TermRole as &dyn AriaRoleDefinition,
"dfn" => &TermRole as &dyn AriaRoleDefinition,
"mark" => &MarkRole as &dyn AriaRoleDefinition,
"dialog" => &DialogRole as &dyn AriaRoleDefinition,
"em" => &EmphasisRole as &dyn AriaRoleDefinition,
"figure" => &FigureRole as &dyn AriaRoleDefinition,
"form" => &FormRole as &dyn AriaRoleDefinition,
"hr" => &SeparatorRole as &dyn AriaRoleDefinition,
"html" => &DocumentRole as &dyn AriaRoleDefinition,
"ins" => &InsertionRole as &dyn AriaRoleDefinition,
"main" => &MainRole as &dyn AriaRoleDefinition,
"marquee" => &MarqueeRole as &dyn AriaRoleDefinition,
"math" => &MathRole as &dyn AriaRoleDefinition,
"menu" => &ListRole as &dyn AriaRoleDefinition,
) -> Option<AriaRole> {
Some(match element {
"article" => AriaRole::Article,
"aside" => AriaRole::Complementary,
"blockquote" => AriaRole::Blockquote,
"button" => AriaRole::Button,
"caption" => AriaRole::Caption,
"code" => AriaRole::Code,
"datalist" => AriaRole::Listbox,
"del" => AriaRole::Deletion,
"dd" => AriaRole::Definition,
"dt" => AriaRole::Term,
"dfn" => AriaRole::Term,
"mark" => AriaRole::Mark,
"dialog" => AriaRole::Dialog,
"em" => AriaRole::Emphasis,
"figure" => AriaRole::Figure,
"form" => AriaRole::Form,
"hr" => AriaRole::Separator,
"html" => AriaRole::Document,
"ins" => AriaRole::Insertion,
"main" => AriaRole::Main,
"marquee" => AriaRole::Marquee,
"math" => AriaRole::Math,
"menu" => AriaRole::List,
"menuitem" => {
let type_values = attributes.get("type")?;
match type_values.first()?.as_str() {
"checkbox" => &MenuItemCheckboxRole as &dyn AriaRoleDefinition,
"radio" => &MenuItemRadioRole as &dyn AriaRoleDefinition,
_ => &MenuItemRole as &dyn AriaRoleDefinition,
"checkbox" => AriaRole::Menuitemcheckbox,
"radio" => AriaRole::Menuitemradio,
_ => AriaRole::Menuitem,
}
}
"meter" => &MeterRole as &dyn AriaRoleDefinition,
"nav" => &NavigationRole as &dyn AriaRoleDefinition,
"ul" | "ol" => &ListRole as &dyn AriaRoleDefinition,
"li" => &ListItemRole as &dyn AriaRoleDefinition,
"option" => &OptionRole as &dyn AriaRoleDefinition,
"optgroup" => &GroupRole as &dyn AriaRoleDefinition,
"output" => &StatusRole as &dyn AriaRoleDefinition,
"p" => &ParagraphRole as &dyn AriaRoleDefinition,
"progress" => &ProgressBarRole as &dyn AriaRoleDefinition,
"search" => &SearchRole as &dyn AriaRoleDefinition,
"strong" => &StrongRole as &dyn AriaRoleDefinition,
"sub" => &SubScriptRole as &dyn AriaRoleDefinition,
"sup" => &SuperScriptRole as &dyn AriaRoleDefinition,
"svg" => &GraphicsDocumentRole as &dyn AriaRoleDefinition,
"table" => &TableRole as &dyn AriaRoleDefinition,
"textarea" => &TextboxRole as &dyn AriaRoleDefinition,
"tr" => &RowRole as &dyn AriaRoleDefinition,
"meter" => AriaRole::Meter,
"nav" => AriaRole::Navigation,
"ul" | "ol" => AriaRole::List,
"li" => AriaRole::Listitem,
"option" => AriaRole::Option,
"optgroup" => AriaRole::Group,
"output" => AriaRole::Status,
"p" => AriaRole::Paragraph,
"progress" => AriaRole::Progressbar,
"search" => AriaRole::Search,
"strong" => AriaRole::Strong,
"sub" => AriaRole::Subscript,
"sup" => AriaRole::Superscript,
"table" => AriaRole::Table,
"textarea" => AriaRole::Textbox,
"tr" => AriaRole::Row,
// cell if a descendant of a <table> element,
// but this crate does not support checking a descendant of an element.
//
// ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/td
"td" => &CellRole as &dyn AriaRoleDefinition,
"td" => AriaRole::Cell,
// <th> element is able to be a rowheader, columnheader,
// but this crate does not support checking a descendant of an element.
//
// ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/th
"th" => &RowHeaderRole as &dyn AriaRoleDefinition,
"time" => &TimeRole as &dyn AriaRoleDefinition,
"address" | "details" | "fieldset" => &GroupRole as &dyn AriaRoleDefinition,
"h1" | "h2" | "h3" | "h4" | "h5" | "h6" => &HeadingRole as &dyn AriaRoleDefinition,
"tbody" | "tfoot" | "thead" => &RowGroupRole as &dyn AriaRoleDefinition,
"th" => AriaRole::Rowheader,
"time" => AriaRole::Time,
"address" | "details" | "fieldset" => AriaRole::Group,
"h1" | "h2" | "h3" | "h4" | "h5" | "h6" => AriaRole::Heading,
"tbody" | "tfoot" | "thead" => AriaRole::Rowgroup,
"input" => {
let type_values = attributes.get("type")?;
match type_values.first()?.as_str() {
"checkbox" => &CheckboxRole as &dyn AriaRoleDefinition,
"number" => &SpinButtonRole as &dyn AriaRoleDefinition,
"radio" => &RadioRole as &dyn AriaRoleDefinition,
"range" => &SliderRole as &dyn AriaRoleDefinition,
"button" | "image" | "reset" | "submit" => {
&ButtonRole as &dyn AriaRoleDefinition
}
"checkbox" => AriaRole::Checkbox,
"number" => AriaRole::Spinbutton,
"radio" => AriaRole::Radio,
"range" => AriaRole::Slider,
"button" | "image" | "reset" | "submit" => AriaRole::Button,
"search" => match attributes.get("list") {
Some(_) => &ComboBoxRole as &dyn AriaRoleDefinition,
_ => &SearchboxRole as &dyn AriaRoleDefinition,
Some(_) => AriaRole::Combobox,
_ => AriaRole::Searchbox,
},
"email" | "tel" | "url" => match attributes.get("list") {
Some(_) => &ComboBoxRole as &dyn AriaRoleDefinition,
_ => &TextboxRole as &dyn AriaRoleDefinition,
Some(_) => AriaRole::Combobox,
_ => AriaRole::Textbox,
},
"text" => &TextboxRole as &dyn AriaRoleDefinition,
_ => &TextboxRole as &dyn AriaRoleDefinition,
"text" => AriaRole::Textbox,
_ => AriaRole::Textbox,
}
}
"a" | "area" => match attributes.get("href") {
Some(_) => &LinkRole as &dyn AriaRoleDefinition,
_ => &GenericRole as &dyn AriaRoleDefinition,
"a" | "area" | "link" => match attributes.get("href") {
Some(_) => AriaRole::Link,
_ => AriaRole::Generic,
},
"img" => match attributes.get("alt") {
Some(values) => {
if values.iter().any(|x| !x.is_empty()) {
&ImgRole as &dyn AriaRoleDefinition
AriaRole::Img
} else {
&PresentationRole as &dyn AriaRoleDefinition
AriaRole::Presentation
}
}
None => &ImgRole as &dyn AriaRoleDefinition,
None => AriaRole::Img,
},
"section" => {
let has_accessible_name = attributes.get("aria-labelledby").is_some()
|| attributes.get("aria-label").is_some()
|| attributes.get("title").is_some();
if has_accessible_name {
&RegionRole as &dyn AriaRoleDefinition
AriaRole::Region
} else {
return None;
}
Expand All @@ -1174,24 +1171,22 @@ impl<'a> AriaRoles {
None => 0,
};
let multiple = attributes.get("multiple");

if multiple.is_none() && size <= 1 {
&ComboBoxRole as &dyn AriaRoleDefinition
AriaRole::Combobox
} else {
&ListBoxRole as &dyn AriaRoleDefinition
AriaRole::Listbox
}
}
"b" | "bdi" | "bdo" | "body" | "data" | "div" | "hgroup" | "i" | "q" | "samp"
| "small" | "span" | "u" | "pre" => &GenericRole as &dyn AriaRoleDefinition,
| "small" | "span" | "u" | "pre" => AriaRole::Generic,
"header" | "footer" => {
// This crate does not support checking a descendant of an element.
// header (maybe BannerRole): https://www.w3.org/WAI/ARIA/apg/patterns/landmarks/examples/banner.html
// footer (maybe ContentInfoRole): https://www.w3.org/WAI/ARIA/apg/patterns/landmarks/examples/contentinfo.html
&GenericRole as &dyn AriaRoleDefinition
AriaRole::Generic
}
_ => return None,
};
Some(result)
})
}

/// Given the name of element, the function tells whether it's interactive
Expand Down Expand Up @@ -1356,13 +1351,13 @@ impl<'a> AriaRoles {
&self,
element_name: &str,
attributes: &FxHashMap<String, Vec<String>>,
) -> Option<&'static dyn AriaRoleDefinition> {
) -> Option<AriaRole> {
attributes
.get("role")
.and_then(|role| role.first())
.map_or_else(
|| self.get_implicit_role(element_name, attributes),
|r| self.get_role(r),
|r| AriaRole::from_str(r).ok(),
)
}

Expand Down Expand Up @@ -1390,10 +1385,9 @@ impl<'a> AriaRoles {

let role_name = self.get_role_by_element_name(element_name, attributes);

match role_name.map(|role| role.type_name()) {
Some("biome_aria::roles::PresentationRole" | "biome_aria::roles::GenericRole") => false,
match role_name {
None | Some(AriaRole::Presentation | AriaRole::Generic) => false,
Some(_) => true,
None => false,
}
}
}
Expand Down Expand Up @@ -1426,6 +1420,7 @@ mod test {
use rustc_hash::FxHashMap;

use crate::AriaRoles;
use biome_aria_metadata::AriaRole;

#[test]
fn should_be_interactive() {
Expand Down Expand Up @@ -1463,23 +1458,20 @@ mod test {
let implicit_role = aria_roles
.get_implicit_role("button", &FxHashMap::default())
.unwrap();
assert_eq!(implicit_role.type_name(), "biome_aria::roles::ButtonRole");
assert_eq!(implicit_role, AriaRole::Button);

// <input type="search">
let mut attributes = FxHashMap::default();
attributes.insert("type".to_string(), vec!["search".to_string()]);
let implicit_role = aria_roles.get_implicit_role("input", &attributes).unwrap();
assert_eq!(
implicit_role.type_name(),
"biome_aria::roles::SearchboxRole"
);
assert_eq!(implicit_role, AriaRole::Searchbox);

// <select name="animals" multiple size="4">
let mut attributes = FxHashMap::default();
attributes.insert("name".to_string(), vec!["animals".to_string()]);
attributes.insert("multiple".to_string(), vec![String::new()]);
attributes.insert("size".to_string(), vec!["4".to_string()]);
let implicit_role = aria_roles.get_implicit_role("select", &attributes).unwrap();
assert_eq!(implicit_role.type_name(), "biome_aria::roles::ListBoxRole");
assert_eq!(implicit_role, AriaRole::Listbox);
}
}
2 changes: 1 addition & 1 deletion crates/biome_aria_metadata/aria-data.json
17 changes: 8 additions & 9 deletions crates/biome_js_analyze/src/lint/a11y/no_redundant_roles.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use std::str::FromStr;

use crate::{services::aria::Aria, JsRuleAction};
use biome_analyze::{
context::RuleContext, declare_lint_rule, FixKind, Rule, RuleDiagnostic, RuleSource,
};
use biome_aria::{roles::AriaRoleDefinition, AriaRoles};
use biome_aria_metadata::AriaRole;
use biome_console::markup;
use biome_js_syntax::{
jsx_ext::AnyJsxElement, AnyJsxAttributeValue, JsxAttribute, JsxAttributeList,
Expand Down Expand Up @@ -76,9 +78,9 @@ impl Rule for NoRedundantRoles {

let role_attribute = node.find_attribute_by_name("role")?;
let role_attribute_value = role_attribute.initializer()?.value().ok()?;
let explicit_role = get_explicit_role(aria_roles, &role_attribute_value)?;
let explicit_role = get_explicit_role(&role_attribute_value)?;

let is_redundant = implicit_role.type_name() == explicit_role.type_name();
let is_redundant = implicit_role == explicit_role;
if is_redundant {
return Some(RuleState {
redundant_attribute: role_attribute,
Expand Down Expand Up @@ -131,17 +133,14 @@ fn get_element_name_and_attributes(node: &AnyJsxElement) -> Option<(String, JsxA
}
}

fn get_explicit_role(
aria_roles: &AriaRoles,
role_attribute_value: &AnyJsxAttributeValue,
) -> Option<&'static dyn AriaRoleDefinition> {
fn get_explicit_role(role_attribute_value: &AnyJsxAttributeValue) -> Option<AriaRole> {
let static_value = role_attribute_value.as_static_value()?;

// If a role attribute has multiple values, the first valid value (specified role) will be used.
// Check: https://www.w3.org/TR/2014/REC-wai-aria-implementation-20140320/#mapping_role
let explicit_role = static_value
.text()
.split(' ')
.find_map(|role| aria_roles.get_role(role))?;
.split_ascii_whitespace()
.find_map(|value| AriaRole::from_str(value).ok())?;
Some(explicit_role)
}
Loading

0 comments on commit e3d9726

Please sign in to comment.