bevyengine / bevyengine/bevy

InteractionDisabled and accessibility

Open
#25,111 1 comment 1 reaction 0 assignees View on GitHub
A-UI C-Refinement D-Complex S-Ready-For-Implementation X-Blessed
Dominant language
Rust
Stars
48.2k
Forks
4.8k
Avg merge
3d 22h
Merged PRs (30d)
161

Description

This is a write-up of a long, and somewhat contentious, discussion on discord about `InteractionDisabled`.

_Warning_: I'm going to try and present both sides of this argument, but because I have strong opinions on this topic, I can't guarantee that my writeup is unbiased, or that I'm not putting words in other people's mouths.

## The TL;DR

The key issue is: what effects should `InteractionDisabled` have when placed on a widget?
* Should it prevent the widget from updating its editable content? (Certainly yes)
* Should it prevent the widget from being focused when clicked?
* Should it remove the widget from the tab navigation sequence?
* Should it prevent the contents of the widget from being copied to the clipboard?
* Should it prevent selection and cursor movement?
* Should it change the widget's appearance (graying out, changing the color of the selection box, etc.)?
* Should it cause the screen reader to announce that the item is disabled?

Some of these questions are linked: for example, if selection and cursor movement is disabled, then there's no way that the contents can be copied to the clipboard (unlike a web browser, Bevy provides no means of selecting arbitrary text on the page, so clipboard support only works with the widget's explicit cooperation - a widget that is entirely frozen cannot do this).

Similarly, the "click-to-focus" and "tab-navigation" go together: if an entity is focusable by tabbing, then there's a strong argument to be made that it should also be focusable by clicking, and would be surprising if this were not true.

## Focus and accessibility

A goal of the Bevy is to support accessibility, which is why we have a dependency on `accesskit`, and why the `bevy_a11y` crate exists. However, merely depending on those crates does nothing unless UI code is written to follow accessibility guidelines. Because many game developers aren't familiar with a11y technologies, the goal is to try and make it as easy as possible for developers - to make a11y the "path of least resistance".

A key principle in making apps accessible is to make them drivable by keyboard shortcuts. Sightless users cannot use a mouse or trackpad; users with palsy or other motor-control issues have difficulty as well. These users are able to learn to use the keyboard and, in conjunction with screen readers, are able to develop the muscle memory to use these apps efficiently - but only if the apps provide good a11y support.

Of course, many games require vision, but not all do; moreover, not every app built on Bevy is a game. Also, the number of users who are completely blind is a fraction of the number of users who have impaired vision (cataracts and so on).

Using a complex app via the keyboard generally means supporting some kind of navigation: either tab navigation, directional navigation, or object-based navigation (where you move up, down, or laterally in the hierarchy). Tab and directional navigation both use input focus as the targeting mechanism.

In a11y literature, there is this notion of _discoverability_: how does a screen reader user know that a particular button or menu item exists? The only way to learn the app's UI is by navigating through the possible input controls and listen for the narration. If the navigation framework skips over a widget, then from the perception of the screen reader user, that widget does not exist. That may be OK: every user knows that apps support "cut, copy, paste", and if the paste menu is missing for some reason (maybe because the clipboard is empty), their a priori knowledge tells them that it exists in potential. They know what to do in order to make it appear: copy something. But there could easily be other menu items and buttons whose enabling conditions are more obscure - if they were able to read the caption, they might be able to guess at how to enable it, but if it's skipped entirely then they will never know it's there, and they will be missing out on valuable functionality.

## Contradictory Standards

If you actually sit down and read the W3C guidelines on disabled widgets, you'll notice a curious ambiguity, which is that it tries to represent both the "aspirational" guidelines - that is, how we would like apps to behave in an ideal world - and the more "realistic" guidelines - that is, what the vast majority of UI frameworks actually implement.

The aspirational approach is that "disabled" items should still be navigable: they should still be in the tab order. Tabbing over a disabled menu item causes the reader to narrate, "(name of item), menu item, disabled".

However, this is not what a lot of UI toolkits do: setting the "disabled" flag causes the screen reader to skip over the item entirely. This is not true of all toolkits, but it is true of some of the more popular ones.

The justification for this is that this is the behavior of the HTML `disabled` attribute: the element is removed from tab order. However, the `disabled` attribute is very old, and comes from an era where the a11y guidelines were not completely well thought out. More recently, the `aria-disabled` attribute was added, which is purely informational: it causes the screen reader to announce that the element is disabled, but has no other effect. If the user wants other effects, they have to be implemented in JavaScript. There is also the `readonly` attribute, which only applies to `` fields, that makes them focusable but not editable - you can still select, move the cursor, copy to clipboard, but you cannot change the content.

