Skip to content

Commit 5057b03

Browse files
committed
docs: add comprehensive accessibility plan
Research-based plan for making the node editor accessible to keyboard-only and screen reader users. Covers 5 tiers from basic node navigation to dynamic announcements, identifies Slint extension needs (live regions, accessible relationships), and references React Flow as prior art.
1 parent 3b1a8f9 commit 5057b03

1 file changed

Lines changed: 310 additions & 0 deletions

File tree

ACCESSIBILITY.md

Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
# Accessibility Plan for slint-node-editor
2+
3+
## Context
4+
5+
Node editors are notoriously inaccessible. The library currently has basic
6+
accessible roles (list/list-item/button) but no keyboard navigation, no
7+
link accessibility, and no dynamic announcements. This plan addresses
8+
accessibility systematically, informed by WAI-ARIA best practices, React
9+
Flow's implementation (the best-in-class web reference), and Slint's
10+
accessibility facilities.
11+
12+
The goal: a blind or keyboard-only user should be able to navigate nodes,
13+
understand connections, select and delete elements, and create links —
14+
all without a mouse.
15+
16+
## Current state
17+
18+
| Component | Role | Keyboard | Screen reader |
19+
|-----------|------|----------|---------------|
20+
| NodeEditor | list | FocusScope (no key bindings) | "Node Editor" |
21+
| BaseNode | list-item | Not focusable | "Node {id}", "selected" |
22+
| Pin | button | Not focusable | "input/output pin" |
23+
| Link | (none) | N/A | Invisible |
24+
| Minimap | (none) | N/A | Invisible |
25+
26+
**Gaps:** No keyboard navigation to nodes/pins. Links invisible to assistive
27+
tech. No announcements for state changes. No keyboard link creation.
28+
29+
## Slint facilities available
30+
31+
**Available (23 properties):**
32+
- `accessible-role` (19 roles including list, list-item, button)
33+
- `accessible-label`, `accessible-description`, `accessible-value`
34+
- `accessible-item-selected`, `accessible-item-selectable`
35+
- `accessible-item-index`, `accessible-item-count`
36+
- `accessible-checked`, `accessible-checkable`
37+
- `accessible-expanded`, `accessible-expandable`
38+
- `accessible-action-default`, `accessible-action-increment/decrement`
39+
- `accessible-action-set-value(String)`, `accessible-action-expand`
40+
- `accessible-delegate-focus` (parent delegates focus to child by index)
41+
- `accessible-enabled`, `accessible-read-only`
42+
43+
**NOT available (would need Slint extensions):**
44+
- Live regions (aria-live) — no way to announce dynamic changes
45+
- Accessible relationships (aria-flowto, aria-controls) — no way to
46+
express "node A connects to node B"
47+
- Custom focus order — only delegate-focus by child index
48+
49+
## Tiered implementation plan
50+
51+
### Tier 1: Structural keyboard navigation (library)
52+
53+
**Goal:** Nodes are focusable, navigable via keyboard, with visible focus indicators.
54+
55+
**1a. Make BaseNode focusable**
56+
57+
BaseNode needs to participate in the focus chain. Two approaches:
58+
- BaseNode inherits FocusScope (changes inheritance from Rectangle)
59+
- Add a FocusScope child inside BaseNode
60+
61+
Recommendation: Add a FocusScope child (preserves Rectangle inheritance, avoids
62+
breaking existing code). The FocusScope covers the full node area and handles
63+
key-pressed for node-level navigation.
64+
65+
**1b. Add focused-node tracking to NodeEditor**
66+
67+
NodeEditor needs:
68+
```slint
69+
// Internal state
70+
property <int> focused-node-index: -1; // -1 = no node focused
71+
property <int> node-count: 0; // set by application
72+
73+
// Delegate focus to the focused node
74+
accessible-delegate-focus: focused-node-index;
75+
```
76+
77+
Tab/Shift+Tab in NodeEditor's key-pressed handler cycles `focused-node-index`
78+
through `0..node-count-1`. This is the same pattern Slint's TabWidget uses.
79+
80+
**1c. Visual focus indicator on BaseNode**
81+
82+
When a node has focus, render a visible focus ring:
83+
```slint
84+
// In BaseNode, a Rectangle child that shows when focused
85+
Rectangle {
86+
visible: root.has-focus; // or a focused property
87+
border-width: 2px;
88+
border-color: #4a9eff;
89+
border-radius: root.border-radius-base + 2px;
90+
// Slightly larger than node to create ring effect
91+
}
92+
```
93+
94+
**1d. Accessible item properties on BaseNode**
95+
96+
```slint
97+
accessible-item-selectable: true;
98+
accessible-item-selected: root.selected;
99+
accessible-item-index: ???; // needs to be passed from repeater
100+
```
101+
102+
The `item-index` is tricky — the repeater index isn't automatically available
103+
to the child component. Options:
104+
- Add an `in property <int> node-index` to BaseNode, set by the repeater
105+
- Use `node-id` as the index (but it's not sequential)
106+
- Compute from the model
107+
108+
Recommendation: Add `in property <int> node-index: -1` to BaseNode. Examples
109+
set it from the repeater: `node-index: index` (Slint repeaters expose `index`).
110+
111+
**Files:** `node-editor-building-blocks.slint` (BaseNode), `node-editor.slint` (NodeEditor)
112+
113+
---
114+
115+
### Tier 2: Node keyboard operations (library provides, embedder binds)
116+
117+
**Goal:** Focused nodes can be selected, moved, and deleted via keyboard.
118+
119+
The library already separates focus (structural) from keymaps (policy). For
120+
accessibility, the library should provide *functions* that embedders bind:
121+
122+
**2a. New public functions on NodeEditor:**
123+
124+
```slint
125+
/// Move focus to the next/previous node
126+
public function focus-next-node();
127+
public function focus-previous-node();
128+
129+
/// Select the currently focused node
130+
public function select-focused-node(shift-held: bool);
131+
132+
/// Move focused/selected node(s) by delta
133+
public function nudge-selection(delta-x: length, delta-y: length);
134+
```
135+
136+
**2b. Recommended keymap (documented, embedder implements):**
137+
138+
| Key | Action |
139+
|-----|--------|
140+
| Tab | Focus next node |
141+
| Shift+Tab | Focus previous node |
142+
| Enter/Space | Select focused node |
143+
| Shift+Enter | Add focused node to selection |
144+
| Arrow keys | Nudge selected node(s) by grid step |
145+
| Delete | Delete selected (already exists) |
146+
| Escape | Cancel link creation / deselect |
147+
148+
The library documents this as the recommended pattern. The advanced example
149+
demonstrates it.
150+
151+
**Files:** `node-editor.slint` (functions), advanced example (keymap demo)
152+
153+
---
154+
155+
### Tier 3: Pin navigation and keyboard link creation (library)
156+
157+
**Goal:** Pins are keyboard-navigable and links can be created without a mouse.
158+
159+
**3a. Pin focus within a node**
160+
161+
When a node has focus, a second level of navigation allows cycling through
162+
its pins. This could be:
163+
- Arrow Left/Right to cycle pins within the focused node
164+
- Or a sub-focus mode entered with Enter, exited with Escape
165+
166+
Recommendation: When a node is focused, Left/Right arrow keys cycle through
167+
pins. The focused pin is highlighted. This stays within the library since
168+
it's structural navigation.
169+
170+
**3b. Keyboard link creation**
171+
172+
The flow:
173+
1. Focus a pin (via node focus + arrow key)
174+
2. Press Enter to start a connection from that pin
175+
3. NodeEditor enters "keyboard link creation" mode
176+
4. Tab navigates through compatible target pins (filtered)
177+
5. Enter completes the connection; Escape cancels
178+
179+
This requires:
180+
- A `keyboard-link-source-pin: int` property
181+
- A `is-keyboard-linking: bool` state
182+
- Modified Tab behavior during keyboard linking
183+
- A way to enumerate and filter compatible pins
184+
185+
**3c. Pin accessible properties**
186+
187+
```slint
188+
// In Pin component
189+
accessible-description: /* "connected to Node B" or "not connected" */;
190+
accessible-action-default => { /* start/complete link creation */ }
191+
```
192+
193+
**Files:** `node-editor-building-blocks.slint` (Pin focus), `node-editor.slint` (link creation mode)
194+
195+
---
196+
197+
### Tier 4: Connection/Link accessibility
198+
199+
**Goal:** Links are discoverable by assistive technology.
200+
201+
**4a. Link accessible metadata**
202+
203+
Links are currently Path elements with no accessibility. Options:
204+
- Add accessible-role to Link (but Path elements may not support it well)
205+
- Maintain a separate accessible-only model of connections
206+
- Describe connections in node/pin descriptions instead
207+
208+
Recommendation: Describe connections in pin descriptions since Slint doesn't
209+
support accessible relationships. Each Pin's `accessible-description` would
210+
include its connection targets:
211+
212+
```
213+
"output pin, connected to Node B input"
214+
"input pin, connected from Node A output"
215+
```
216+
217+
This requires the library to track connections and generate descriptions.
218+
Currently connections are opaque to the Slint side (only Rust knows pin
219+
topology). A new global or callback would expose connection descriptions.
220+
221+
**4b. Node connection summary**
222+
223+
BaseNode's `accessible-description` could include a connection summary:
224+
```
225+
"selected, 2 connections: output to Node B, output to Node C"
226+
```
227+
228+
This also requires Rust-side connection topology to be exposed to Slint.
229+
230+
**Files:** `node-editor-building-blocks.slint`, new global or callback for
231+
connection descriptions, Rust helpers for generating descriptions
232+
233+
---
234+
235+
### Tier 5: Dynamic announcements (needs Slint extension)
236+
237+
**Goal:** State changes are announced to screen readers.
238+
239+
**Problem:** Slint has no live region support. There's no way to make a
240+
screen reader announce "Node A selected" or "Connection created from A to B"
241+
without the user navigating to the element.
242+
243+
**Workarounds within current Slint:**
244+
- Use `accessible-value` on NodeEditor to hold a "last action" string that
245+
changes on every operation. Screen readers that watch value changes would
246+
pick this up. But this is unreliable.
247+
- Use a hidden Text element with changing text — screen readers may announce
248+
text changes on focused elements.
249+
250+
**Slint extension needed:**
251+
- `accessible-live: "polite" | "assertive"` on elements whose text changes
252+
should be announced without focus
253+
- Or a `accessible-announce(message: string)` callback/function
254+
255+
Recommendation: File a Slint feature request for live region support. In the
256+
meantime, implement the `accessible-value` workaround on NodeEditor to hold
257+
a status string. This won't work on all platforms but is better than nothing.
258+
259+
---
260+
261+
## Implementation order
262+
263+
| Phase | Scope | Effort | Impact |
264+
|-------|-------|--------|--------|
265+
| **Tier 1** | Node focus + navigation | Medium | High — unblocks all keyboard use |
266+
| **Tier 2** | Node operations via keyboard | Small | High — basic editing without mouse |
267+
| **Tier 3** | Pin navigation + keyboard linking | Large | Medium — advanced operation |
268+
| **Tier 4** | Connection descriptions | Medium | Medium — information for screen readers |
269+
| **Tier 5** | Announcements | Small (workaround) | Low until Slint adds live regions |
270+
271+
Recommend implementing Tiers 1-2 first as they provide the most value.
272+
Tier 3 is the most complex but essential for full keyboard operability.
273+
Tier 4 provides information richness. Tier 5 depends on Slint extending
274+
its accessibility facilities.
275+
276+
## Files to modify
277+
278+
| File | Changes |
279+
|------|---------|
280+
| `node-editor-building-blocks.slint` | BaseNode: focus support, focus ring, item properties. Pin: description with connections, focus within node |
281+
| `node-editor.slint` | NodeEditor: focus tracking, Tab navigation functions, keyboard link creation mode, accessible-value for announcements |
282+
| `src/lib.rs` or new module | Helpers for generating connection descriptions |
283+
| `examples/advanced/ui/ui.slint` | Demonstrate recommended accessible keymap |
284+
| `AGENTS.md` / `README.md` | Document accessibility features and recommended patterns |
285+
286+
## Slint extension requests
287+
288+
1. **Live regions**`accessible-live: "polite"` for announcing state changes
289+
2. **Accessible relationships**`accessible-flowto` or equivalent for expressing
290+
node connections in the accessibility tree
291+
3. **Repeater index exposure** — make repeater index available as a property for
292+
`accessible-item-index` without requiring manual plumbing
293+
294+
## Verification
295+
296+
- Screen reader testing (VoiceOver on macOS): navigate nodes, read labels,
297+
verify selection announcements
298+
- Keyboard-only testing: Tab through nodes, select, delete, create links
299+
- MCP introspection: verify accessible tree structure, labels, descriptions
300+
- Automated: test accessible properties in level8 test file
301+
302+
## Reference implementations
303+
304+
- **React Flow**: Best-in-class web node editor accessibility
305+
- Tab navigation, Enter/Space select, Arrow key move
306+
- aria-live announcements
307+
- Semantic ARIA roles
308+
- https://reactflow.dev/learn/advanced-use/accessibility
309+
- **Slint TabWidget**: Pattern for accessible-delegate-focus with child FocusScopes
310+
- Source: `slint-ui/slint/internal/compiler/widgets/*/tabwidget.slint`

0 commit comments

Comments
 (0)