Accessibility

Accessibility is a topic that usually focuses on assistive technologies like screen readers, keyboard navigation and high-contrast themes. However, working accessibility is a hard requirement of a proper user interface, not a feature.

Clutter and St have built-in support for accessibility, which means two things:

  1. It works by default

    You probably don't need to do anything to support accessibility, except basic testing before a release. Standard widgets like buttons and menus already use the correct attributes, and the focus order is usually as logical as the layout.

  2. If it doesn't work, there is a bug in your code

    Broken accessibility either means you have a design flaw, or a widget with incorrect roles, relationships or states. Usually you just need to set some properties, or make a few function calls.

This document will teach you the basics of implementing accessibility, with examples of the type of widgets that have built-in support.

Further Reading

Basic Concepts

The library used to set accessible attributes in GNOME Shell is Atkopen in new window, and St.Widgetopen in new window includes convenience methods and properties, with access to the Atk.Objectopen in new window for everything else.

Roles, relationships and states define the semantics of an element in the user interface. Roles like Atk.Role.RADIO_BUTTONopen in new window affect presentation, behavior and keyboard focus, but they also have a range of states dependent on their relationship with other elements. The semantics are the shared language of design, code, translations and user experience.

Roles

The Atk.Roleopen in new window is usually static, and represents the primary purpose of an element in the user interface. You can use the St.Widget:accessible-roleopen in new window property to check and set the proper role.

In addition to accessible relationships, the role may depend on another widget. For example, a menu item should have the role Atk.Role.CHECK_MENU_ITEM if it has a child with the role Atk.Role.CHECK_BUTTON.

Relationships

The Atk.RelationTypeopen in new window set an element has establishes meaningful links to other elements, like a label and the widget it describes. This is the most common relationship and handled automatically by the St.Widget:label-actoropen in new window property, which should usually be set to a widget with the role Atk.Role.LABELopen in new window like St.Labelopen in new window.

For other relationships, you can call the inherited method Clutter.Actor.get_accessible()open in new window to get the Atk.Objectopen in new window, then call Atk.Object.add_relationship()open in new window and Atk.Object.removed_relationship()open in new window as needed.

States

The Atk.StateTypeopen in new window determines the current state of an element, many of which are already handled by St. States can be added with St.Widget.add_accessible_state()open in new window and St.Widget.remove_accessible_state()open in new window respectively.

Common states like Atk.StateType.SENSITIVE and Atk.StateType.VISIBLE are handled by Clutter based on properties like Clutter.Actor:reactive and Clutter.Actor:visible. St.Widget:can-focus sets Atk.StateType.FOCUSABLE, while watching for the CSS pseudo-classes checked and selected to apply the Atk.StateType.CHECKED and Atk.StateType.SELECTED states.

Other more purposeful widgets also set the state, such as St.Button. It uses the St.Button:toggle-modeopen in new window property to change its role and updates the state by adding or removing the CSS pseudo-class checked when the St.Button:checkedopen in new window property changes.

Implementing Accessibility

The PopupMenu.PopupSwitchMenuItem class from GNOME Shell has examples of almost everything you'll need to do. It subclasses a generic widget to implement the role and state of a switch. It then subclasses the base menu item and ensures the role, relationships and state are updated to match the switch.

Basic Example

The accessible role of the switch is set to Atk.Role.CHECK_BUTTON, while the PopupMenu.Switch:state property updates the accessible state by adding and removing the checked pseudo-class (just like St.Button).

const Switch = GObject.registerClass({
    Properties: {
        'state': GObject.ParamSpec.boolean(
            'state', 'state', 'state',
            GObject.ParamFlags.READWRITE,
            false),
    },
}, class Switch extends St.Bin {
    _init(state) {
        this._state = false;

        super._init({
            style_class: 'toggle-switch',
            accessible_role: Atk.Role.CHECK_BOX,
            state,
        });
    }

    get state() {
        return this._state;
    }

    set state(state) {
        if (this._state === state)
            return;

        if (state)
            this.add_style_pseudo_class('checked');
        else
            this.remove_style_pseudo_class('checked');

        this._state = state;
        this.notify('state');
    }

    toggle() {
        this.state = !this.state;
    }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

The PopupMenu.PopupSwitchMenuItem is given the role Atk.Role.CHECK_MENU_ITEM by default, while the state is kept in sync with the PopupMenu.Switch. Also notice that St.Widget:label-actor is set on the menu item, so that the item's label is understood to describe it.

The menu item also handles the case where the switch is disabled. If PopupMenu.PopupSwitchMenuItem.setStatus() is called with a non-null value, the item will change roles to Atk.Role.MENU_ITEM, the Atk.StateType.CHECKED state is removed and the switch is replaced with a status label.

const PopupSwitchMenuItem = GObject.registerClass({
    Signals: {'toggled': {param_types: [GObject.TYPE_BOOLEAN]}},
}, class PopupSwitchMenuItem extends PopupBaseMenuItem {
    _init(text, active, params) {
        super._init(params);

        this.label = new St.Label({
            text,
            y_expand: true,
            y_align: Clutter.ActorAlign.CENTER,
        });
        this._switch = new Switch(active);

        this.accessible_role = Atk.Role.CHECK_MENU_ITEM;
        this.checkAccessibleState();
        this.label_actor = this.label;

        this.add_child(this.label);

        this._statusBin = new St.Bin({
            x_align: Clutter.ActorAlign.END,
            x_expand: true,
        });
        this.add_child(this._statusBin);

        this._statusLabel = new St.Label({
            text: '',
            style_class: 'popup-status-menu-item',
            y_expand: true,
            y_align: Clutter.ActorAlign.CENTER,
        });
        this._statusBin.child = this._switch;
    }

    setStatus(text) {
        if (text != null) {
            this._statusLabel.text = text;
            this._statusBin.child = this._statusLabel;
            this.reactive = false;
            this.accessible_role = Atk.Role.MENU_ITEM;
        } else {
            this._statusBin.child = this._switch;
            this.reactive = true;
            this.accessible_role = Atk.Role.CHECK_MENU_ITEM;
        }
        this.checkAccessibleState();
    }

    activate(event) {
        if (this._switch.mapped)
            this.toggle();

        // we allow pressing space to toggle the switch
        // without closing the menu
        if (event.type() === Clutter.EventType.KEY_PRESS &&
            event.get_key_symbol() === Clutter.KEY_space)
            return;

        super.activate(event);
    }

    toggle() {
        this._switch.toggle();
        this.emit('toggled', this._switch.state);
        this.checkAccessibleState();
    }

    get state() {
        return this._switch.state;
    }

    setToggleState(state) {
        this._switch.state = state;
        this.checkAccessibleState();
    }

    checkAccessibleState() {
        switch (this.accessible_role) {
        case Atk.Role.CHECK_MENU_ITEM:
            if (this._switch.state)
                this.add_accessible_state(Atk.StateType.CHECKED);
            else
                this.remove_accessible_state(Atk.StateType.CHECKED);
            break;
        default:
            this.remove_accessible_state(Atk.StateType.CHECKED);
        }
    }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
Last Updated: 8/23/2023, 7:08:54 PM
Contributors: Andy Holmes, Andy Holmes