use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct AriaAttributes {
pub role: Option<AriaRole>,
pub label: Option<String>,
pub labelledby: Option<String>,
pub describedby: Option<String>,
pub expanded: Option<bool>,
pub selected: Option<bool>,
pub checked: Option<TriState>,
pub disabled: Option<bool>,
pub required: Option<bool>,
pub invalid: Option<bool>,
pub live: Option<AriaLive>,
pub atomic: Option<bool>,
pub controls: Option<String>,
pub owns: Option<String>,
pub haspopup: Option<AriaHasPopup>,
pub level: Option<u8>,
pub orientation: Option<Orientation>,
pub readonly: Option<bool>,
pub multiselectable: Option<bool>,
pub valuemin: Option<f64>,
pub valuemax: Option<f64>,
pub valuenow: Option<f64>,
pub valuetext: Option<String>,
pub hidden: Option<bool>,
pub activedescendant: Option<String>,
pub busy: Option<bool>,
pub posinset: Option<u32>,
pub setsize: Option<u32>,
pub colcount: Option<u32>,
pub colindex: Option<u32>,
pub colspan: Option<u32>,
pub rowcount: Option<u32>,
pub rowindex: Option<u32>,
pub rowspan: Option<u32>,
pub sort: Option<AriaSort>,
pub autocomplete: Option<AriaAutocomplete>,
pub current: Option<AriaCurrent>,
pub errormessage: Option<String>,
pub keyshortcuts: Option<String>,
pub roledescription: Option<String>,
pub modal: Option<bool>,
pub placeholder: Option<String>,
}
impl AriaAttributes {
pub fn new() -> Self {
Self::default()
}
pub fn with_role(mut self, role: AriaRole) -> Self {
self.role = Some(role);
self
}
pub fn with_label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
pub fn with_labelledby(mut self, id: impl Into<String>) -> Self {
self.labelledby = Some(id.into());
self
}
pub fn with_describedby(mut self, id: impl Into<String>) -> Self {
self.describedby = Some(id.into());
self
}
pub fn with_expanded(mut self, expanded: bool) -> Self {
self.expanded = Some(expanded);
self
}
pub fn with_selected(mut self, selected: bool) -> Self {
self.selected = Some(selected);
self
}
pub fn with_checked(mut self, checked: TriState) -> Self {
self.checked = Some(checked);
self
}
pub fn with_disabled(mut self, disabled: bool) -> Self {
self.disabled = Some(disabled);
self
}
pub fn with_controls(mut self, id: impl Into<String>) -> Self {
self.controls = Some(id.into());
self
}
pub fn with_modal(mut self, modal: bool) -> Self {
self.modal = Some(modal);
self
}
pub fn with_haspopup(mut self, popup: AriaHasPopup) -> Self {
self.haspopup = Some(popup);
self
}
pub fn with_orientation(mut self, orientation: Orientation) -> Self {
self.orientation = Some(orientation);
self
}
pub fn to_attr_pairs(&self) -> Vec<(String, String)> {
let mut pairs = Vec::new();
if let Some(ref role) = self.role {
pairs.push(("role".to_string(), role.as_str().to_string()));
}
if let Some(ref label) = self.label {
pairs.push(("aria-label".to_string(), label.clone()));
}
if let Some(ref id) = self.labelledby {
pairs.push(("aria-labelledby".to_string(), id.clone()));
}
if let Some(ref id) = self.describedby {
pairs.push(("aria-describedby".to_string(), id.clone()));
}
if let Some(expanded) = self.expanded {
pairs.push(("aria-expanded".to_string(), expanded.to_string()));
}
if let Some(selected) = self.selected {
pairs.push(("aria-selected".to_string(), selected.to_string()));
}
if let Some(ref checked) = self.checked {
pairs.push(("aria-checked".to_string(), checked.as_str().to_string()));
}
if let Some(disabled) = self.disabled {
pairs.push(("aria-disabled".to_string(), disabled.to_string()));
}
if let Some(required) = self.required {
pairs.push(("aria-required".to_string(), required.to_string()));
}
if let Some(invalid) = self.invalid {
pairs.push(("aria-invalid".to_string(), invalid.to_string()));
}
if let Some(ref live) = self.live {
pairs.push(("aria-live".to_string(), live.as_str().to_string()));
}
if let Some(atomic) = self.atomic {
pairs.push(("aria-atomic".to_string(), atomic.to_string()));
}
if let Some(ref controls) = self.controls {
pairs.push(("aria-controls".to_string(), controls.clone()));
}
if let Some(ref owns) = self.owns {
pairs.push(("aria-owns".to_string(), owns.clone()));
}
if let Some(ref popup) = self.haspopup {
pairs.push(("aria-haspopup".to_string(), popup.as_str().to_string()));
}
if let Some(level) = self.level {
let clamped = level.clamp(1, 6);
pairs.push(("aria-level".to_string(), clamped.to_string()));
}
if let Some(ref orientation) = self.orientation {
pairs.push((
"aria-orientation".to_string(),
orientation.as_str().to_string(),
));
}
if let Some(readonly) = self.readonly {
pairs.push(("aria-readonly".to_string(), readonly.to_string()));
}
if let Some(multi) = self.multiselectable {
pairs.push(("aria-multiselectable".to_string(), multi.to_string()));
}
if let Some(min) = self.valuemin {
pairs.push(("aria-valuemin".to_string(), min.to_string()));
}
if let Some(max) = self.valuemax {
pairs.push(("aria-valuemax".to_string(), max.to_string()));
}
if let Some(now) = self.valuenow {
pairs.push(("aria-valuenow".to_string(), now.to_string()));
}
if let Some(ref text) = self.valuetext {
pairs.push(("aria-valuetext".to_string(), text.clone()));
}
if let Some(hidden) = self.hidden {
pairs.push(("aria-hidden".to_string(), hidden.to_string()));
}
if let Some(ref id) = self.activedescendant {
pairs.push(("aria-activedescendant".to_string(), id.clone()));
}
if let Some(busy) = self.busy {
pairs.push(("aria-busy".to_string(), busy.to_string()));
}
if let Some(pos) = self.posinset {
pairs.push(("aria-posinset".to_string(), pos.to_string()));
}
if let Some(size) = self.setsize {
pairs.push(("aria-setsize".to_string(), size.to_string()));
}
if let Some(modal) = self.modal {
pairs.push(("aria-modal".to_string(), modal.to_string()));
}
if let Some(col) = self.colcount {
pairs.push(("aria-colcount".to_string(), col.to_string()));
}
if let Some(col) = self.colindex {
pairs.push(("aria-colindex".to_string(), col.to_string()));
}
if let Some(col) = self.colspan {
pairs.push(("aria-colspan".to_string(), col.to_string()));
}
if let Some(row) = self.rowcount {
pairs.push(("aria-rowcount".to_string(), row.to_string()));
}
if let Some(row) = self.rowindex {
pairs.push(("aria-rowindex".to_string(), row.to_string()));
}
if let Some(row) = self.rowspan {
pairs.push(("aria-rowspan".to_string(), row.to_string()));
}
if let Some(ref sort) = self.sort {
pairs.push(("aria-sort".to_string(), sort.as_str().to_string()));
}
if let Some(ref ac) = self.autocomplete {
pairs.push(("aria-autocomplete".to_string(), ac.as_str().to_string()));
}
if let Some(ref cur) = self.current {
pairs.push(("aria-current".to_string(), cur.as_str().to_string()));
}
if let Some(ref err) = self.errormessage {
pairs.push(("aria-errormessage".to_string(), err.clone()));
}
if let Some(ref ks) = self.keyshortcuts {
pairs.push(("aria-keyshortcuts".to_string(), ks.clone()));
}
if let Some(ref rd) = self.roledescription {
pairs.push(("aria-roledescription".to_string(), rd.clone()));
}
if let Some(ref ph) = self.placeholder {
pairs.push(("aria-placeholder".to_string(), ph.clone()));
}
pairs
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum AriaRole {
Alert,
AlertDialog,
Button,
Checkbox,
Combobox,
Dialog,
Feed,
Grid,
GridCell,
Group,
Heading,
Img,
Link,
List,
ListItem,
ListBox,
Log,
Marquee,
Menu,
MenuBar,
MenuItem,
MenuItemCheckbox,
MenuItemRadio,
Navigation,
None,
Option,
Presentation,
ProgressBar,
Radio,
RadioGroup,
Region,
Row,
RowGroup,
RowHeader,
ScrollBar,
Search,
SearchBox,
Separator,
Slider,
SpinButton,
Status,
Switch,
Tab,
TabList,
TabPanel,
Table,
TextBox,
Timer,
ToolBar,
ToolTip,
Tree,
TreeGrid,
TreeItem,
ColumnHeader,
Cell,
Form,
Main,
Banner,
Complementary,
ContentInfo,
Definition,
Document,
Figure,
Note,
Term,
Application,
}
impl AriaRole {
pub fn as_str(&self) -> &'static str {
match self {
Self::Alert => "alert",
Self::AlertDialog => "alertdialog",
Self::Button => "button",
Self::Checkbox => "checkbox",
Self::Combobox => "combobox",
Self::Dialog => "dialog",
Self::Feed => "feed",
Self::Grid => "grid",
Self::GridCell => "gridcell",
Self::Group => "group",
Self::Heading => "heading",
Self::Img => "img",
Self::Link => "link",
Self::List => "list",
Self::ListItem => "listitem",
Self::ListBox => "listbox",
Self::Log => "log",
Self::Marquee => "marquee",
Self::Menu => "menu",
Self::MenuBar => "menubar",
Self::MenuItem => "menuitem",
Self::MenuItemCheckbox => "menuitemcheckbox",
Self::MenuItemRadio => "menuitemradio",
Self::Navigation => "navigation",
Self::None => "none",
Self::Option => "option",
Self::Presentation => "presentation",
Self::ProgressBar => "progressbar",
Self::Radio => "radio",
Self::RadioGroup => "radiogroup",
Self::Region => "region",
Self::Row => "row",
Self::RowGroup => "rowgroup",
Self::RowHeader => "rowheader",
Self::ScrollBar => "scrollbar",
Self::Search => "search",
Self::SearchBox => "searchbox",
Self::Separator => "separator",
Self::Slider => "slider",
Self::SpinButton => "spinbutton",
Self::Status => "status",
Self::Switch => "switch",
Self::Tab => "tab",
Self::TabList => "tablist",
Self::TabPanel => "tabpanel",
Self::Table => "table",
Self::TextBox => "textbox",
Self::Timer => "timer",
Self::ToolBar => "toolbar",
Self::ToolTip => "tooltip",
Self::Tree => "tree",
Self::TreeGrid => "treegrid",
Self::TreeItem => "treeitem",
Self::ColumnHeader => "columnheader",
Self::Cell => "cell",
Self::Form => "form",
Self::Main => "main",
Self::Banner => "banner",
Self::Complementary => "complementary",
Self::ContentInfo => "contentinfo",
Self::Definition => "definition",
Self::Document => "document",
Self::Figure => "figure",
Self::Note => "note",
Self::Term => "term",
Self::Application => "application",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum TriState {
True,
False,
Mixed,
}
impl TriState {
pub fn as_str(&self) -> &'static str {
match self {
Self::True => "true",
Self::False => "false",
Self::Mixed => "mixed",
}
}
pub fn is_checked(&self) -> bool {
matches!(self, Self::True)
}
pub fn toggle(&self) -> Self {
match self {
Self::True => Self::False,
Self::False => Self::True,
Self::Mixed => Self::True,
}
}
}
impl From<bool> for TriState {
fn from(value: bool) -> Self {
if value { Self::True } else { Self::False }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum AriaLive {
Off,
Polite,
Assertive,
}
impl AriaLive {
pub fn as_str(&self) -> &'static str {
match self {
Self::Off => "off",
Self::Polite => "polite",
Self::Assertive => "assertive",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum AriaHasPopup {
True,
Menu,
ListBox,
Tree,
Grid,
Dialog,
}
impl AriaHasPopup {
pub fn as_str(&self) -> &'static str {
match self {
Self::True => "true",
Self::Menu => "menu",
Self::ListBox => "listbox",
Self::Tree => "tree",
Self::Grid => "grid",
Self::Dialog => "dialog",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Orientation {
Horizontal,
Vertical,
}
impl Orientation {
pub fn as_str(&self) -> &'static str {
match self {
Self::Horizontal => "horizontal",
Self::Vertical => "vertical",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum AriaSort {
None,
Ascending,
Descending,
Other,
}
impl AriaSort {
pub fn as_str(&self) -> &'static str {
match self {
Self::None => "none",
Self::Ascending => "ascending",
Self::Descending => "descending",
Self::Other => "other",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum AriaAutocomplete {
None,
Inline,
List,
Both,
}
impl AriaAutocomplete {
pub fn as_str(&self) -> &'static str {
match self {
Self::None => "none",
Self::Inline => "inline",
Self::List => "list",
Self::Both => "both",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum AriaCurrent {
True,
Page,
Step,
Location,
Date,
Time,
}
impl AriaCurrent {
pub fn as_str(&self) -> &'static str {
match self {
Self::True => "true",
Self::Page => "page",
Self::Step => "step",
Self::Location => "location",
Self::Date => "date",
Self::Time => "time",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn aria_attributes_default_is_empty() {
let attrs = AriaAttributes::default();
assert!(attrs.role.is_none());
assert!(attrs.label.is_none());
assert_eq!(attrs.to_attr_pairs().len(), 0);
}
#[test]
fn aria_attributes_builder() {
let attrs = AriaAttributes::new()
.with_role(AriaRole::Button)
.with_label("Click me")
.with_disabled(false)
.with_expanded(true);
assert_eq!(attrs.role, Some(AriaRole::Button));
assert_eq!(attrs.label, Some("Click me".to_string()));
assert_eq!(attrs.disabled, Some(false));
assert_eq!(attrs.expanded, Some(true));
}
#[test]
fn aria_attributes_to_attr_pairs() {
let attrs = AriaAttributes::new()
.with_role(AriaRole::Button)
.with_label("Save")
.with_expanded(false);
let pairs = attrs.to_attr_pairs();
assert!(pairs.contains(&("role".to_string(), "button".to_string())));
assert!(pairs.contains(&("aria-label".to_string(), "Save".to_string())));
assert!(pairs.contains(&("aria-expanded".to_string(), "false".to_string())));
assert_eq!(pairs.len(), 3);
}
#[test]
fn aria_role_as_str() {
assert_eq!(AriaRole::Button.as_str(), "button");
assert_eq!(AriaRole::Dialog.as_str(), "dialog");
assert_eq!(AriaRole::TabList.as_str(), "tablist");
assert_eq!(AriaRole::TreeItem.as_str(), "treeitem");
assert_eq!(AriaRole::AlertDialog.as_str(), "alertdialog");
}
#[test]
fn tri_state_toggle() {
assert_eq!(TriState::False.toggle(), TriState::True);
assert_eq!(TriState::True.toggle(), TriState::False);
assert_eq!(TriState::Mixed.toggle(), TriState::True);
}
#[test]
fn tri_state_from_bool() {
assert_eq!(TriState::from(true), TriState::True);
assert_eq!(TriState::from(false), TriState::False);
}
#[test]
fn aria_live_as_str() {
assert_eq!(AriaLive::Polite.as_str(), "polite");
assert_eq!(AriaLive::Assertive.as_str(), "assertive");
assert_eq!(AriaLive::Off.as_str(), "off");
}
#[test]
fn orientation_as_str() {
assert_eq!(Orientation::Horizontal.as_str(), "horizontal");
assert_eq!(Orientation::Vertical.as_str(), "vertical");
}
#[test]
fn aria_attributes_serialization() {
let attrs = AriaAttributes::new()
.with_role(AriaRole::Checkbox)
.with_checked(TriState::Mixed);
let json = serde_json::to_string(&attrs).unwrap();
let deserialized: AriaAttributes = serde_json::from_str(&json).unwrap();
assert_eq!(attrs, deserialized);
}
#[test]
fn all_aria_roles_have_str() {
let roles = vec![
AriaRole::Alert,
AriaRole::AlertDialog,
AriaRole::Button,
AriaRole::Checkbox,
AriaRole::Combobox,
AriaRole::Dialog,
AriaRole::Grid,
AriaRole::Group,
AriaRole::Heading,
AriaRole::Link,
AriaRole::List,
AriaRole::ListBox,
AriaRole::Menu,
AriaRole::MenuBar,
AriaRole::MenuItem,
AriaRole::Navigation,
AriaRole::ProgressBar,
AriaRole::Radio,
AriaRole::RadioGroup,
AriaRole::Separator,
AriaRole::Slider,
AriaRole::SpinButton,
AriaRole::Status,
AriaRole::Switch,
AriaRole::Tab,
AriaRole::TabList,
AriaRole::TabPanel,
AriaRole::Table,
AriaRole::TextBox,
AriaRole::ToolTip,
AriaRole::Tree,
AriaRole::TreeItem,
];
for role in roles {
assert!(!role.as_str().is_empty());
}
}
}