Skip to main content

ravenrs_app_a11y/
names.rs

1//! What screen readers call the fields and the actions of an application, from the model.
2//!
3//! The caption is the name, unless the model says otherwise. A field says it with
4//! [`FieldInfo::accessible_name`] and [`FieldInfo::accessible_description`], which the model
5//! copies to the item of its form as `AccessibleName` and `AccessibleDescription`. There a
6//! translation or an administrator changes them, as they do the caption. An action says it with
7//! [`Action::accessible_name`] and [`Action::accessible_description`].
8
9use ravenrs_app::{Action, FieldInfo, ModelNode};
10
11/// The name and the description screen readers get for a field or an action.
12#[derive(Clone, Debug, PartialEq, Eq)]
13#[non_exhaustive]
14pub struct AccessibleText {
15    /// What it is called.
16    pub name: String,
17    /// What is said after the name. With none, the control keeps what it has, such as the hint
18    /// of a field.
19    pub description: Option<String>,
20    /// The name is not the caption on the screen.
21    pub renamed: bool,
22}
23
24impl AccessibleText {
25    /// For a field whose form item is `item` and which the screen calls `caption`. The model's
26    /// item comes first, then the declaration, then the caption.
27    pub fn of_field(field: &FieldInfo, item: Option<&ModelNode>, caption: &str) -> Self {
28        let said = |name: &str, declared: &Option<String>| {
29            item.and_then(|item| item.text(name))
30                .map(str::to_owned)
31                .or_else(|| declared.clone())
32                .filter(|text| !text.trim().is_empty())
33        };
34        let named = said("AccessibleName", &field.accessible_name);
35        Self {
36            renamed: named.as_deref().is_some_and(|name| name != caption),
37            name: named.unwrap_or_else(|| caption.to_owned()),
38            description: said("AccessibleDescription", &field.accessible_description),
39        }
40    }
41
42    /// For an action: its own words, or its caption.
43    pub fn of_action(action: &Action) -> Self {
44        let named = action.accessible_name_text().filter(|text| !text.trim().is_empty());
45        Self {
46            renamed: named.is_some_and(|name| name != action.caption()),
47            name: named.unwrap_or(action.caption()).to_owned(),
48            description: action.accessible_description_text().map(str::to_owned),
49        }
50    }
51
52    /// Whether the control has to be told anything besides what it shows.
53    pub fn says_more(&self) -> bool {
54        self.renamed || self.description.is_some()
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61    use ravenrs_app::FieldKind;
62
63    #[test]
64    fn the_caption_is_the_name_unless_the_model_says_otherwise() {
65        let field = FieldInfo::new("until", FieldKind::Date).caption("Até");
66        let plain = AccessibleText::of_field(&field, None, "Até");
67        assert_eq!((plain.name.as_str(), plain.renamed, plain.says_more()), ("Até", false, false));
68
69        let field = field.accessible_name("Fim do período").accessible_description("O último dia");
70        let declared = AccessibleText::of_field(&field, None, "Até");
71        assert_eq!(declared.name, "Fim do período");
72        assert_eq!(declared.description.as_deref(), Some("O último dia"));
73        assert!(declared.renamed);
74
75        let item = ModelNode::new("until").with("AccessibleName", "End of the period");
76        let translated = AccessibleText::of_field(&field, Some(&item), "Until");
77        assert_eq!(translated.name, "End of the period", "the model's item comes first");
78    }
79
80    #[test]
81    fn an_action_is_its_caption_unless_it_says_otherwise() {
82        let next = Action::simple("Next", "›");
83        assert_eq!(AccessibleText::of_action(&next).name, "›");
84        let next =
85            next.accessible_name("Próximo registro").accessible_description("Abre o seguinte da lista");
86        let text = AccessibleText::of_action(&next);
87        assert_eq!(text.name, "Próximo registro");
88        assert!(text.renamed && text.says_more());
89    }
90}