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}