open_gpui/_accessibility.rs
1//! # Accessibility in GPUI
2//!
3//! "Accessibility" refers to the ability of your application to be used by all
4//! users, regardless of disability status. There are many aspects, all important, including:
5//! - Ensuring sufficient text contrast.
6//! - Providing a mechanism to disable animations.
7//! - Providing a mechanism to increase text sizes.
8//! - etc.
9//!
10//! This guide is focused on **programmatic accessibility**. This allows
11//! assistive technology, such as screen readers or Braille displays, to inspect
12//! and interact with your app.
13//!
14//! GPUI integrates with [AccessKit] to provide programmatic accessibility
15//! features (referred to as simply "accessibility" for the rest of this guide).
16//!
17//! A minimal example can be found in the `examples/a11y` directory.
18//!
19//! ## Background
20//!
21//! Accessibility support is based on two key capabilities:
22//! - Exposing information about the current UI state to assistive technology.
23//! - Responding to actions requested by assistive technology.
24//!
25//! For example, a screen reader might want to announce to the user that a new
26//! button has appeared. The user may then want to use a voice control program
27//! to press that button.
28//!
29//! ### IDs in GPUI - [`ElementId`] and [`GlobalElementId`]
30//!
31//! In GPUI, each [`Element`] can have an [`id`][Element::id]:
32//! ```rust
33//! # use open_gpui::*;
34//! let div_with_id = div().id("my-id").child(text!("hello"));
35//!
36//! // IDs are optional
37//! let div_without_id = div().child(text!("hello"));
38//! ```
39//!
40//! [`Element`]s with IDs are also assigned a [`GlobalElementId`]. This global
41//! ID is formed by composing all the non-`None` IDs of its ancestors. For
42//! example:
43//! ```rust
44//! # use open_gpui::*;
45//! let inner = div().id("inner-id");
46//! let middle = div().child(inner); // no ID
47//! let outer = div().id("outer-id").child(middle);
48//! ```
49//! In this example, `inner`s global ID is (roughly speaking) `["outer-id",
50//! "inner-id"]`.
51//!
52//! Since `middle` doesn't have an ID itself, it has no global ID.
53//!
54//! [`GlobalElementId`]s should be unique per-frame. Duplicate global IDs in the
55//! same frame will likely cause bugs.
56//!
57//! ### IDs and accessibility
58//!
59//! When GPUI renders a frame, it walks your UI tree, and finds nodes with
60//! global IDs, and informs assistive technology about this node.
61//!
62//! In order for nodes to be reported, they must also have a non-`None`
63//! [`role`][Element::a11y_role]. This is used to inform assistive technology
64//! what *sort* of node it is (button, label, table, etc.). You can use
65//! [`div().id(...).role()`][StatefulInteractiveElement::role] to set the role.
66//!
67//! Nodes with the same global ID *across frames* are considered to be "the
68//! same" node. For example:
69//! ```rust
70//! # use open_gpui::*;
71//! // The UI in frame 1
72//! let frame_1 = div()
73//! .id("parent")
74//! .role(Role::Button)
75//! .child(
76//! div()
77//! .id("id-1")
78//! .role(Role::Label)
79//! .child(text!("hello"))
80//! );
81//!
82//! // The UI on the next frame
83//! let frame_2 = div()
84//! .id("parent")
85//! .role(Role::Button)
86//! .child(
87//! div()
88//! .id("id-2") // <- different ID
89//! .role(Role::Label)
90//! .child(text!("hello"))
91//! );
92//! ```
93//! Logically, the UI has not changed. But the screen reader has no way of
94//! knowing that both child [`div`]s are "the same". So assistive technology
95//! will interpret this as one node being removed, and another node being added.
96//! This can be very disorienting for users, since announcements typically only
97//! happen when something has *meaningfully* changed.
98//!
99//! In other words, by controlling the ID of an element, you can control whether
100//! a change to a UI element is considered meaningful. You can also control
101//! whether elements are reported to assistive technology *at all* by setting
102//! the [`role`][Element::a11y_role], since nodes with no role are not reported.
103//!
104//! #### IDs and text
105//!
106//! Special care must be taken when dealing with text.
107//!
108//! GPUI provides the [`text!`] macro, which wraps strings in the [`Text`] type,
109//! but automatically derives an ID. Usually, this is what you want. However,
110//! the way it generates its ID is subtle and perhaps surprising.
111//!
112//! The ID of an invocation of the [`text!`] macro is derived from the
113//! **location in the source code of that invocation**. For example:
114//!
115//! ```rust
116//! # use open_gpui::*;
117//! let a = text!("a");
118//! let b = text!("b");
119//!
120//! // Different source locations, different IDs
121//! assert_ne!(a.id(), b.id());
122//!
123//! // However:
124//!
125//! fn make_text(s: &str) -> Text { text!(s) }
126//!
127//! let a = make_text("a");
128//! let b = make_text("b");
129//!
130//! // Both `a` and `b` are produced by the same `text!` invocation, so the IDs
131//! // are the same
132//! assert_eq!(a.id(), b.id());
133//! ```
134//! This can produce surprising behaviour. For example, this footgun:
135//! ```rust
136//! # use open_gpui::*;
137//! let todos = vec!["eat lunch", "drink water", "go to gym"];
138//! let todo_divs = todos.into_iter().map(|todo| {
139//! text!(todo)
140//! });
141//!
142//! div()
143//! .id("todo-list")
144//! .role(Role::Document)
145//! .children(todo_divs); // ERROR: multiple nodes with the same global ID
146//! ```
147//!
148//! Here, when we map the iterator, since we have only written [`text!`] once,
149//! there is only one ID. And since they have the same ancestors and the same
150//! ID, they will have the same global ID. In release builds, this will mean
151//! some nodes get silently dropped!
152//!
153//! To fix this, you can set an ID:
154//! ```rust
155//! # use open_gpui::*;
156//! let todos = vec!["eat lunch", "drink water", "go to gym"];
157//! let todo_divs = todos.into_iter().enumerate().map(|(index, todo)| {
158//! text!(todo).with_id(index) // OR `text(id = index, todo)`
159//! });
160//!
161//! div()
162//! .id("todo-list")
163//! .role(Role::Document)
164//! .children(todo_divs);
165//! ```
166//! Another possible solution is to wrap the [`text!`] in another node that
167//! *does* have a unique global ID. For example:
168//! ```rust
169//! # use open_gpui::*;
170//! let todos = vec!["eat lunch", "drink water", "go to gym"];
171//! let todo_divs = todos.into_iter().enumerate().map(|(index, todo)| {
172//! div().id(index).child(text!(todo))
173//! });
174//!
175//! div()
176//! .id("todo-list")
177//! .role(Role::Document)
178//! .children(todo_divs);
179//! ```
180//! Since the AccessKit [`NodeId`][accesskit::NodeId] is derived from the global
181//! ID, and the global ID takes into account the IDs of all ancestors, this
182//! works too.
183//!
184//! Occasionally, you will need to create a [`Text`] element with *no* ID. You
185//! can achieve this with [`Text::new_inaccessible`]. If you are creating a
186//! custom UI component (e.g. a button), you may want this so that you can set a
187//! label property on a parent [`div`] without duplicating the text in the
188//! accessibility tree.
189//!
190//! ### Handling actions
191//!
192//! Assistive technology can dispatch actions to the UI. While many users of
193//! assistive technology use traditional input devices (e.g. a keyboard), some
194//! use more specialized systems. For example, users with limited mobility may
195//! use voice control to interact with your app.
196//!
197//! When a user dispatches an action, it is dispatched *to a specific node*. It
198//! is your responsibility to tell the UI elements how they should respond when
199//! a request comes in.
200//!
201//! Note, these actions are **totally unrelated** to GPUI's [`Action`] trait.
202//! AccessKit exposes [`accesskit::Action`]. In GPUI, this is re-exported as
203//! [`AccessibleAction`].
204//!
205//! To respond to an accessible action, use
206//! [`div().on_a11y_action()`][InteractiveElement::on_a11y_action]:
207//! ```rust,ignore
208//! div()
209//! .id("my-slider")
210//! .role(Role::Slider)
211//! .on_a11y_action(AccessibleAction::Increment, |_extra, _window, _cx| {
212//! position += 1;
213//! cx.notify();
214//! })
215//! .child(my_cool_slider());
216//! ```
217//!
218//! Note that some common actions are automatically registered. For example,
219//! [`.on_click()`][StatefulInteractiveElement::on_click] adds an
220//! [`AccessibleAction::Click`] handler that calls the click handler.
221//!
222//! ## Further reading
223//!
224//! Designing high-quality accessible interfaces can be challenging, in the same
225//! way that designing high-quality traditional interfaces can be. The
226//! following pages have useful information:
227//!
228//! - [AccessKit]: The cross-platform accessibility toolkit GPUI uses
229//! internally.
230//! - [MDN WAI-ARIA basics][mdn-aria]: Introduction to roles, properties, and
231//! states.
232//! - [ARIA Authoring Practices Guide][apg]: W3C patterns for accessible
233//! widgets.
234//!
235//! Note that, while GPUI mimics web APIs, it doesn't necessarily behave
236//! *exactly* as a web browser would with the same attributes.
237//!
238//! [AccessKit]: https://accesskit.dev/
239//! [mdn-aria]: https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Accessibility/WAI-ARIA_basics
240//! [apg]: https://www.w3.org/WAI/ARIA/apg/
241
242#[cfg(doc)]
243use crate::*; // so I don't have to qualify every type :)