diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter
index a6b7f3b490cc3..2759069858445 100644
--- a/ci/licenses_golden/licenses_flutter
+++ b/ci/licenses_golden/licenses_flutter
@@ -43012,7 +43012,6 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/scene_view.dart + ../../../fl
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/accessibility.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/checkable.dart + ../../../flutter/LICENSE
-ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/dialog.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/focusable.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/heading.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/image.dart + ../../../flutter/LICENSE
@@ -43021,6 +43020,7 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/label_and_value.dar
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/link.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/live_region.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/platform_view.dart + ../../../flutter/LICENSE
+ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/route.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/scrollable.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/semantics_helper.dart + ../../../flutter/LICENSE
@@ -45899,7 +45899,6 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/scene_view.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/accessibility.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/checkable.dart
-FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/dialog.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/focusable.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/heading.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/image.dart
@@ -45908,6 +45907,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/label_and_value.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/link.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/live_region.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/platform_view.dart
+FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/route.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/scrollable.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/semantics_helper.dart
diff --git a/lib/web_ui/lib/src/engine.dart b/lib/web_ui/lib/src/engine.dart
index b02fd59e2025c..cdf615a388617 100644
--- a/lib/web_ui/lib/src/engine.dart
+++ b/lib/web_ui/lib/src/engine.dart
@@ -145,7 +145,6 @@ export 'engine/scene_painting.dart';
export 'engine/scene_view.dart';
export 'engine/semantics/accessibility.dart';
export 'engine/semantics/checkable.dart';
-export 'engine/semantics/dialog.dart';
export 'engine/semantics/focusable.dart';
export 'engine/semantics/heading.dart';
export 'engine/semantics/image.dart';
@@ -154,6 +153,7 @@ export 'engine/semantics/label_and_value.dart';
export 'engine/semantics/link.dart';
export 'engine/semantics/live_region.dart';
export 'engine/semantics/platform_view.dart';
+export 'engine/semantics/route.dart';
export 'engine/semantics/scrollable.dart';
export 'engine/semantics/semantics.dart';
export 'engine/semantics/semantics_helper.dart';
diff --git a/lib/web_ui/lib/src/engine/semantics/focusable.dart b/lib/web_ui/lib/src/engine/semantics/focusable.dart
index b3289e35da829..54bc0d4e55392 100644
--- a/lib/web_ui/lib/src/engine/semantics/focusable.dart
+++ b/lib/web_ui/lib/src/engine/semantics/focusable.dart
@@ -254,14 +254,14 @@ class AccessibilityFocusManager {
// as it is subject to non-local effects. Let's say the framework decides
// that a semantics node is currently not focused. That would lead to
// changeFocus(false) to be called. However, what if this node is inside
- // a dialog, and nothing else in the dialog is focused. The Flutter
+ // a route, and nothing else in the route is focused? The Flutter
// framework expects that the screen reader will focus on the first (in
- // traversal order) focusable element inside the dialog and send a
+ // traversal order) focusable element inside the route and send a
// SemanticsAction.focus action. Screen readers on the web do not do
// that, and so the web engine has to implement this behavior directly. So
- // the dialog will look for a focusable element and request focus on it,
+ // the route will look for a focusable element and request focus on it,
// but now there may be a race between this method unsetting the focus and
- // the dialog requesting focus on the same element.
+ // the route requesting focus on the same element.
return;
}
diff --git a/lib/web_ui/lib/src/engine/semantics/dialog.dart b/lib/web_ui/lib/src/engine/semantics/route.dart
similarity index 55%
rename from lib/web_ui/lib/src/engine/semantics/dialog.dart
rename to lib/web_ui/lib/src/engine/semantics/route.dart
index 84896677dd21b..89052e65827f3 100644
--- a/lib/web_ui/lib/src/engine/semantics/dialog.dart
+++ b/lib/web_ui/lib/src/engine/semantics/route.dart
@@ -6,19 +6,27 @@ import '../dom.dart';
import '../semantics.dart';
import '../util.dart';
-/// Provides accessibility for routes, including dialogs and pop-up menus.
-class SemanticDialog extends SemanticRole {
- SemanticDialog(SemanticsObject semanticsObject) : super.blank(SemanticRoleKind.dialog, semanticsObject) {
- // The following behaviors can coexist with dialog. Generic `RouteName`
- // and `LabelAndValue` are not used by this role because when the dialog
- // names its own route an `aria-label` is used instead of `aria-describedby`.
+/// Denotes that all descendant nodes are inside a route.
+///
+/// Routes can include dialogs, pop-up menus, sub-screens, and more.
+///
+/// See also:
+///
+/// * [RouteName], which provides a description for this route in the absense
+/// of an explicit route label set on the route itself.
+class SemanticRoute extends SemanticRole {
+ SemanticRoute(SemanticsObject semanticsObject) : super.blank(SemanticRoleKind.route, semanticsObject) {
+ // The following behaviors can coexist with the route. Generic `RouteName`
+ // and `LabelAndValue` are not used by this role because when the route
+ // names its own route an `aria-label` is used instead of
+ // `aria-describedby`.
addFocusManagement();
addLiveRegion();
- // When a route/dialog shows up it is expected that the screen reader will
- // focus on something inside it. There could be two possibilities:
+ // When a route is pushed it is expected that the screen reader will focus
+ // on something inside it. There could be two possibilities:
//
- // 1. The framework explicitly marked a node inside the dialog as focused
+ // 1. The framework explicitly marked a node inside the route as focused
// via the `isFocusable` and `isFocused` flags. In this case, the node
// will request focus directly and there's nothing to do on top of that.
// 2. No node inside the route takes focus explicitly. In this case, the
@@ -53,103 +61,114 @@ class SemanticDialog extends SemanticRole {
void update() {
super.update();
- // If semantic object corresponding to the dialog also provides the label
- // for itself it is applied as `aria-label`. See also [describeBy].
+ // If semantic object corresponding to the route also provides the label for
+ // itself it is applied as `aria-label`. See also [describeBy].
if (semanticsObject.namesRoute) {
final String? label = semanticsObject.label;
assert(() {
if (label == null || label.trim().isEmpty) {
printWarning(
'Semantic node ${semanticsObject.id} had both scopesRoute and '
- 'namesRoute set, indicating a self-labelled dialog, but it is '
- 'missing the label. A dialog should be labelled either by setting '
+ 'namesRoute set, indicating a self-labelled route, but it is '
+ 'missing the label. A route should be labelled either by setting '
'namesRoute on itself and providing a label, or by containing a '
'child node with namesRoute that can describe it with its content.'
);
}
return true;
}());
+
setAttribute('aria-label', label ?? '');
- setAriaRole('dialog');
+ _assignRole();
}
}
- /// Sets the description of this dialog based on a [RouteName] descendant
- /// node, unless the dialog provides its own label.
+ /// Sets the description of this route based on a [RouteName] descendant
+ /// node, unless the route provides its own label.
void describeBy(RouteName routeName) {
if (semanticsObject.namesRoute) {
- // The dialog provides its own label, which takes precedence.
+ // The route provides its own label, which takes precedence.
return;
}
- setAriaRole('dialog');
+ _assignRole();
setAttribute(
'aria-describedby',
routeName.semanticsObject.element.id,
);
}
+ void _assignRole() {
+ // Lacking any more specific information, ARIA role "dialog" is the
+ // closest thing to Flutter's route. This can be revisited if better
+ // options become available, especially if the framework volunteers more
+ // specific information about the route. Other attributes in the vicinity
+ // of routes include: "alertdialog", `aria-modal`, "menu", "tooltip".
+ setAriaRole('dialog');
+ }
+
@override
bool focusAsRouteDefault() {
- // Dialogs are the ones that look inside themselves to find elements to
- // focus on. It doesn't make sense to focus on the dialog itself.
+ // Routes are the ones that look inside themselves to find elements to
+ // focus on. It doesn't make sense to focus on the route itself.
return false;
}
}
-/// Supplies a description for the nearest ancestor [SemanticDialog].
+/// Supplies a description for the nearest ancestor [SemanticRoute].
///
/// This role is assigned to nodes that have `namesRoute` set but not
-/// `scopesRoute`. When both flags are set the node only gets the [SemanticDialog] role.
+/// `scopesRoute`. When both flags are set the node only gets the
+/// [SemanticRoute] role.
///
-/// If the ancestor dialog is missing, this role has no effect. It is up to the
-/// framework, widget, and app authors to make sure a route name is scoped under
-/// a route.
+/// If the ancestor route is missing, this role has no effect. It is up to the
+/// framework, widget, and app authors to make sure a route name is scoped
+/// under a route.
class RouteName extends SemanticBehavior {
RouteName(super.semanticsObject, super.owner);
- SemanticDialog? _dialog;
+ SemanticRoute? _route;
@override
void update() {
// NOTE(yjbanov): this does not handle the case when the node structure
- // changes such that this RouteName is no longer attached to the same
- // dialog. While this is technically expressible using the semantics API,
- // after discussing this case with customers I decided that this case is not
+ // changes such that this RouteName is no longer attached to the same route.
+ // While this is technically expressible using the semantics API, after
+ // discussing this case with customers I decided that this case is not
// interesting enough to support. A tree restructure like this is likely to
// confuse screen readers, and it would add complexity to the engine's
// semantics code. Since reparenting can be done with no update to either
- // the Dialog or RouteName we'd have to scan intermediate nodes for
- // structural changes.
+ // the SemanticRoute or RouteName we'd have to scan intermediate nodes
+ // for structural changes.
if (!semanticsObject.namesRoute) {
return;
}
if (semanticsObject.isLabelDirty) {
- final SemanticDialog? dialog = _dialog;
- if (dialog != null) {
- // Already attached to a dialog, just update the description.
- dialog.describeBy(this);
+ final SemanticRoute? route = _route;
+ if (route != null) {
+ // Already attached to a route, just update the description.
+ route.describeBy(this);
} else {
// Setting the label for the first time. Wait for the DOM tree to be
- // established, then find the nearest dialog and update its label.
+ // established, then find the nearest route and update its label.
semanticsObject.owner.addOneTimePostUpdateCallback(() {
if (!isDisposed) {
- _lookUpNearestAncestorDialog();
- _dialog?.describeBy(this);
+ _lookUpNearestAncestorRoute();
+ _route?.describeBy(this);
}
});
}
}
}
- void _lookUpNearestAncestorDialog() {
+ void _lookUpNearestAncestorRoute() {
SemanticsObject? parent = semanticsObject.parent;
- while (parent != null && parent.semanticRole?.kind != SemanticRoleKind.dialog) {
+ while (parent != null && parent.semanticRole?.kind != SemanticRoleKind.route) {
parent = parent.parent;
}
- if (parent != null && parent.semanticRole?.kind == SemanticRoleKind.dialog) {
- _dialog = parent.semanticRole! as SemanticDialog;
+ if (parent != null && parent.semanticRole?.kind == SemanticRoleKind.route) {
+ _route = parent.semanticRole! as SemanticRoute;
}
}
}
diff --git a/lib/web_ui/lib/src/engine/semantics/semantics.dart b/lib/web_ui/lib/src/engine/semantics/semantics.dart
index a35651ba210e4..9b7135c8b79b2 100644
--- a/lib/web_ui/lib/src/engine/semantics/semantics.dart
+++ b/lib/web_ui/lib/src/engine/semantics/semantics.dart
@@ -20,7 +20,6 @@ import '../vector_math.dart';
import '../window.dart';
import 'accessibility.dart';
import 'checkable.dart';
-import 'dialog.dart';
import 'focusable.dart';
import 'heading.dart';
import 'image.dart';
@@ -29,6 +28,7 @@ import 'label_and_value.dart';
import 'link.dart';
import 'live_region.dart';
import 'platform_view.dart';
+import 'route.dart';
import 'scrollable.dart';
import 'semantics_helper.dart';
import 'tappable.dart';
@@ -379,19 +379,19 @@ enum SemanticRoleKind {
/// There are 3 possible situations:
///
/// * The node also has the `namesRoute` bit set. This means that the node's
- /// `label` describes the dialog, which can be expressed by adding the
+ /// `label` describes the route, which can be expressed by adding the
/// `aria-label` attribute.
/// * A descendant node has the `namesRoute` bit set. This means that the
- /// child's content describes the dialog. The child may simply be labelled,
- /// or it may be a subtree of nodes that describe the dialog together. The
+ /// child's content describes the route. The child may simply be labelled,
+ /// or it may be a subtree of nodes that describe the route together. The
/// nearest HTML equivalent is `aria-describedby`. The child acquires the
/// [routeName] role, which manages the relevant ARIA attributes.
/// * There is no `namesRoute` bit anywhere in the sub-tree rooted at the
- /// current node. In this case it's likely not a dialog at all, and the node
+ /// current node. In this case it's likely not a route at all, and the node
/// should not get a label or the "dialog" role. It's just a group of
/// children. For example, a modal barrier has `scopesRoute` set but marking
- /// it as a dialog would be wrong.
- dialog,
+ /// it as a route would be wrong.
+ route,
/// The node's role is to host a platform view.
platformView,
@@ -1653,7 +1653,7 @@ class SemanticsObject {
} else if (isScrollContainer) {
return SemanticRoleKind.scrollable;
} else if (scopesRoute) {
- return SemanticRoleKind.dialog;
+ return SemanticRoleKind.route;
} else if (isLink) {
return SemanticRoleKind.link;
} else {
@@ -1668,7 +1668,7 @@ class SemanticsObject {
SemanticRoleKind.incrementable => SemanticIncrementable(this),
SemanticRoleKind.button => SemanticButton(this),
SemanticRoleKind.checkable => SemanticCheckable(this),
- SemanticRoleKind.dialog => SemanticDialog(this),
+ SemanticRoleKind.route => SemanticRoute(this),
SemanticRoleKind.image => SemanticImage(this),
SemanticRoleKind.platformView => SemanticPlatformView(this),
SemanticRoleKind.link => SemanticLink(this),
diff --git a/lib/web_ui/test/engine/semantics/semantics_test.dart b/lib/web_ui/test/engine/semantics/semantics_test.dart
index 5c5e25fda0269..e55fc2cfda883 100644
--- a/lib/web_ui/test/engine/semantics/semantics_test.dart
+++ b/lib/web_ui/test/engine/semantics/semantics_test.dart
@@ -102,8 +102,8 @@ void runSemanticsTests() {
group('group', () {
_testGroup();
});
- group('dialog', () {
- _testDialog();
+ group('route', () {
+ _testRoute();
});
group('focusable', () {
_testFocusable();
@@ -2970,7 +2970,7 @@ void _testGroup() {
});
}
-void _testDialog() {
+void _testRoute() {
test('renders named and labeled routes', () {
semantics()
..debugOverrideTimestampFunction(() => _testTime)
@@ -2979,7 +2979,7 @@ void _testDialog() {
final ui.SemanticsUpdateBuilder builder = ui.SemanticsUpdateBuilder();
updateNode(
builder,
- label: 'this is a dialog label',
+ label: 'this is a route label',
flags: 0 | ui.SemanticsFlag.scopesRoute.index | ui.SemanticsFlag.namesRoute.index,
transform: Matrix4.identity().toFloat64(),
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
@@ -2995,12 +2995,12 @@ void _testDialog() {
owner().updateSemantics(builder.build());
expectSemanticsTree(owner(), '''
-
+
''');
expect(
owner().debugSemanticsTree![0]!.semanticRole?.kind,
- SemanticRoleKind.dialog,
+ SemanticRoleKind.route,
);
semantics().semanticsEnabled = false;
@@ -3034,7 +3034,7 @@ void _testDialog() {
expect(
warnings,
[
- 'Semantic node 0 had both scopesRoute and namesRoute set, indicating a self-labelled dialog, but it is missing the label. A dialog should be labelled either by setting namesRoute on itself and providing a label, or by containing a child node with namesRoute that can describe it with its content.',
+ 'Semantic node 0 had both scopesRoute and namesRoute set, indicating a self-labelled route, but it is missing the label. A route should be labelled either by setting namesRoute on itself and providing a label, or by containing a child node with namesRoute that can describe it with its content.',
],
);
@@ -3045,13 +3045,13 @@ void _testDialog() {
expect(
owner().debugSemanticsTree![0]!.semanticRole?.kind,
- SemanticRoleKind.dialog,
+ SemanticRoleKind.route,
);
semantics().semanticsEnabled = false;
});
- test('dialog can be described by a descendant', () {
+ test('route can be described by a descendant', () {
semantics()
..debugOverrideTimestampFunction(() => _testTime)
..semanticsEnabled = true;
@@ -3090,11 +3090,11 @@ void _testDialog() {
''');
}
- pumpSemantics(label: 'Dialog label');
+ pumpSemantics(label: 'Route label');
expect(
owner().debugSemanticsTree![0]!.semanticRole?.kind,
- SemanticRoleKind.dialog,
+ SemanticRoleKind.route,
);
expect(
owner().debugSemanticsTree![2]!.semanticRole?.kind,
@@ -3105,12 +3105,12 @@ void _testDialog() {
contains(RouteName),
);
- pumpSemantics(label: 'Updated dialog label');
+ pumpSemantics(label: 'Updated route label');
semantics().semanticsEnabled = false;
});
- test('scopesRoute alone sets the dialog role with no label', () {
+ test('scopesRoute alone sets the SemanticRoute role with no label', () {
final List warnings = [];
printWarning = warnings.add;
@@ -3132,7 +3132,7 @@ void _testDialog() {
expect(
owner().debugSemanticsTree![0]!.semanticRole?.kind,
- SemanticRoleKind.dialog,
+ SemanticRoleKind.route,
);
expect(
owner().debugSemanticsTree![0]!.semanticRole?.behaviors,
@@ -3190,7 +3190,7 @@ void _testDialog() {
semantics().semanticsEnabled = false;
});
- // Test the simple scenario of a dialog coming up and containing focusable
+ // Test the simple scenario of a route coming up and containing focusable
// descendants that are not initially focused. The expectation is that the
// first descendant will be auto-focused.
test('focuses on the first unfocused Focusable', () async {
@@ -3248,9 +3248,9 @@ void _testDialog() {
semantics().semanticsEnabled = false;
});
- // Test the scenario of a dialog coming up and containing focusable
+ // Test the scenario of a route coming up and containing focusable
// descendants with one of them explicitly requesting focus. The expectation
- // is that the dialog will not attempt to auto-focus on anything and let the
+ // is that the route will not attempt to auto-focus on anything and let the
// respective descendant take focus.
test('does nothing if a descendant asks for focus explicitly', () async {
semantics()
@@ -3306,7 +3306,7 @@ void _testDialog() {
semantics().semanticsEnabled = false;
});
- // Test the scenario of a dialog coming up and containing non-focusable
+ // Test the scenario of a route coming up and containing non-focusable
// descendants that can have a11y focus. The expectation is that the first
// descendant will be auto-focused, even if it's not input-focusable.
test('focuses on the first non-focusable descedant', () async {
@@ -3378,8 +3378,8 @@ void _testDialog() {
});
// This mostly makes sure the engine doesn't crash if given a completely empty
- // dialog trying to find something to focus on.
- test('does nothing if nothing is focusable inside the dialog', () async {
+ // route trying to find something to focus on.
+ test('does nothing if nothing is focusable inside the route', () async {
semantics()
..debugOverrideTimestampFunction(() => _testTime)
..semanticsEnabled = true;