In addition, some of these UI frameworks (particularly on the native side, like Java Swing or Qt) support alternative forms of navigation, such as "object nav", which allows navigation to disabled elements even when they are not in the tab order.

So, one counter-argument to the idea that disabled elements should be focusable is that it's swimming against the tide: that we should offer to devs a toolkit that behaves in ways that are familiar to them, especially if they have experience on the web with React or Svelte.

## Bevy's aspiration

Bevy takes a lot of ideas from the web, but Bevy is not the web: because Bevy is a clean slate design, and not compelled to adopt ancient legacy decisions, we can improve on what the web offers.

In particular, one thing that Bevy aspires to support is "diagetic" UIs: an interface made up of clickable or navigable 2d and 3d objects. Bevy's input focus system is explicitly designed to support this.

## Input focus dependency inversion

However, this creates a problem for users of `InteractionDisabled`, which is that this component lives in `bevy_ui` which is a layer above `bevy_input_focus`. So the focus system can't know that an entity is disabled without creating a dependency inversion. This means that the global "click-to-focus" observer will focus a disabled entity. The only way to solve this is to move the marker component to a lower-level crate.

Another way to make elements unfocusable is by removing the `TabIndex` component. However, this is destructive, because `TabIndex` contains a number which is the tab order; you'd need to remember this value when re-enabling the entity. If we had split `TabIndex` into two separate components, one which enables tabbing, and the other which holds the tab order, this would be less of a problem. Again, this is an area where we made the mistake of following the web too closely.

Similarly, we could create a `NotTabbable` component that inhibits tabbing, but it seems strange to add a component (`TabIndex`) and then add another component (`NotTabbable`) to undo the effects of the first.

## The meaning of "interaction"

As the person who wrote the initial implementation of `InteractionDisabled`, I will be the first to admit that this was badly named: what I was intending was what many people refer to as "grayed out", that is, a widget that is in a readonly state, where it is inoperable and uneditable, but can still be focused, browsed, selected, and copied to the clipboard. Basically, what I wanted is something more like the HTML "readonly" attribute, but universally applied to all widgets. And this is how the Bevy widgets behave currently.

There's a semantic argument as to whether things like "selection" or "copy to clipboard" count as "interactions". While I could argue that they are not, I also think that this argument is a waste of time - I would much rather focus on "what behaviors do we actually need?". If it turns out that the behaviors we need are misnamed, then we can work to change the name. What we shouldn't do, I think, is change the behaviors to match the name if the name is wrong.

(You can take this argument to ridiculous extremes, such as whether a slider should update the thumb position if the slider value is changed programmatically. The right answer, I think, is to avoid the problematic word "interaction" entirely, because it's not well-defined.)

A related argument has to do with the mechanics of implementation: in a previous version of `bevy_ui_widgets`, both the "click to focus" and "update the value" functions were handled by the same observer. However, these are separate features, and could have just as easily been implemented as separate observers. Regardless, where the code is located should have no bearing on whether the "disabled" state should have sway over what the code does - the only thing that matters is the user's perceptions.

So on the "what we need" question, I think there's clear evidence that "readonly" is an important state to support. But what about the more maximalist position, corresponding to the HTML "disabled" attribute? One in which focus, selection, and everything else is prevented?

For myself, I have a hard time seeing the use case for this - for widgets that are completely frozen, static. However, I'm willing to listen to counter-arguments, and will support it if people really want it. But I think that the "readonly" state is the more important one.

In practice, "readonly" widgets don't appear in game HUDs very much. Because screen real-estate is at a premium, a UI is more likely to hide a widget entirely than gray it out. Its main use is in editors, and in options pages.

@alice-i-cecile @ickshonpe @kfc35 @gagnus

Contributor guide

Open the contributing guide

Research direction

Start by reading InteractionDisabled and TabIndex in bevy_ui and bevy_ui_widgets, then trace the click-to-focus behavior in bevy_input_focus and the accessibility integration through bevy_a11y and accesskit. The issue first needs agreement on whether the state is readonly or fully disabled, including focus, tab navigation, selection, copying, appearance, and screen-reader behavior. Done means the semantics and implementation scope are decided before any code changes begin.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
accessibility
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.