@@ -6,19 +6,27 @@ import '../dom.dart';
6
6
import '../semantics.dart' ;
7
7
import '../util.dart' ;
8
8
9
- /// Provides accessibility for routes, including dialogs and pop-up menus.
10
- class SemanticDialog extends SemanticRole {
11
- SemanticDialog (SemanticsObject semanticsObject) : super .blank (SemanticRoleKind .dialog, semanticsObject) {
12
- // The following behaviors can coexist with dialog. Generic `RouteName`
13
- // and `LabelAndValue` are not used by this role because when the dialog
14
- // names its own route an `aria-label` is used instead of `aria-describedby`.
9
+ /// Denotes that all descendant nodes are inside a route.
10
+ ///
11
+ /// Routes can include dialogs, pop-up menus, sub-screens, and more.
12
+ ///
13
+ /// See also:
14
+ ///
15
+ /// * [RouteName] , which provides a description for this route in the absense
16
+ /// of an explicit route label set on the route itself.
17
+ class SemanticRoute extends SemanticRole {
18
+ SemanticRoute (SemanticsObject semanticsObject) : super .blank (SemanticRoleKind .route, semanticsObject) {
19
+ // The following behaviors can coexist with the route. Generic `RouteName`
20
+ // and `LabelAndValue` are not used by this role because when the route
21
+ // names its own route an `aria-label` is used instead of
22
+ // `aria-describedby`.
15
23
addFocusManagement ();
16
24
addLiveRegion ();
17
25
18
- // When a route/dialog shows up it is expected that the screen reader will
19
- // focus on something inside it. There could be two possibilities:
26
+ // When a route is pushed it is expected that the screen reader will focus
27
+ // on something inside it. There could be two possibilities:
20
28
//
21
- // 1. The framework explicitly marked a node inside the dialog as focused
29
+ // 1. The framework explicitly marked a node inside the route as focused
22
30
// via the `isFocusable` and `isFocused` flags. In this case, the node
23
31
// will request focus directly and there's nothing to do on top of that.
24
32
// 2. No node inside the route takes focus explicitly. In this case, the
@@ -53,103 +61,114 @@ class SemanticDialog extends SemanticRole {
53
61
void update () {
54
62
super .update ();
55
63
56
- // If semantic object corresponding to the dialog also provides the label
57
- // for itself it is applied as `aria-label`. See also [describeBy].
64
+ // If semantic object corresponding to the route also provides the label for
65
+ // itself it is applied as `aria-label`. See also [describeBy].
58
66
if (semanticsObject.namesRoute) {
59
67
final String ? label = semanticsObject.label;
60
68
assert (() {
61
69
if (label == null || label.trim ().isEmpty) {
62
70
printWarning (
63
71
'Semantic node ${semanticsObject .id } had both scopesRoute and '
64
- 'namesRoute set, indicating a self-labelled dialog , but it is '
65
- 'missing the label. A dialog should be labelled either by setting '
72
+ 'namesRoute set, indicating a self-labelled route , but it is '
73
+ 'missing the label. A route should be labelled either by setting '
66
74
'namesRoute on itself and providing a label, or by containing a '
67
75
'child node with namesRoute that can describe it with its content.'
68
76
);
69
77
}
70
78
return true ;
71
79
}());
80
+
72
81
setAttribute ('aria-label' , label ?? '' );
73
- setAriaRole ( 'dialog' );
82
+ _assignRole ( );
74
83
}
75
84
}
76
85
77
- /// Sets the description of this dialog based on a [RouteName] descendant
78
- /// node, unless the dialog provides its own label.
86
+ /// Sets the description of this route based on a [RouteName] descendant
87
+ /// node, unless the route provides its own label.
79
88
void describeBy (RouteName routeName) {
80
89
if (semanticsObject.namesRoute) {
81
- // The dialog provides its own label, which takes precedence.
90
+ // The route provides its own label, which takes precedence.
82
91
return ;
83
92
}
84
93
85
- setAriaRole ( 'dialog' );
94
+ _assignRole ( );
86
95
setAttribute (
87
96
'aria-describedby' ,
88
97
routeName.semanticsObject.element.id,
89
98
);
90
99
}
91
100
101
+ void _assignRole () {
102
+ // Lacking any more specific information, ARIA role "dialog" is the
103
+ // closest thing to Flutter's route. This can be revisited if better
104
+ // options become available, especially if the framework volunteers more
105
+ // specific information about the route. Other attributes in the vicinity
106
+ // of routes include: "alertdialog", `aria-modal`, "menu", "tooltip".
107
+ setAriaRole ('dialog' );
108
+ }
109
+
92
110
@override
93
111
bool focusAsRouteDefault () {
94
- // Dialogs are the ones that look inside themselves to find elements to
95
- // focus on. It doesn't make sense to focus on the dialog itself.
112
+ // Routes are the ones that look inside themselves to find elements to
113
+ // focus on. It doesn't make sense to focus on the route itself.
96
114
return false ;
97
115
}
98
116
}
99
117
100
- /// Supplies a description for the nearest ancestor [SemanticDialog ] .
118
+ /// Supplies a description for the nearest ancestor [SemanticRoute ] .
101
119
///
102
120
/// This role is assigned to nodes that have `namesRoute` set but not
103
- /// `scopesRoute` . When both flags are set the node only gets the [SemanticDialog] role.
121
+ /// `scopesRoute` . When both flags are set the node only gets the
122
+ /// [SemanticRoute] role.
104
123
///
105
- /// If the ancestor dialog is missing, this role has no effect. It is up to the
106
- /// framework, widget, and app authors to make sure a route name is scoped under
107
- /// a route.
124
+ /// If the ancestor route is missing, this role has no effect. It is up to the
125
+ /// framework, widget, and app authors to make sure a route name is scoped
126
+ /// under a route.
108
127
class RouteName extends SemanticBehavior {
109
128
RouteName (super .semanticsObject, super .owner);
110
129
111
- SemanticDialog ? _dialog ;
130
+ SemanticRoute ? _route ;
112
131
113
132
@override
114
133
void update () {
115
134
// NOTE(yjbanov): this does not handle the case when the node structure
116
- // changes such that this RouteName is no longer attached to the same
117
- // dialog. While this is technically expressible using the semantics API,
118
- // after discussing this case with customers I decided that this case is not
135
+ // changes such that this RouteName is no longer attached to the same route.
136
+ // While this is technically expressible using the semantics API, after
137
+ // discussing this case with customers I decided that this case is not
119
138
// interesting enough to support. A tree restructure like this is likely to
120
139
// confuse screen readers, and it would add complexity to the engine's
121
140
// semantics code. Since reparenting can be done with no update to either
122
- // the Dialog or RouteName we'd have to scan intermediate nodes for
123
- // structural changes.
141
+ // the SemanticRoute or RouteName we'd have to scan intermediate nodes
142
+ // for structural changes.
124
143
if (! semanticsObject.namesRoute) {
125
144
return ;
126
145
}
127
146
128
147
if (semanticsObject.isLabelDirty) {
129
- final SemanticDialog ? dialog = _dialog ;
130
- if (dialog != null ) {
131
- // Already attached to a dialog , just update the description.
132
- dialog .describeBy (this );
148
+ final SemanticRoute ? route = _route ;
149
+ if (route != null ) {
150
+ // Already attached to a route , just update the description.
151
+ route .describeBy (this );
133
152
} else {
134
153
// Setting the label for the first time. Wait for the DOM tree to be
135
- // established, then find the nearest dialog and update its label.
154
+ // established, then find the nearest route and update its label.
136
155
semanticsObject.owner.addOneTimePostUpdateCallback (() {
137
156
if (! isDisposed) {
138
- _lookUpNearestAncestorDialog ();
139
- _dialog ? .describeBy (this );
157
+ _lookUpNearestAncestorRoute ();
158
+ _route ? .describeBy (this );
140
159
}
141
160
});
142
161
}
143
162
}
144
163
}
145
164
146
- void _lookUpNearestAncestorDialog () {
165
+ void _lookUpNearestAncestorRoute () {
147
166
SemanticsObject ? parent = semanticsObject.parent;
148
- while (parent != null && parent.semanticRole? .kind != SemanticRoleKind .dialog ) {
167
+ while (parent != null && parent.semanticRole? .kind != SemanticRoleKind .route ) {
149
168
parent = parent.parent;
150
169
}
151
- if (parent != null && parent.semanticRole? .kind == SemanticRoleKind .dialog ) {
152
- _dialog = parent.semanticRole! as SemanticDialog ;
170
+ if (parent != null && parent.semanticRole? .kind == SemanticRoleKind .route ) {
171
+ _route = parent.semanticRole! as SemanticRoute ;
153
172
}
154
173
}
155
174
}
0 commit comments