diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart
index 94befea6f9..05dbf88c9b 100644
--- a/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart
+++ b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart
@@ -1771,6 +1771,8 @@ class SemanticsObject {
return EngineSemanticsRole.link;
} else if (isHeader) {
return EngineSemanticsRole.header;
+ } else if (isButtonLike) {
+ return EngineSemanticsRole.button;
} else {
return EngineSemanticsRole.generic;
}
@@ -1852,8 +1854,14 @@ class SemanticsObject {
hasAction(ui.SemanticsAction.increase) || hasAction(ui.SemanticsAction.decrease);
/// Whether the object represents a button.
+ ///
+ /// See also [isButtonLike].
bool get isButton => hasFlag(ui.SemanticsFlag.isButton);
+ /// Whether the object behaves like a button even if it does not formally have
+ /// the [ui.SemanticsFlag.isButton] flag.
+ bool get isButtonLike => isTappable && !hasChildren;
+
/// Represents a tappable or clickable widget, such as button, icon button,
/// "hamburger" menu, etc.
bool get isTappable => hasAction(ui.SemanticsAction.tap);
diff --git a/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_test.dart b/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_test.dart
index 4f62b47456..d7c3998c9c 100644
--- a/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_test.dart
+++ b/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_test.dart
@@ -62,9 +62,6 @@ void runSemanticsTests() {
group('Roles', () {
_testRoleLifecycle();
});
- group('Text', () {
- _testText();
- });
group('labels', () {
_testLabels();
});
@@ -859,58 +856,6 @@ 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.semanticRole?.kind, EngineSemanticsRole.generic);
- expect(node.semanticRole!.behaviors!.map((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.semanticRole?.kind, EngineSemanticsRole.generic);
- expect(node.semanticRole!.behaviors!.map((m) => m.runtimeType).toList(), [
- Focusable,
- LiveRegion,
- RouteName,
- LabelAndValue,
- Tappable,
- ]);
- semantics().semanticsEnabled = false;
- });
-}
-
void _testLabels() {
test('computeDomSemanticsLabel combines tooltip, label, value, and hint', () {
expect(computeDomSemanticsLabel(tooltip: 'tooltip'), 'tooltip');
@@ -2306,7 +2251,7 @@ void _testSelectables() {
}
void _testTappable() {
- test('renders an enabled tappable widget', () async {
+ test('renders an enabled button', () async {
semantics()
..debugOverrideTimestampFunction(() => _testTime)
..semanticsEnabled = true;
@@ -2335,21 +2280,23 @@ void _testTappable() {
semantics().semanticsEnabled = false;
});
- test('renders a disabled tappable widget', () async {
+ test('renders a disabled button', () async {
semantics()
..debugOverrideTimestampFunction(() => _testTime)
..semanticsEnabled = true;
- final ui.SemanticsUpdateBuilder builder = ui.SemanticsUpdateBuilder();
- updateNode(
- builder,
- actions: 0 | ui.SemanticsAction.tap.index,
- flags: 0 | ui.SemanticsFlag.hasEnabledState.index | ui.SemanticsFlag.isButton.index,
- transform: Matrix4.identity().toFloat64(),
+ final SemanticsTester tester = SemanticsTester(owner());
+ tester.updateNode(
+ id: 0,
+ isFocusable: true,
+ hasTap: true,
+ hasEnabledState: true,
+ isEnabled: false,
+ isButton: true,
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
);
+ tester.apply();
- owner().updateSemantics(builder.build());
expectSemanticsTree(owner(), '''
''');
@@ -2357,7 +2304,40 @@ void _testTappable() {
semantics().semanticsEnabled = false;
});
- test('can switch tappable between enabled and disabled', () async {
+ test('tappable leaf node is a button', () async {
+ semantics()
+ ..debugOverrideTimestampFunction(() => _testTime)
+ ..semanticsEnabled = true;
+
+ final SemanticsTester tester = SemanticsTester(owner());
+ tester.updateNode(
+ id: 0,
+ isFocusable: true,
+ hasEnabledState: true,
+ isEnabled: true,
+
+ // Not a button
+ isButton: false,
+
+ // But has a tap action and no children
+ hasTap: true,
+ rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
+ );
+ tester.apply();
+
+ expectSemanticsTree(owner(), '''
+
+''');
+
+ final SemanticsObject node = owner().debugSemanticsTree![0]!;
+ expect(node.semanticRole?.kind, EngineSemanticsRole.button);
+ expect(node.semanticRole?.debugSemanticBehaviorTypes, containsAll([Focusable, Tappable]));
+ expect(tester.getSemanticsObject(0).element.tabIndex, 0);
+
+ semantics().semanticsEnabled = false;
+ });
+
+ test('can switch a button between enabled and disabled', () async {
semantics()
..debugOverrideTimestampFunction(() => _testTime)
..semanticsEnabled = true;
@@ -2390,7 +2370,7 @@ void _testTappable() {
semantics().semanticsEnabled = false;
});
- test('focuses on tappable after element has been attached', () async {
+ test('focuses on a button after element has been attached', () async {
semantics()
..debugOverrideTimestampFunction(() => _testTime)
..semanticsEnabled = true;