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 {