244 lines
8.9 KiB
Rust
244 lines
8.9 KiB
Rust
//! # Accessibility in GPUI
|
|
//!
|
|
//! "Accessibility" refers to the ability of your application to be used by all
|
|
//! users, regardless of disability status. There are many aspects, all important, including:
|
|
//! - Ensuring sufficient text contrast.
|
|
//! - Providing a mechanism to disable animations.
|
|
//! - Providing a mechanism to increase text sizes.
|
|
//! - etc.
|
|
//!
|
|
//! This guide is focused on **programmatic accessibility**. This allows
|
|
//! assistive technology, such as screen readers or Braille displays, to inspect
|
|
//! and interact with your app.
|
|
//!
|
|
//! GPUI integrates with [AccessKit] to provide programmatic accessibility
|
|
//! features (referred to as simply "accessibility" for the rest of this guide).
|
|
//!
|
|
//! A minimal example can be found in the `examples/a11y` directory.
|
|
//!
|
|
//! ## Background
|
|
//!
|
|
//! Accessibility support is based on two key capabilities:
|
|
//! - Exposing information about the current UI state to assistive technology.
|
|
//! - Responding to actions requested by assistive technology.
|
|
//!
|
|
//! For example, a screen reader might want to announce to the user that a new
|
|
//! button has appeared. The user may then want to use a voice control program
|
|
//! to press that button.
|
|
//!
|
|
//! ### IDs in GPUI - [`ElementId`] and [`GlobalElementId`]
|
|
//!
|
|
//! In GPUI, each [`Element`] can have an [`id`][Element::id]:
|
|
//! ```rust
|
|
//! # use gpui::*;
|
|
//! let div_with_id = div().id("my-id").child(text!("hello"));
|
|
//!
|
|
//! // IDs are optional
|
|
//! let div_without_id = div().child(text!("hello"));
|
|
//! ```
|
|
//!
|
|
//! [`Element`]s with IDs are also assigned a [`GlobalElementId`]. This global
|
|
//! ID is formed by composing all the non-`None` IDs of its ancestors. For
|
|
//! example:
|
|
//! ```rust
|
|
//! # use gpui::*;
|
|
//! let inner = div().id("inner-id");
|
|
//! let middle = div().child(inner); // no ID
|
|
//! let outer = div().id("outer-id").child(middle);
|
|
//! ```
|
|
//! In this example, `inner`s global ID is (roughly speaking) `["outer-id",
|
|
//! "inner-id"]`.
|
|
//!
|
|
//! Since `middle` doesn't have an ID itself, it has no global ID.
|
|
//!
|
|
//! [`GlobalElementId`]s should be unique per-frame. Duplicate global IDs in the
|
|
//! same frame will likely cause bugs.
|
|
//!
|
|
//! ### IDs and accessibility
|
|
//!
|
|
//! When GPUI renders a frame, it walks your UI tree, and finds nodes with
|
|
//! global IDs, and informs assistive technology about this node.
|
|
//!
|
|
//! In order for nodes to be reported, they must also have a non-`None`
|
|
//! [`role`][Element::a11y_role]. This is used to inform assistive technology
|
|
//! what *sort* of node it is (button, label, table, etc.). You can use
|
|
//! [`div().id(...).role()`][StatefulInteractiveElement::role] to set the role.
|
|
//!
|
|
//! Nodes with the same global ID *across frames* are considered to be "the
|
|
//! same" node. For example:
|
|
//! ```rust
|
|
//! # use gpui::*;
|
|
//! // The UI in frame 1
|
|
//! let frame_1 = div()
|
|
//! .id("parent")
|
|
//! .role(Role::Button)
|
|
//! .child(
|
|
//! div()
|
|
//! .id("id-1")
|
|
//! .role(Role::Label)
|
|
//! .child(text!("hello"))
|
|
//! );
|
|
//!
|
|
//! // The UI on the next frame
|
|
//! let frame_2 = div()
|
|
//! .id("parent")
|
|
//! .role(Role::Button)
|
|
//! .child(
|
|
//! div()
|
|
//! .id("id-2") // <- different ID
|
|
//! .role(Role::Label)
|
|
//! .child(text!("hello"))
|
|
//! );
|
|
//! ```
|
|
//! Logically, the UI has not changed. But the screen reader has no way of
|
|
//! knowing that both child [`div`]s are "the same". So assistive technology
|
|
//! will interpret this as one node being removed, and another node being added.
|
|
//! This can be very disorienting for users, since announcements typically only
|
|
//! happen when something has *meaningfully* changed.
|
|
//!
|
|
//! In other words, by controlling the ID of an element, you can control whether
|
|
//! a change to a UI element is considered meaningful. You can also control
|
|
//! whether elements are reported to assistive technology *at all* by setting
|
|
//! the [`role`][Element::a11y_role], since nodes with no role are not reported.
|
|
//!
|
|
//! #### IDs and text
|
|
//!
|
|
//! Special care must be taken when dealing with text.
|
|
//!
|
|
//! GPUI provides the [`text!`] macro, which wraps strings in the [`Text`] type,
|
|
//! but automatically derives an ID. Usually, this is what you want. However,
|
|
//! the way it generates its ID is subtle and perhaps surprising.
|
|
//!
|
|
//! The ID of an invocation of the [`text!`] macro is derived from the
|
|
//! **location in the source code of that invocation**. For example:
|
|
//!
|
|
//! ```rust
|
|
//! # use gpui::*;
|
|
//! let a = text!("a");
|
|
//! let b = text!("b");
|
|
//!
|
|
//! // Different source locations, different IDs
|
|
//! assert_ne!(a.id(), b.id());
|
|
//!
|
|
//! // However:
|
|
//!
|
|
//! fn make_text(s: &str) -> Text { text!(s) }
|
|
//!
|
|
//! let a = make_text("a");
|
|
//! let b = make_text("b");
|
|
//!
|
|
//! // Both `a` and `b` are produced by the same `text!` invocation, so the IDs
|
|
//! // are the same
|
|
//! assert_eq!(a.id(), b.id());
|
|
//! ```
|
|
//! This can produce surprising behaviour. For example, this footgun:
|
|
//! ```rust
|
|
//! # use gpui::*;
|
|
//! let todos = vec!["eat lunch", "drink water", "go to gym"];
|
|
//! let todo_divs = todos.into_iter().map(|todo| {
|
|
//! text!(todo)
|
|
//! });
|
|
//!
|
|
//! div()
|
|
//! .id("todo-list")
|
|
//! .role(Role::Document)
|
|
//! .children(todo_divs); // ERROR: multiple nodes with the same global ID
|
|
//! ```
|
|
//!
|
|
//! Here, when we map the iterator, since we have only written [`text!`] once,
|
|
//! there is only one ID. And since they have the same ancestors and the same
|
|
//! ID, they will have the same global ID. In release builds, this will mean
|
|
//! some nodes get silently dropped!
|
|
//!
|
|
//! To fix this, you can set an ID:
|
|
//! ```rust
|
|
//! # use gpui::*;
|
|
//! let todos = vec!["eat lunch", "drink water", "go to gym"];
|
|
//! let todo_divs = todos.into_iter().enumerate().map(|(index, todo)| {
|
|
//! text!(todo).with_id(index) // OR `text(id = index, todo)`
|
|
//! });
|
|
//!
|
|
//! div()
|
|
//! .id("todo-list")
|
|
//! .role(Role::Document)
|
|
//! .children(todo_divs);
|
|
//! ```
|
|
//! Another possible solution is to wrap the [`text!`] in another node that
|
|
//! *does* have a unique global ID. For example:
|
|
//! ```rust
|
|
//! # use gpui::*;
|
|
//! let todos = vec!["eat lunch", "drink water", "go to gym"];
|
|
//! let todo_divs = todos.into_iter().enumerate().map(|(index, todo)| {
|
|
//! div().id(index).child(text!(todo))
|
|
//! });
|
|
//!
|
|
//! div()
|
|
//! .id("todo-list")
|
|
//! .role(Role::Document)
|
|
//! .children(todo_divs);
|
|
//! ```
|
|
//! Since the AccessKit [`NodeId`][accesskit::NodeId] is derived from the global
|
|
//! ID, and the global ID takes into account the IDs of all ancestors, this
|
|
//! works too.
|
|
//!
|
|
//! Occasionally, you will need to create a [`Text`] element with *no* ID. You
|
|
//! can achieve this with [`Text::new_inaccessible`]. If you are creating a
|
|
//! custom UI component (e.g. a button), you may want this so that you can set a
|
|
//! label property on a parent [`div`] without duplicating the text in the
|
|
//! accessibility tree.
|
|
//!
|
|
//! ### Handling actions
|
|
//!
|
|
//! Assistive technology can dispatch actions to the UI. While many users of
|
|
//! assistive technology use traditional input devices (e.g. a keyboard), some
|
|
//! use more specialized systems. For example, users with limited mobility may
|
|
//! use voice control to interact with your app.
|
|
//!
|
|
//! When a user dispatches an action, it is dispatched *to a specific node*. It
|
|
//! is your responsibility to tell the UI elements how they should respond when
|
|
//! a request comes in.
|
|
//!
|
|
//! Note, these actions are **totally unrelated** to GPUI's [`Action`] trait.
|
|
//! AccessKit exposes [`accesskit::Action`]. In GPUI, this is re-exported as
|
|
//! [`AccessibleAction`].
|
|
//!
|
|
//! To respond to an accessible action, use
|
|
//! [`div().on_a11y_action()`][InteractiveElement::on_a11y_action]:
|
|
//! ```rust,ignore
|
|
//! div()
|
|
//! .id("my-slider")
|
|
//! .role(Role::Slider)
|
|
//! .on_a11y_action(AccessibleAction::Increment, |_extra, _window, _cx| {
|
|
//! position += 1;
|
|
//! cx.notify();
|
|
//! })
|
|
//! .child(my_cool_slider());
|
|
//! ```
|
|
//!
|
|
//! Note that some common actions are automatically registered. For example,
|
|
//! [`.on_click()`][StatefulInteractiveElement::on_click] adds an
|
|
//! [`AccessibleAction::Click`] handler that calls the click handler.
|
|
//!
|
|
//! ## Further reading
|
|
//!
|
|
//! Designing high-quality accessible interfaces can be challenging, in the same
|
|
//! way that designing high-quality traditional interfaces can be. The
|
|
//! following pages have useful information:
|
|
//!
|
|
//! - [AccessKit]: The cross-platform accessibility toolkit GPUI uses
|
|
//! internally.
|
|
//! - [MDN WAI-ARIA basics][mdn-aria]: Introduction to roles, properties, and
|
|
//! states.
|
|
//! - [ARIA Authoring Practices Guide][apg]: W3C patterns for accessible
|
|
//! widgets.
|
|
//!
|
|
//! Note that, while GPUI mimics web APIs, it doesn't necessarily behave
|
|
//! *exactly* as a web browser would with the same attributes.
|
|
//!
|
|
//! [AccessKit]: https://accesskit.dev/
|
|
//! [mdn-aria]: https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Accessibility/WAI-ARIA_basics
|
|
//! [apg]: https://www.w3.org/WAI/ARIA/apg/
|
|
|
|
#[cfg(doc)]
|
|
use crate::*; // so I don't have to qualify every type :)
|