diff --git a/lib/web_ui/lib/src/engine/semantics/checkable.dart b/lib/web_ui/lib/src/engine/semantics/checkable.dart
index 9c634f6f3a8f6..cf37b2423b59c 100644
--- a/lib/web_ui/lib/src/engine/semantics/checkable.dart
+++ b/lib/web_ui/lib/src/engine/semantics/checkable.dart
@@ -56,7 +56,9 @@ class Checkable extends PrimaryRoleManager {
PrimaryRole.checkable,
semanticsObject,
labelRepresentation: LeafLabelRepresentation.ariaLabel,
- );
+ ) {
+ addTappable();
+ }
final _CheckableKind _kind;
diff --git a/lib/web_ui/lib/src/engine/semantics/link.dart b/lib/web_ui/lib/src/engine/semantics/link.dart
index bb4346b538089..d9f93078f005f 100644
--- a/lib/web_ui/lib/src/engine/semantics/link.dart
+++ b/lib/web_ui/lib/src/engine/semantics/link.dart
@@ -11,7 +11,9 @@ class Link extends PrimaryRoleManager {
PrimaryRole.link,
semanticsObject,
labelRepresentation: LeafLabelRepresentation.domText,
- );
+ ) {
+ addTappable();
+ }
@override
DomElement createElement() {
diff --git a/lib/web_ui/lib/src/engine/semantics/semantics.dart b/lib/web_ui/lib/src/engine/semantics/semantics.dart
index 4f130bb001801..3d10056c20fa6 100644
--- a/lib/web_ui/lib/src/engine/semantics/semantics.dart
+++ b/lib/web_ui/lib/src/engine/semantics/semantics.dart
@@ -439,7 +439,6 @@ abstract class PrimaryRoleManager {
addLiveRegion();
addRouteName();
addLabelAndValue(labelRepresentation: labelRepresentation);
- addTappable();
}
/// Initializes a blank role for a [semanticsObject].
@@ -625,7 +624,17 @@ final class GenericRole extends PrimaryRoleManager {
PrimaryRole.generic,
semanticsObject,
labelRepresentation: LeafLabelRepresentation.domText,
- );
+ ) {
+ // Typically a tappable widget would have a more specific role, such as
+ // "link", "button", "checkbox", etc. However, there are situations when a
+ // tappable is not a leaf node, but contains other nodes, which can also be
+ // tappable. For example, the dismiss barrier of a pop-up menu is a tappable
+ // ancestor of the menu itself, while the menu may contain tappable
+ // children.
+ if (semanticsObject.isTappable) {
+ addTappable();
+ }
+ }
@override
void update() {
diff --git a/lib/web_ui/lib/src/engine/semantics/tappable.dart b/lib/web_ui/lib/src/engine/semantics/tappable.dart
index bbcdd216c3c4b..f99faab8f55cf 100644
--- a/lib/web_ui/lib/src/engine/semantics/tappable.dart
+++ b/lib/web_ui/lib/src/engine/semantics/tappable.dart
@@ -12,6 +12,7 @@ class Button extends PrimaryRoleManager {
semanticsObject,
labelRepresentation: LeafLabelRepresentation.domText,
) {
+ addTappable();
setAriaRole('button');
}
diff --git a/lib/web_ui/test/engine/semantics/semantics_test.dart b/lib/web_ui/test/engine/semantics/semantics_test.dart
index 661553b3be569..34dd6c79b8a5e 100644
--- a/lib/web_ui/test/engine/semantics/semantics_test.dart
+++ b/lib/web_ui/test/engine/semantics/semantics_test.dart
@@ -51,6 +51,9 @@ void runSemanticsTests() {
group('Role managers', () {
_testRoleManagerLifecycle();
});
+ group('Text', () {
+ _testText();
+ });
group('container', () {
_testContainer();
});
@@ -718,6 +721,74 @@ void _testLongestIncreasingSubsequence() {
});
}
+void _testText() {
+ test('renders a piece of plain text', () async {
+ semantics()
+ ..debugOverrideTimestampFunction(() => _testTime)
+ ..semanticsEnabled = true;
+
+ final ui.SemanticsUpdateBuilder builder = ui.SemanticsUpdateBuilder();
+ updateNode(
+ builder,
+ label: 'plain text',
+ rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
+ );
+ owner().updateSemantics(builder.build());
+
+ expectSemanticsTree(
+ owner(),
+ '''plain text''',
+ );
+
+ final SemanticsObject node = owner().debugSemanticsTree![0]!;
+ expect(node.primaryRole?.role, PrimaryRole.generic);
+ expect(
+ node.primaryRole!.secondaryRoleManagers!.map((RoleManager m) => m.runtimeType).toList(),
+ [
+ Focusable,
+ LiveRegion,
+ RouteName,
+ LabelAndValue,
+ ],
+ );
+ semantics().semanticsEnabled = false;
+ });
+
+ test('renders a tappable piece of text', () async {
+ semantics()
+ ..debugOverrideTimestampFunction(() => _testTime)
+ ..semanticsEnabled = true;
+
+ final SemanticsTester tester = SemanticsTester(owner());
+ tester.updateNode(
+ id: 0,
+ hasTap: true,
+ label: 'tappable text',
+ rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
+ );
+ tester.apply();
+
+ expectSemanticsTree(
+ owner(),
+ '''tappable text''',
+ );
+
+ final SemanticsObject node = owner().debugSemanticsTree![0]!;
+ expect(node.primaryRole?.role, PrimaryRole.generic);
+ expect(
+ node.primaryRole!.secondaryRoleManagers!.map((RoleManager m) => m.runtimeType).toList(),
+ [
+ Focusable,
+ LiveRegion,
+ RouteName,
+ LabelAndValue,
+ Tappable,
+ ],
+ );
+ semantics().semanticsEnabled = false;
+ });
+}
+
void _testContainer() {
test('container node has no transform when there is no rect offset',
() async {