Skip to main content

eui_core/
a11y.rs

1//! Round 1 (docs/round-1-contract.md §6): the accessibility contract as data
2//! (`catalog/a11y.json`, embedded): per component its role and keyboard
3//! expectations, the rules every component keeps, and the commands a host
4//! may send a surface (`surface::SurfaceCommand`). Mirrors `src/a11y.ts`.
5
6use std::sync::LazyLock;
7
8use indexmap::IndexMap;
9use serde::Deserialize;
10
11use crate::generated::catalog as g;
12
13#[derive(Debug, Clone, PartialEq, Deserialize)]
14pub struct ComponentA11y {
15    /// The platform role it maps to (ARIA names).
16    pub role: String,
17    /// Keyboard expectations, one line per key group.
18    #[serde(default)]
19    pub keys: Vec<String>,
20}
21
22#[derive(Debug, Clone, PartialEq, Deserialize)]
23pub struct A11yCommand {
24    pub args: Vec<String>,
25    pub description: String,
26}
27
28#[derive(Debug, Clone, Deserialize)]
29pub struct A11yContract {
30    pub rules: Vec<String>,
31    pub commands: IndexMap<String, A11yCommand>,
32    pub components: IndexMap<String, ComponentA11y>,
33}
34
35pub static A11Y: LazyLock<A11yContract> = LazyLock::new(|| serde_json::from_str(g::A11Y_JSON).expect("a11y.json"));
36
37/// A component's entry, if it has one (extensions declare their own).
38pub fn component_a11y(name: &str) -> Option<&'static ComponentA11y> {
39    A11Y.components.get(name)
40}
41
42#[cfg(test)]
43mod tests {
44    use super::*;
45
46    #[test]
47    fn every_core_component_has_a_role_and_the_four_commands_exist() {
48        for name in g::COMPONENT_NAMES {
49            assert!(component_a11y(name).is_some_and(|a| !a.role.is_empty()), "{name}");
50        }
51        assert_eq!(A11Y.commands.keys().map(String::as_str).collect::<Vec<_>>(), ["focus", "announce", "scrollIntoView", "scrollToIndex"]);
52        assert!(!A11Y.rules.is_empty());
53    }
54}