From 7c7dabae8c4ee5655fe0c9b12b318675b41ac11f Mon Sep 17 00:00:00 2001 From: Emmanuele Bassi Date: Fri, 16 Oct 2020 17:03:50 +0100 Subject: [PATCH 1/6] a11y: Rework accessible name/description computation The ARIA spec determines the name and description of accessible elements in a more complex way that simply mapping to a single property; instead, it will chain up multiple definitions (if it finds them). For instance, let's assume we have a button that saves a file selected from a file selection widget; the widgets have the following attributes: - the file selection widget has a "label" attribute set to the selected file, e.g. "Final paper.pdf" - the "download" button has a "label" attribute set to the "Download" string - the "download" button has a "labelled-by" attribute set to reference the file selection widget The ARIA spec says that the accessible name of the "Download" button should be computed as "Download Final paper.pdf". The algorithm defined in section 4.3 of the WAI-ARIA specification applies to both accessible names (using the "label" and "labelled-by" attributes), and to accessible descriptions (using the "description" and "described-by" attributes). --- gtk/a11y/gtkatspicontext.c | 18 ++- gtk/gtkatcontext.c | 220 ++++++++++++++++++++++++++++++++----- gtk/gtkatcontextprivate.h | 3 +- 3 files changed, 208 insertions(+), 33 deletions(-) diff --git a/gtk/a11y/gtkatspicontext.c b/gtk/a11y/gtkatspicontext.c index 4c3746ba24..1a2434ff7c 100644 --- a/gtk/a11y/gtkatspicontext.c +++ b/gtk/a11y/gtkatspicontext.c @@ -606,7 +606,14 @@ handle_accessible_get_property (GDBusConnection *connection, } else if (g_strcmp0 (property_name, "Description") == 0) { - char *label = gtk_at_context_get_label (GTK_AT_CONTEXT (self)); + char *label = gtk_at_context_get_description (GTK_AT_CONTEXT (self)); + + if (label == NULL || *label == '\0') + { + g_free (label); + label = gtk_at_context_get_name (GTK_AT_CONTEXT (self)); + } + res = g_variant_new_string (label); g_free (label); } @@ -932,7 +939,14 @@ gtk_at_spi_context_state_change (GtkATContext *ctx, if (changed_properties & GTK_ACCESSIBLE_PROPERTY_CHANGE_LABEL) { - char *label = gtk_at_context_get_label (GTK_AT_CONTEXT (self)); + char *label = gtk_at_context_get_name (GTK_AT_CONTEXT (self)); + GVariant *v = g_variant_new_take_string (label); + emit_property_changed (self, "accessible-name", v); + } + + if (changed_properties & GTK_ACCESSIBLE_PROPERTY_CHANGE_DESCRIPTION) + { + char *label = gtk_at_context_get_description (GTK_AT_CONTEXT (self)); GVariant *v = g_variant_new_take_string (label); emit_property_changed (self, "accessible-description", v); } diff --git a/gtk/gtkatcontext.c b/gtk/gtkatcontext.c index de9dc79275..7bd1398ebb 100644 --- a/gtk/gtkatcontext.c +++ b/gtk/gtkatcontext.c @@ -730,47 +730,34 @@ gtk_at_context_get_accessible_relation (GtkATContext *self, return gtk_accessible_attribute_set_get_value (self->relations, relation); } -/*< private > - * gtk_at_context_get_label: - * @self: a #GtkATContext - * - * Retrieves the accessible label of the #GtkATContext. - * - * This is a convenience function meant to be used by #GtkATContext implementations. - * - * Returns: (transfer full): the label of the #GtkATContext - */ -char * -gtk_at_context_get_label (GtkATContext *self) +/* See the WAI-ARIA ยง 4.3, "Accessible Name and Description Computation" */ +static void +gtk_at_context_get_name_accumulate (GtkATContext *self, + GPtrArray *names, + gboolean recurse) { - g_return_val_if_fail (GTK_IS_AT_CONTEXT (self), NULL); - GtkAccessibleValue *value = NULL; - if (gtk_accessible_attribute_set_contains (self->states, GTK_ACCESSIBLE_STATE_HIDDEN)) - { - value = gtk_accessible_attribute_set_get_value (self->states, GTK_ACCESSIBLE_STATE_HIDDEN); - - if (gtk_boolean_accessible_value_get (value)) - return g_strdup (""); - } - if (gtk_accessible_attribute_set_contains (self->properties, GTK_ACCESSIBLE_PROPERTY_LABEL)) { value = gtk_accessible_attribute_set_get_value (self->properties, GTK_ACCESSIBLE_PROPERTY_LABEL); - return g_strdup (gtk_string_accessible_value_get (value)); + g_ptr_array_add (names, (char *) gtk_string_accessible_value_get (value)); } - if (gtk_accessible_attribute_set_contains (self->relations, GTK_ACCESSIBLE_RELATION_LABELLED_BY)) + if (recurse && gtk_accessible_attribute_set_contains (self->relations, GTK_ACCESSIBLE_RELATION_LABELLED_BY)) { value = gtk_accessible_attribute_set_get_value (self->relations, GTK_ACCESSIBLE_RELATION_LABELLED_BY); GList *list = gtk_reference_list_accessible_value_get (value); - GtkAccessible *rel = GTK_ACCESSIBLE (list->data); - GtkATContext *rel_context = gtk_accessible_get_at_context (rel); - return gtk_at_context_get_label (rel_context); + for (GList *l = list; l != NULL; l = l->data) + { + GtkAccessible *rel = GTK_ACCESSIBLE (l->data); + GtkATContext *rel_context = gtk_accessible_get_at_context (rel); + + gtk_at_context_get_name_accumulate (rel_context, names, FALSE); + } } GtkAccessibleRole role = gtk_at_context_get_accessible_role (self); @@ -784,6 +771,7 @@ gtk_at_context_get_label (GtkATContext *self) GTK_ACCESSIBLE_PROPERTY_VALUE_NOW, }; + value = NULL; for (int i = 0; i < G_N_ELEMENTS (range_attrs); i++) { if (gtk_accessible_attribute_set_contains (self->properties, range_attrs[i])) @@ -794,7 +782,7 @@ gtk_at_context_get_label (GtkATContext *self) } if (value != NULL) - return g_strdup (gtk_string_accessible_value_get (value)); + g_ptr_array_add (names, (char *) gtk_string_accessible_value_get (value)); } break; @@ -802,13 +790,185 @@ gtk_at_context_get_label (GtkATContext *self) break; } + /* If there is no label or labelled-by attribute, hidden elements + * have no name + */ + if (gtk_accessible_attribute_set_contains (self->states, GTK_ACCESSIBLE_STATE_HIDDEN)) + { + value = gtk_accessible_attribute_set_get_value (self->states, GTK_ACCESSIBLE_STATE_HIDDEN); + + if (gtk_boolean_accessible_value_get (value)) + return; + } + + /* This fallback is in place only for unlabelled elements */ + if (names->len != 0) + return; + GEnumClass *enum_class = g_type_class_peek (GTK_TYPE_ACCESSIBLE_ROLE); GEnumValue *enum_value = g_enum_get_value (enum_class, role); if (enum_value != NULL) - return g_strdup (enum_value->value_nick); + g_ptr_array_add (names, (char *) enum_value->value_nick); +} - return g_strdup ("widget"); +static void +gtk_at_context_get_description_accumulate (GtkATContext *self, + GPtrArray *labels, + gboolean recurse) +{ + GtkAccessibleValue *value = NULL; + + if (gtk_accessible_attribute_set_contains (self->properties, GTK_ACCESSIBLE_PROPERTY_DESCRIPTION)) + { + value = gtk_accessible_attribute_set_get_value (self->properties, GTK_ACCESSIBLE_PROPERTY_DESCRIPTION); + + g_ptr_array_add (labels, (char *) gtk_string_accessible_value_get (value)); + } + + if (recurse && gtk_accessible_attribute_set_contains (self->relations, GTK_ACCESSIBLE_RELATION_DESCRIBED_BY)) + { + value = gtk_accessible_attribute_set_get_value (self->relations, GTK_ACCESSIBLE_RELATION_DESCRIBED_BY); + + GList *list = gtk_reference_list_accessible_value_get (value); + + for (GList *l = list; l != NULL; l = l->data) + { + GtkAccessible *rel = GTK_ACCESSIBLE (l->data); + GtkATContext *rel_context = gtk_accessible_get_at_context (rel); + + gtk_at_context_get_description_accumulate (rel_context, labels, FALSE); + } + } + + GtkAccessibleRole role = gtk_at_context_get_accessible_role (self); + + switch ((int) role) + { + case GTK_ACCESSIBLE_ROLE_RANGE: + { + int range_attrs[] = { + GTK_ACCESSIBLE_PROPERTY_VALUE_TEXT, + GTK_ACCESSIBLE_PROPERTY_VALUE_NOW, + }; + + value = NULL; + for (int i = 0; i < G_N_ELEMENTS (range_attrs); i++) + { + if (gtk_accessible_attribute_set_contains (self->properties, range_attrs[i])) + { + value = gtk_accessible_attribute_set_get_value (self->properties, range_attrs[i]); + break; + } + } + + if (value != NULL) + g_ptr_array_add (labels, (char *) gtk_string_accessible_value_get (value)); + } + break; + + default: + break; + } + + /* If there is no label or labelled-by attribute, hidden elements + * have no name + */ + if (gtk_accessible_attribute_set_contains (self->states, GTK_ACCESSIBLE_STATE_HIDDEN)) + { + value = gtk_accessible_attribute_set_get_value (self->states, GTK_ACCESSIBLE_STATE_HIDDEN); + + if (gtk_boolean_accessible_value_get (value)) + return; + } + + /* This fallback is in place only for unlabelled elements */ + if (labels->len != 0) + return; + + GEnumClass *enum_class = g_type_class_peek (GTK_TYPE_ACCESSIBLE_ROLE); + GEnumValue *enum_value = g_enum_get_value (enum_class, role); + + if (enum_value != NULL) + g_ptr_array_add (labels, (char *) enum_value->value_nick); +} + +/*< private > + * gtk_at_context_get_name: + * @self: a #GtkATContext + * + * Retrieves the accessible name of the #GtkATContext. + * + * This is a convenience function meant to be used by #GtkATContext implementations. + * + * Returns: (transfer full): the label of the #GtkATContext + */ +char * +gtk_at_context_get_name (GtkATContext *self) +{ + g_return_val_if_fail (GTK_IS_AT_CONTEXT (self), NULL); + + GPtrArray *names = g_ptr_array_new (); + + gtk_at_context_get_name_accumulate (self, names, TRUE); + + if (names->len == 0) + { + g_ptr_array_unref (names); + return g_strdup (""); + } + + GString *res = g_string_new (""); + g_string_append (res, g_ptr_array_index (names, 0)); + + for (guint i = 1; i < names->len; i++) + { + g_string_append (res, " "); + g_string_append (res, g_ptr_array_index (names, i)); + } + + g_ptr_array_unref (names); + + return g_string_free (res, FALSE); +} + +/*< private > + * gtk_at_context_get_description: + * @self: a #GtkATContext + * + * Retrieves the accessible description of the #GtkATContext. + * + * This is a convenience function meant to be used by #GtkATContext implementations. + * + * Returns: (transfer full): the label of the #GtkATContext + */ +char * +gtk_at_context_get_description (GtkATContext *self) +{ + g_return_val_if_fail (GTK_IS_AT_CONTEXT (self), NULL); + + GPtrArray *names = g_ptr_array_new (); + + gtk_at_context_get_description_accumulate (self, names, TRUE); + + if (names->len == 0) + { + g_ptr_array_unref (names); + return g_strdup (""); + } + + GString *res = g_string_new (""); + g_string_append (res, g_ptr_array_index (names, 0)); + + for (guint i = 1; i < names->len; i++) + { + g_string_append (res, " "); + g_string_append (res, g_ptr_array_index (names, i)); + } + + g_ptr_array_unref (names); + + return g_string_free (res, FALSE); } void diff --git a/gtk/gtkatcontextprivate.h b/gtk/gtkatcontextprivate.h index 4fec1444c8..ae276e1137 100644 --- a/gtk/gtkatcontextprivate.h +++ b/gtk/gtkatcontextprivate.h @@ -148,7 +148,8 @@ gboolean gtk_at_context_has_accessible_relation (GtkATContext GtkAccessibleValue * gtk_at_context_get_accessible_relation (GtkATContext *self, GtkAccessibleRelation relation); -char * gtk_at_context_get_label (GtkATContext *self); +char * gtk_at_context_get_name (GtkATContext *self); +char * gtk_at_context_get_description (GtkATContext *self); void gtk_at_context_platform_changed (GtkATContext *self, GtkAccessiblePlatformChange change); From 03745a489ce16465af3116ca3b9c4d082071b097 Mon Sep 17 00:00:00 2001 From: Emmanuele Bassi Date: Mon, 19 Oct 2020 18:37:30 +0100 Subject: [PATCH 2/6] docs: Start outlining a11y authoring practices We should have documentation for application developers and widget authors, so they can deal with the new accessibility API. --- docs/reference/gtk/section-accessibility.md | 64 +++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/docs/reference/gtk/section-accessibility.md b/docs/reference/gtk/section-accessibility.md index ecd3dacd26..9d352f640f 100644 --- a/docs/reference/gtk/section-accessibility.md +++ b/docs/reference/gtk/section-accessibility.md @@ -220,3 +220,67 @@ which acts as a proxy to the specific platform's accessibility API: Additionally, an ad hoc accessibility backend is available for the GTK testsuite, to ensure reproducibility of issues in the CI pipeline. + +## Authoring practices {#authoring-practices} + +The authoring practices are aimed at application developers, as well as +developers of GUI elements based on GTK. + +Functionally, #GtkAccessible roles, states, properties, and relations are +analogous to a CSS for assistive technologies. For screen reader users, for +instance, the various accessible attributes control the rendering of their +non-visual experience. Incorrect roles and attributes may result in a +completely inaccessible user interface. + +### A role is a promise + +The following code: + +```c +gtk_widget_class_set_accessible_role (widget_class, GTK_ACCESSIBLE_ROLE_BUTTON); +``` + +is a promise that the widget being created will provide the same keyboard +interactions expected for a button. An accessible role of a button will not +turn automatically any widget into a #GtkButton; but if your widget behaves +like a button, using the %GTK_ACCESSIBLE_ROLE_BUTTON will allow any +assistive technology to handle it like they would a #GtkButton. + +### Attributes can both hide and enhance + +Accessible attributes can be used to override the content of a UI element, +for instance: + +```c +gtk_label_set_text (GTK_LABEL (label), "Some text"); +gtk_accessible_update_property (GTK_ACCESSIBLE (label), + GTK_ACCESSIBLE_PROPERTY_LABEL, + "Assistive technologies users will perceive " + "this text, not the contents of the label", + -1); +``` + +In the example above, the "label" property will override the contents of the +label widget. + +The attributes can also enhance the UI: + +```c +gtk_button_set_label (GTK_BUTTON (button), "Download"); +gtk_box_append (GTK_BOX (button), button); + +gtk_label_set_text (GTK_LABEL (label), "Final report.pdf"); +gtk_box_append (GTK_BOX (box), label); + +gtk_accessible_update_relation (GTK_ACCESSIBLE (button), + GTK_ACCESSIBLE_RELATION_LABELLED_BY, + g_list_append (NULL, label), + -1); +``` + +In the example above, an assistive technology will read the button's +accessible label as "Download Final report.pdf". + +The power of hiding and enhancing can be a double-edged sword, as it can +lead to inadvertently overriding the accessible semantics of existing +widgets. From f52c86ae21507fade4080e68708451d0efd546ab Mon Sep 17 00:00:00 2001 From: Emmanuele Bassi Date: Mon, 19 Oct 2020 18:39:40 +0100 Subject: [PATCH 3/6] docs: Add a section on a11y patterns --- docs/reference/gtk/section-accessibility.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/reference/gtk/section-accessibility.md b/docs/reference/gtk/section-accessibility.md index 9d352f640f..5576066e2f 100644 --- a/docs/reference/gtk/section-accessibility.md +++ b/docs/reference/gtk/section-accessibility.md @@ -284,3 +284,7 @@ accessible label as "Download Final report.pdf". The power of hiding and enhancing can be a double-edged sword, as it can lead to inadvertently overriding the accessible semantics of existing widgets. + +## Design patterns and custom widgets + +... From 08ae513064c9a5b056a260ce0f73780a215d8745 Mon Sep 17 00:00:00 2001 From: Matthias Clasen Date: Mon, 19 Oct 2020 14:28:32 -0400 Subject: [PATCH 4/6] label: Set the accessible label property This will make label text show up in ATs again. --- gtk/gtklabel.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gtk/gtklabel.c b/gtk/gtklabel.c index d23f0253a9..b6c1e43e49 100644 --- a/gtk/gtklabel.c +++ b/gtk/gtklabel.c @@ -1696,6 +1696,10 @@ gtk_label_set_text_internal (GtkLabel *self, g_free (self->text); self->text = str; + gtk_accessible_update_property (GTK_ACCESSIBLE (self), + GTK_ACCESSIBLE_PROPERTY_LABEL, str, + -1); + gtk_label_select_region_index (self, 0, 0); } From 77d1026c5a56e7dc47721cd98ce29b39864bb8d8 Mon Sep 17 00:00:00 2001 From: Matthias Clasen Date: Mon, 19 Oct 2020 14:57:43 -0400 Subject: [PATCH 5/6] atspi: Use name and description as provided GtkATContext already does fallbacks to derive values for these, so no need for the atspi implementation to do its own fallback on top of that. --- gtk/a11y/gtkatspicontext.c | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/gtk/a11y/gtkatspicontext.c b/gtk/a11y/gtkatspicontext.c index 1a2434ff7c..6c3810a1fd 100644 --- a/gtk/a11y/gtkatspicontext.c +++ b/gtk/a11y/gtkatspicontext.c @@ -592,29 +592,14 @@ handle_accessible_get_property (GDBusConnection *connection, if (g_strcmp0 (property_name, "Name") == 0) { - if (GTK_IS_WIDGET (accessible)) - res = g_variant_new_string (gtk_widget_get_name (GTK_WIDGET (accessible))); - else if (GTK_IS_STACK_PAGE (accessible)) - { - const char *name = gtk_stack_page_get_name (GTK_STACK_PAGE (accessible)); - if (name == NULL) - name = G_OBJECT_TYPE_NAME (accessible); - res = g_variant_new_string (name); - } - else - res = g_variant_new_string (G_OBJECT_TYPE_NAME (accessible)); + char *label = gtk_at_context_get_name (GTK_AT_CONTEXT (self)); + res = g_variant_new_string (label ? label : ""); + g_free (label); } else if (g_strcmp0 (property_name, "Description") == 0) { char *label = gtk_at_context_get_description (GTK_AT_CONTEXT (self)); - - if (label == NULL || *label == '\0') - { - g_free (label); - label = gtk_at_context_get_name (GTK_AT_CONTEXT (self)); - } - - res = g_variant_new_string (label); + res = g_variant_new_string (label ? label : ""); g_free (label); } else if (g_strcmp0 (property_name, "Locale") == 0) From dfc7d26275f1cf402e207c6e50978cedde267cc2 Mon Sep 17 00:00:00 2001 From: Matthias Clasen Date: Mon, 19 Oct 2020 14:58:34 -0400 Subject: [PATCH 6/6] a11y: Tweak name and description computation Instead of falling back to the role nick for both, fall back to the class name for the name, and to the empty string for the description. This makes labels show up in Accerciser the same way they did in GTK 3, and seems more useful to me than the alternative. --- gtk/gtkatcontext.c | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/gtk/gtkatcontext.c b/gtk/gtkatcontext.c index 7bd1398ebb..fbe5a44f61 100644 --- a/gtk/gtkatcontext.c +++ b/gtk/gtkatcontext.c @@ -805,11 +805,8 @@ gtk_at_context_get_name_accumulate (GtkATContext *self, if (names->len != 0) return; - GEnumClass *enum_class = g_type_class_peek (GTK_TYPE_ACCESSIBLE_ROLE); - GEnumValue *enum_value = g_enum_get_value (enum_class, role); - - if (enum_value != NULL) - g_ptr_array_add (names, (char *) enum_value->value_nick); + if (self->accessible) + g_ptr_array_add (names, (char *)G_OBJECT_TYPE_NAME (self->accessible)); } static void @@ -871,8 +868,8 @@ gtk_at_context_get_description_accumulate (GtkATContext *self, break; } - /* If there is no label or labelled-by attribute, hidden elements - * have no name + /* If there is no description or described-by attribute, hidden elements + * have no description */ if (gtk_accessible_attribute_set_contains (self->states, GTK_ACCESSIBLE_STATE_HIDDEN)) { @@ -881,16 +878,6 @@ gtk_at_context_get_description_accumulate (GtkATContext *self, if (gtk_boolean_accessible_value_get (value)) return; } - - /* This fallback is in place only for unlabelled elements */ - if (labels->len != 0) - return; - - GEnumClass *enum_class = g_type_class_peek (GTK_TYPE_ACCESSIBLE_ROLE); - GEnumValue *enum_value = g_enum_get_value (enum_class, role); - - if (enum_value != NULL) - g_ptr_array_add (labels, (char *) enum_value->value_nick); } /*< private >