Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 795c0d8

Browse files
authored
[web] rename dialog to route to match the framework (#54228)
Rename "dialog" to "route" to match the framework. The ARIA role "dialog" is an implementation detail, and it might change if a better implementation comes along. The semantic node itself implements the framework's "route" concept.
1 parent 0046e32 commit 795c0d8

File tree

6 files changed

+97
-78
lines changed

6 files changed

+97
-78
lines changed

ci/licenses_golden/licenses_flutter

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43014,7 +43014,6 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/scene_view.dart + ../../../fl
4301443014
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics.dart + ../../../flutter/LICENSE
4301543015
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/accessibility.dart + ../../../flutter/LICENSE
4301643016
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/checkable.dart + ../../../flutter/LICENSE
43017-
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/dialog.dart + ../../../flutter/LICENSE
4301843017
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/focusable.dart + ../../../flutter/LICENSE
4301943018
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/heading.dart + ../../../flutter/LICENSE
4302043019
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/image.dart + ../../../flutter/LICENSE
@@ -43023,6 +43022,7 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/label_and_value.dar
4302343022
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/link.dart + ../../../flutter/LICENSE
4302443023
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/live_region.dart + ../../../flutter/LICENSE
4302543024
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/platform_view.dart + ../../../flutter/LICENSE
43025+
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/route.dart + ../../../flutter/LICENSE
4302643026
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/scrollable.dart + ../../../flutter/LICENSE
4302743027
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart + ../../../flutter/LICENSE
4302843028
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/semantics_helper.dart + ../../../flutter/LICENSE
@@ -45903,7 +45903,6 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/scene_view.dart
4590345903
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics.dart
4590445904
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/accessibility.dart
4590545905
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/checkable.dart
45906-
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/dialog.dart
4590745906
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/focusable.dart
4590845907
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/heading.dart
4590945908
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/image.dart
@@ -45912,6 +45911,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/label_and_value.dart
4591245911
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/link.dart
4591345912
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/live_region.dart
4591445913
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/platform_view.dart
45914+
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/route.dart
4591545915
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/scrollable.dart
4591645916
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart
4591745917
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/semantics_helper.dart

lib/web_ui/lib/src/engine.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,6 @@ export 'engine/scene_painting.dart';
145145
export 'engine/scene_view.dart';
146146
export 'engine/semantics/accessibility.dart';
147147
export 'engine/semantics/checkable.dart';
148-
export 'engine/semantics/dialog.dart';
149148
export 'engine/semantics/focusable.dart';
150149
export 'engine/semantics/heading.dart';
151150
export 'engine/semantics/image.dart';
@@ -154,6 +153,7 @@ export 'engine/semantics/label_and_value.dart';
154153
export 'engine/semantics/link.dart';
155154
export 'engine/semantics/live_region.dart';
156155
export 'engine/semantics/platform_view.dart';
156+
export 'engine/semantics/route.dart';
157157
export 'engine/semantics/scrollable.dart';
158158
export 'engine/semantics/semantics.dart';
159159
export 'engine/semantics/semantics_helper.dart';

lib/web_ui/lib/src/engine/semantics/focusable.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -254,14 +254,14 @@ class AccessibilityFocusManager {
254254
// as it is subject to non-local effects. Let's say the framework decides
255255
// that a semantics node is currently not focused. That would lead to
256256
// changeFocus(false) to be called. However, what if this node is inside
257-
// a dialog, and nothing else in the dialog is focused. The Flutter
257+
// a route, and nothing else in the route is focused? The Flutter
258258
// framework expects that the screen reader will focus on the first (in
259-
// traversal order) focusable element inside the dialog and send a
259+
// traversal order) focusable element inside the route and send a
260260
// SemanticsAction.focus action. Screen readers on the web do not do
261261
// that, and so the web engine has to implement this behavior directly. So
262-
// the dialog will look for a focusable element and request focus on it,
262+
// the route will look for a focusable element and request focus on it,
263263
// but now there may be a race between this method unsetting the focus and
264-
// the dialog requesting focus on the same element.
264+
// the route requesting focus on the same element.
265265
return;
266266
}
267267

lib/web_ui/lib/src/engine/semantics/dialog.dart renamed to lib/web_ui/lib/src/engine/semantics/route.dart

Lines changed: 61 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,27 @@ import '../dom.dart';
66
import '../semantics.dart';
77
import '../util.dart';
88

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`.
1523
addFocusManagement();
1624
addLiveRegion();
1725

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:
2028
//
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
2230
// via the `isFocusable` and `isFocused` flags. In this case, the node
2331
// will request focus directly and there's nothing to do on top of that.
2432
// 2. No node inside the route takes focus explicitly. In this case, the
@@ -53,103 +61,114 @@ class SemanticDialog extends SemanticRole {
5361
void update() {
5462
super.update();
5563

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].
5866
if (semanticsObject.namesRoute) {
5967
final String? label = semanticsObject.label;
6068
assert(() {
6169
if (label == null || label.trim().isEmpty) {
6270
printWarning(
6371
'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 '
6674
'namesRoute on itself and providing a label, or by containing a '
6775
'child node with namesRoute that can describe it with its content.'
6876
);
6977
}
7078
return true;
7179
}());
80+
7281
setAttribute('aria-label', label ?? '');
73-
setAriaRole('dialog');
82+
_assignRole();
7483
}
7584
}
7685

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.
7988
void describeBy(RouteName routeName) {
8089
if (semanticsObject.namesRoute) {
81-
// The dialog provides its own label, which takes precedence.
90+
// The route provides its own label, which takes precedence.
8291
return;
8392
}
8493

85-
setAriaRole('dialog');
94+
_assignRole();
8695
setAttribute(
8796
'aria-describedby',
8897
routeName.semanticsObject.element.id,
8998
);
9099
}
91100

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+
92110
@override
93111
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.
96114
return false;
97115
}
98116
}
99117

100-
/// Supplies a description for the nearest ancestor [SemanticDialog].
118+
/// Supplies a description for the nearest ancestor [SemanticRoute].
101119
///
102120
/// 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.
104123
///
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.
108127
class RouteName extends SemanticBehavior {
109128
RouteName(super.semanticsObject, super.owner);
110129

111-
SemanticDialog? _dialog;
130+
SemanticRoute? _route;
112131

113132
@override
114133
void update() {
115134
// 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
119138
// interesting enough to support. A tree restructure like this is likely to
120139
// confuse screen readers, and it would add complexity to the engine's
121140
// 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.
124143
if (!semanticsObject.namesRoute) {
125144
return;
126145
}
127146

128147
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);
133152
} else {
134153
// 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.
136155
semanticsObject.owner.addOneTimePostUpdateCallback(() {
137156
if (!isDisposed) {
138-
_lookUpNearestAncestorDialog();
139-
_dialog?.describeBy(this);
157+
_lookUpNearestAncestorRoute();
158+
_route?.describeBy(this);
140159
}
141160
});
142161
}
143162
}
144163
}
145164

146-
void _lookUpNearestAncestorDialog() {
165+
void _lookUpNearestAncestorRoute() {
147166
SemanticsObject? parent = semanticsObject.parent;
148-
while (parent != null && parent.semanticRole?.kind != SemanticRoleKind.dialog) {
167+
while (parent != null && parent.semanticRole?.kind != SemanticRoleKind.route) {
149168
parent = parent.parent;
150169
}
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;
153172
}
154173
}
155174
}

lib/web_ui/lib/src/engine/semantics/semantics.dart

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import '../vector_math.dart';
2020
import '../window.dart';
2121
import 'accessibility.dart';
2222
import 'checkable.dart';
23-
import 'dialog.dart';
2423
import 'focusable.dart';
2524
import 'heading.dart';
2625
import 'image.dart';
@@ -29,6 +28,7 @@ import 'label_and_value.dart';
2928
import 'link.dart';
3029
import 'live_region.dart';
3130
import 'platform_view.dart';
31+
import 'route.dart';
3232
import 'scrollable.dart';
3333
import 'semantics_helper.dart';
3434
import 'tappable.dart';
@@ -379,19 +379,19 @@ enum SemanticRoleKind {
379379
/// There are 3 possible situations:
380380
///
381381
/// * The node also has the `namesRoute` bit set. This means that the node's
382-
/// `label` describes the dialog, which can be expressed by adding the
382+
/// `label` describes the route, which can be expressed by adding the
383383
/// `aria-label` attribute.
384384
/// * A descendant node has the `namesRoute` bit set. This means that the
385-
/// child's content describes the dialog. The child may simply be labelled,
386-
/// or it may be a subtree of nodes that describe the dialog together. The
385+
/// child's content describes the route. The child may simply be labelled,
386+
/// or it may be a subtree of nodes that describe the route together. The
387387
/// nearest HTML equivalent is `aria-describedby`. The child acquires the
388388
/// [routeName] role, which manages the relevant ARIA attributes.
389389
/// * There is no `namesRoute` bit anywhere in the sub-tree rooted at the
390-
/// current node. In this case it's likely not a dialog at all, and the node
390+
/// current node. In this case it's likely not a route at all, and the node
391391
/// should not get a label or the "dialog" role. It's just a group of
392392
/// children. For example, a modal barrier has `scopesRoute` set but marking
393-
/// it as a dialog would be wrong.
394-
dialog,
393+
/// it as a route would be wrong.
394+
route,
395395

396396
/// The node's role is to host a platform view.
397397
platformView,
@@ -1653,7 +1653,7 @@ class SemanticsObject {
16531653
} else if (isScrollContainer) {
16541654
return SemanticRoleKind.scrollable;
16551655
} else if (scopesRoute) {
1656-
return SemanticRoleKind.dialog;
1656+
return SemanticRoleKind.route;
16571657
} else if (isLink) {
16581658
return SemanticRoleKind.link;
16591659
} else {
@@ -1668,7 +1668,7 @@ class SemanticsObject {
16681668
SemanticRoleKind.incrementable => SemanticIncrementable(this),
16691669
SemanticRoleKind.button => SemanticButton(this),
16701670
SemanticRoleKind.checkable => SemanticCheckable(this),
1671-
SemanticRoleKind.dialog => SemanticDialog(this),
1671+
SemanticRoleKind.route => SemanticRoute(this),
16721672
SemanticRoleKind.image => SemanticImage(this),
16731673
SemanticRoleKind.platformView => SemanticPlatformView(this),
16741674
SemanticRoleKind.link => SemanticLink(this),

0 commit comments

Comments
 (0)