use super::AccessibleNode;
use std::collections::HashMap;
#[derive(Clone, Debug, PartialEq)]
pub enum AriaAttribute {
Role(String),
Label(String),
LabelledBy(String),
DescribedBy(String),
Live(String),
Atomic(bool),
Hidden(bool),
Expanded(Option<bool>),
Checked(Option<bool>),
Selected(bool),
Pressed(Option<bool>),
Disabled(bool),
ReadOnly(bool),
Required(bool),
Invalid(bool),
Modal(bool),
Current(Option<String>),
HasPopup(Option<String>),
Orientation(String),
ValueMin(f64),
ValueMax(f64),
ValueNow(f64),
ValueText(String),
PosInSet(usize),
SetSize(usize),
Level(usize),
ErrorMessage(String),
TabIndex(i16),
Alt(String),
Custom(String, String),
}
impl AriaAttribute {
pub fn name(&self) -> &str {
match self {
AriaAttribute::Role(_) => "role",
AriaAttribute::Label(_) => "aria-label",
AriaAttribute::LabelledBy(_) => "aria-labelledby",
AriaAttribute::DescribedBy(_) => "aria-describedby",
AriaAttribute::Live(_) => "aria-live",
AriaAttribute::Atomic(_) => "aria-atomic",
AriaAttribute::Hidden(_) => "aria-hidden",
AriaAttribute::Expanded(_) => "aria-expanded",
AriaAttribute::Checked(_) => "aria-checked",
AriaAttribute::Selected(_) => "aria-selected",
AriaAttribute::Pressed(_) => "aria-pressed",
AriaAttribute::Disabled(_) => "aria-disabled",
AriaAttribute::ReadOnly(_) => "aria-readonly",
AriaAttribute::Required(_) => "aria-required",
AriaAttribute::Invalid(_) => "aria-invalid",
AriaAttribute::Modal(_) => "aria-modal",
AriaAttribute::Current(_) => "aria-current",
AriaAttribute::HasPopup(_) => "aria-haspopup",
AriaAttribute::Orientation(_) => "aria-orientation",
AriaAttribute::ValueMin(_) => "aria-valuemin",
AriaAttribute::ValueMax(_) => "aria-valuemax",
AriaAttribute::ValueNow(_) => "aria-valuenow",
AriaAttribute::ValueText(_) => "aria-valuetext",
AriaAttribute::PosInSet(_) => "aria-posinset",
AriaAttribute::SetSize(_) => "aria-setsize",
AriaAttribute::Level(_) => "aria-level",
AriaAttribute::ErrorMessage(_) => "aria-errormessage",
AriaAttribute::TabIndex(_) => "tabindex",
AriaAttribute::Alt(_) => "alt",
AriaAttribute::Custom(name, _) => name,
}
}
pub fn value(&self) -> String {
match self {
AriaAttribute::Role(v) => v.clone(),
AriaAttribute::Label(v) => v.clone(),
AriaAttribute::LabelledBy(v) => v.clone(),
AriaAttribute::DescribedBy(v) => v.clone(),
AriaAttribute::Live(v) => v.clone(),
AriaAttribute::Atomic(v) => v.to_string(),
AriaAttribute::Hidden(v) => v.to_string(),
AriaAttribute::Expanded(Some(v)) => v.to_string(),
AriaAttribute::Expanded(None) => "undefined".to_string(),
AriaAttribute::Checked(Some(v)) => v.to_string(),
AriaAttribute::Checked(None) => "mixed".to_string(),
AriaAttribute::Selected(v) => v.to_string(),
AriaAttribute::Pressed(Some(v)) => v.to_string(),
AriaAttribute::Pressed(None) => "mixed".to_string(),
AriaAttribute::Disabled(v) => v.to_string(),
AriaAttribute::ReadOnly(v) => v.to_string(),
AriaAttribute::Required(v) => v.to_string(),
AriaAttribute::Invalid(v) => v.to_string(),
AriaAttribute::Modal(v) => v.to_string(),
AriaAttribute::Current(Some(v)) => v.clone(),
AriaAttribute::Current(None) => "false".to_string(),
AriaAttribute::HasPopup(Some(v)) => v.clone(),
AriaAttribute::HasPopup(None) => "false".to_string(),
AriaAttribute::Orientation(v) => v.clone(),
AriaAttribute::ValueMin(v) => v.to_string(),
AriaAttribute::ValueMax(v) => v.to_string(),
AriaAttribute::ValueNow(v) => v.to_string(),
AriaAttribute::ValueText(v) => v.clone(),
AriaAttribute::PosInSet(v) => v.to_string(),
AriaAttribute::SetSize(v) => v.to_string(),
AriaAttribute::Level(v) => v.to_string(),
AriaAttribute::ErrorMessage(v) => v.clone(),
AriaAttribute::TabIndex(v) => v.to_string(),
AriaAttribute::Alt(v) => v.clone(),
AriaAttribute::Custom(_, v) => v.clone(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum LiveRegion {
Off,
#[default]
Polite,
Assertive,
}
impl LiveRegion {
pub fn as_str(&self) -> &'static str {
match self {
LiveRegion::Off => "off",
LiveRegion::Polite => "polite",
LiveRegion::Assertive => "assertive",
}
}
}
pub struct AriaBuilder {
attributes: Vec<AriaAttribute>,
live_region: Option<LiveRegion>,
atomic: bool,
}
impl Default for AriaBuilder {
fn default() -> Self {
Self::new()
}
}
impl AriaBuilder {
pub fn new() -> Self {
Self {
attributes: Vec::new(),
live_region: None,
atomic: false,
}
}
pub fn from_node(mut self, node: &AccessibleNode) -> Self {
self.attributes
.push(AriaAttribute::Role(node.role.name().to_string()));
if let Some(label) = &node.label {
self.attributes.push(AriaAttribute::Label(label.clone()));
}
if let Some(desc) = &node.description {
self.attributes
.push(AriaAttribute::DescribedBy(desc.clone()));
}
let state = &node.state;
if state.disabled {
self.attributes.push(AriaAttribute::Disabled(true));
}
if state.hidden {
self.attributes.push(AriaAttribute::Hidden(true));
}
if let Some(expanded) = state.expanded {
self.attributes
.push(AriaAttribute::Expanded(Some(expanded)));
}
if let Some(checked) = state.checked {
self.attributes.push(AriaAttribute::Checked(Some(checked)));
}
if state.selected {
self.attributes.push(AriaAttribute::Selected(true));
}
if let Some(pressed) = state.pressed {
self.attributes.push(AriaAttribute::Pressed(Some(pressed)));
}
if let Some(now) = state.value_now {
self.attributes.push(AriaAttribute::ValueNow(now));
}
if let Some(min) = state.value_min {
self.attributes.push(AriaAttribute::ValueMin(min));
}
if let Some(max) = state.value_max {
self.attributes.push(AriaAttribute::ValueMax(max));
}
if let Some(text) = &state.value_text {
self.attributes.push(AriaAttribute::ValueText(text.clone()));
}
if let Some(pos) = state.pos_in_set {
self.attributes.push(AriaAttribute::PosInSet(pos));
}
if let Some(size) = state.set_size {
self.attributes.push(AriaAttribute::SetSize(size));
}
if let Some(level) = state.level {
self.attributes.push(AriaAttribute::Level(level));
}
if let Some(err) = &state.error_message {
self.attributes
.push(AriaAttribute::ErrorMessage(err.clone()));
}
if node.is_focusable() {
self.attributes.push(AriaAttribute::TabIndex(0));
} else if !node.role.is_interactive() {
self.attributes.push(AriaAttribute::TabIndex(-1));
}
for (key, value) in &node.properties {
if key.starts_with("aria-") {
self.attributes
.push(AriaAttribute::Custom(key.clone(), value.clone()));
}
}
self
}
pub fn live_region(mut self, live: LiveRegion) -> Self {
self.live_region = Some(live);
self.attributes
.push(AriaAttribute::Live(live.as_str().to_string()));
self
}
pub fn atomic(mut self, atomic: bool) -> Self {
self.atomic = atomic;
self.attributes.push(AriaAttribute::Atomic(atomic));
self
}
pub fn labelled_by(mut self, id: impl Into<String>) -> Self {
self.attributes.push(AriaAttribute::LabelledBy(id.into()));
self
}
pub fn described_by(mut self, id: impl Into<String>) -> Self {
self.attributes.push(AriaAttribute::DescribedBy(id.into()));
self
}
pub fn modal(mut self, modal: bool) -> Self {
self.attributes.push(AriaAttribute::Modal(modal));
self
}
pub fn has_popup(mut self, has: bool, popup_type: Option<&str>) -> Self {
let value = if has {
Some(popup_type.unwrap_or("true").to_string())
} else {
None
};
self.attributes.push(AriaAttribute::HasPopup(value));
self
}
pub fn orientation(mut self, orientation: &str) -> Self {
self.attributes
.push(AriaAttribute::Orientation(orientation.to_string()));
self
}
pub fn build(self) -> HashMap<String, String> {
let mut map = HashMap::new();
for attr in self.attributes {
map.insert(attr.name().to_string(), attr.value());
}
map
}
pub fn build_pairs(self) -> Vec<(String, String)> {
self.attributes
.into_iter()
.map(|attr| (attr.name().to_string(), attr.value()))
.collect()
}
pub fn get_live_region(&self) -> Option<LiveRegion> {
self.live_region
}
pub fn is_atomic(&self) -> bool {
self.atomic
}
}
pub fn aria_attributes(node: &AccessibleNode) -> HashMap<String, String> {
AriaBuilder::new().from_node(node).build()
}
pub fn aria_pairs(node: &AccessibleNode) -> Vec<(String, String)> {
AriaBuilder::new().from_node(node).build_pairs()
}