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;