Skip to main content

a11y_kit/
node.rs

1//! The semantic node model (a11y-kit, ADR-0004 §2): role, accessible name,
2//! states, and relations — platform-free, so it can be audited headlessly
3//! in CI and lowered into AccessKit (or anything else) at the edge.
4
5use core::fmt;
6
7/// A stable node identity within one tree.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
9pub struct NodeId(pub u32);
10
11impl fmt::Display for NodeId {
12    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13        write!(f, "#{}", self.0)
14    }
15}
16
17/// The semantic roles the estate's controls map onto. Deliberately a
18/// toolkit-neutral subset of the ARIA/accesskit role vocabulary — the
19/// lowering adapters pick the framework's spelling.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21#[non_exhaustive]
22pub enum A11yRole {
23    /// Top-level window.
24    Window,
25    /// Generic container.
26    Group,
27    /// Section heading.
28    Heading,
29    /// Static text.
30    Paragraph,
31    /// Push button.
32    Button,
33    /// Toggle / tri-state checkbox.
34    Checkbox,
35    /// Single-line text entry.
36    Textbox,
37    /// Multi-line text entry.
38    MultilineTextbox,
39    /// Numeric slider.
40    Slider,
41    /// Dropdown / select.
42    ComboBox,
43    /// List container.
44    List,
45    /// List item.
46    ListItem,
47    /// Tab strip.
48    TabList,
49    /// One tab.
50    Tab,
51    /// Data table.
52    Table,
53    /// Table row.
54    Row,
55    /// Table cell.
56    Cell,
57    /// Toolbar.
58    Toolbar,
59    /// Menu container.
60    Menu,
61    /// Menu entry.
62    MenuItem,
63    /// Status/alert region (live region).
64    StatusBar,
65    /// Dialog.
66    Dialog,
67    /// Blocking alert.
68    Alert,
69    /// Progress indicator.
70    ProgressBar,
71    /// Link.
72    Link,
73    /// Separator.
74    Separator,
75    /// Image with alternatives.
76    Image,
77    /// Canvas/surface region.
78    Canvas,
79}
80
81/// True for roles a keyboard user must be able to reach and operate —
82/// roles whose accessible name the audit requires.
83#[must_use]
84pub const fn is_interactive(role: A11yRole) -> bool {
85    matches!(
86        role,
87        A11yRole::Button
88            | A11yRole::Checkbox
89            | A11yRole::Textbox
90            | A11yRole::MultilineTextbox
91            | A11yRole::Slider
92            | A11yRole::ComboBox
93            | A11yRole::ListItem
94            | A11yRole::Tab
95            | A11yRole::MenuItem
96            | A11yRole::Link
97    )
98}
99
100/// Tri-state checkbox value.
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub enum TriState {
103    /// Not checked.
104    Unchecked,
105    /// Checked.
106    Checked,
107    /// Indeterminate ("mixed").
108    Mixed,
109}
110
111/// Node state flags.
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
113pub struct States {
114    /// Disabled.
115    pub disabled: bool,
116    /// Focused (at most one per tree — audited).
117    pub focused: bool,
118    /// Selected.
119    pub selected: bool,
120    /// Expanded (collapsible containers).
121    pub expanded: bool,
122    /// Required (form input).
123    pub required: bool,
124    /// Invalid (form input with an error).
125    pub invalid: bool,
126    /// Hidden from the a11y tree (still present for focus management).
127    pub hidden: bool,
128    /// Modal (blocks sibling interaction).
129    pub modal: bool,
130    /// Checkbox value.
131    pub checked: Option<TriState>,
132}
133
134/// A relation between nodes (the edges a screen reader traverses).
135#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136pub enum Relation {
137    /// This node controls the target (e.g. a toggle controls a panel).
138    Controls(NodeId),
139    /// The target describes this node (an error message).
140    DescribedBy(NodeId),
141    /// The target names this node.
142    LabelledBy(NodeId),
143    /// The focused control's current suggestion (combobox autocomplete).
144    ActiveDescendant(NodeId),
145}
146
147/// One semantic node.
148#[derive(Debug, Clone, PartialEq)]
149pub struct A11yNode {
150    /// Identity.
151    pub id: NodeId,
152    /// Semantic role.
153    pub role: A11yRole,
154    /// Accessible name (required for interactive roles).
155    pub name: String,
156    /// Longer description.
157    pub description: Option<String>,
158    /// State flags.
159    pub states: States,
160    /// Structural parent (None for a root).
161    pub parent: Option<NodeId>,
162    /// Non-structural relations.
163    pub relations: Vec<Relation>,
164}
165
166impl A11yNode {
167    /// A node with the common fields; states/relations default empty.
168    #[must_use]
169    pub fn new(id: NodeId, role: A11yRole, name: impl Into<String>) -> Self {
170        Self {
171            id,
172            role,
173            name: name.into(),
174            description: None,
175            states: States::default(),
176            parent: None,
177            relations: Vec::new(),
178        }
179    }
180
181    /// Attach a description.
182    pub fn with_description(mut self, description: impl Into<String>) -> Self {
183        self.description = Some(description.into());
184        self
185    }
186
187    /// Set states.
188    #[must_use]
189    pub fn with_states(mut self, states: States) -> Self {
190        self.states = states;
191        self
192    }
193
194    /// Set the structural parent.
195    #[must_use]
196    pub fn with_parent(mut self, parent: NodeId) -> Self {
197        self.parent = Some(parent);
198        self
199    }
200
201    /// Add a relation.
202    pub fn with_relation(mut self, relation: Relation) -> Self {
203        self.relations.push(relation);
204        self
205    }
206}