From 8ac1636cbda0324b76dee0cfd69924ba1ae3f491 Mon Sep 17 00:00:00 2001 From: Chun-Heng Tai Date: Fri, 28 Aug 2026 11:56:16 -0700 Subject: [PATCH 1/3] [material_ui] Fixes SearchAnchor and SearchBar semantics --- .../material_ui/lib/src/search_anchor.dart | 10 ++- ...hange_2026_08_27_search_bar_semantics.yaml | 3 + .../material_ui/test/search_anchor_test.dart | 74 +++++++++++++++++++ 3 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 packages/material_ui/pending_changelogs/change_2026_08_27_search_bar_semantics.yaml diff --git a/packages/material_ui/lib/src/search_anchor.dart b/packages/material_ui/lib/src/search_anchor.dart index 845de4d3ddb1..cd7b113c78b9 100644 --- a/packages/material_ui/lib/src/search_anchor.dart +++ b/packages/material_ui/lib/src/search_anchor.dart @@ -606,7 +606,11 @@ class _SearchAnchorState extends State { duration: _kAnchorFadeDuration, child: IgnorePointer( ignoring: !widget.enabled, - child: GestureDetector(onTap: _openView, child: widget.builder(context, _searchController)), + child: GestureDetector( + excludeFromSemantics: true, + onTap: _openView, + child: widget.builder(context, _searchController), + ), ), ); } @@ -1825,6 +1829,10 @@ class _SearchBarState extends State { child: IgnorePointer( ignoring: !widget.enabled, child: InkWell( + canRequestFocus: false, + // Avoid providing duplicate semantics actions that the TextField + // already provides. + excludeFromSemantics: true, onTap: () { widget.onTap?.call(); if (!_focusNode.hasFocus) { diff --git a/packages/material_ui/pending_changelogs/change_2026_08_27_search_bar_semantics.yaml b/packages/material_ui/pending_changelogs/change_2026_08_27_search_bar_semantics.yaml new file mode 100644 index 000000000000..a353e374d3f8 --- /dev/null +++ b/packages/material_ui/pending_changelogs/change_2026_08_27_search_bar_semantics.yaml @@ -0,0 +1,3 @@ +changelog: | + - Fixes unlabeled tap target semantics in `SearchBar`. +version: patch diff --git a/packages/material_ui/test/search_anchor_test.dart b/packages/material_ui/test/search_anchor_test.dart index d810d74dce07..ddd5e9bb0249 100644 --- a/packages/material_ui/test/search_anchor_test.dart +++ b/packages/material_ui/test/search_anchor_test.dart @@ -3407,6 +3407,80 @@ void main() { semantics.dispose(); }); + testWidgets('SearchBar meets labeledTapTargetGuideline', (WidgetTester tester) async { + final semantics = SemanticsTester(tester); + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Center( + child: SearchBar( + hintText: 'Search...', + trailing: [ + IconButton(tooltip: 'Clear', icon: const Icon(Icons.clear), onPressed: () {}), + ], + ), + ), + ), + ), + ); + + await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); + semantics.dispose(); + }); + + testWidgets('SearchAnchor.bar meets labeledTapTargetGuideline', (WidgetTester tester) async { + final semantics = SemanticsTester(tester); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SearchAnchor.bar( + barHintText: 'Search...', + barTrailing: [ + IconButton(tooltip: 'Clear', icon: const Icon(Icons.clear), onPressed: () {}), + ], + suggestionsBuilder: (BuildContext context, SearchController controller) { + return []; + }, + ), + ), + ), + ); + + await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); + semantics.dispose(); + }); + + testWidgets('SearchBar does not produce an intermediate unlabeled semantics node', ( + WidgetTester tester, + ) async { + final semantics = SemanticsTester(tester); + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Center( + child: SearchBar( + hintText: 'Search...', + trailing: [ + IconButton(tooltip: 'Clear', icon: const Icon(Icons.clear), onPressed: () {}), + ], + ), + ), + ), + ), + ); + + for (final SemanticsNode node in semantics.nodesWith( + actions: [SemanticsAction.tap], + )) { + final bool isTextField = node.hasFlag(SemanticsFlag.isTextField); + final bool hasLabel = node.label.isNotEmpty; + final bool hasTooltip = node.tooltip.isNotEmpty; + final bool hasValue = node.value.isNotEmpty; + expect(isTextField || hasLabel || hasTooltip || hasValue, isTrue); + } + semantics.dispose(); + }); + testWidgets('Check SearchBar opacity when disabled', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( From 6c30b16fae617a63577bbcddc95ffdfe05a365d9 Mon Sep 17 00:00:00 2001 From: Chun-Heng Tai Date: Fri, 28 Aug 2026 12:08:35 -0700 Subject: [PATCH 2/3] update --- packages/material_ui/lib/src/search_anchor.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/material_ui/lib/src/search_anchor.dart b/packages/material_ui/lib/src/search_anchor.dart index cd7b113c78b9..0dcf7ef3123e 100644 --- a/packages/material_ui/lib/src/search_anchor.dart +++ b/packages/material_ui/lib/src/search_anchor.dart @@ -607,6 +607,7 @@ class _SearchAnchorState extends State { child: IgnorePointer( ignoring: !widget.enabled, child: GestureDetector( + // Avoid providing duplicate semantics actions. excludeFromSemantics: true, onTap: _openView, child: widget.builder(context, _searchController), From 657cb73a1d94e06b60256402a0cbff0faa5bdb5e Mon Sep 17 00:00:00 2001 From: Chun-Heng Tai Date: Fri, 28 Aug 2026 12:18:40 -0700 Subject: [PATCH 3/3] remove test --- .../material_ui/test/search_anchor_test.dart | 31 ------------------- 1 file changed, 31 deletions(-) diff --git a/packages/material_ui/test/search_anchor_test.dart b/packages/material_ui/test/search_anchor_test.dart index ddd5e9bb0249..cb7a1fb8454d 100644 --- a/packages/material_ui/test/search_anchor_test.dart +++ b/packages/material_ui/test/search_anchor_test.dart @@ -3450,37 +3450,6 @@ void main() { semantics.dispose(); }); - testWidgets('SearchBar does not produce an intermediate unlabeled semantics node', ( - WidgetTester tester, - ) async { - final semantics = SemanticsTester(tester); - await tester.pumpWidget( - MaterialApp( - home: Material( - child: Center( - child: SearchBar( - hintText: 'Search...', - trailing: [ - IconButton(tooltip: 'Clear', icon: const Icon(Icons.clear), onPressed: () {}), - ], - ), - ), - ), - ), - ); - - for (final SemanticsNode node in semantics.nodesWith( - actions: [SemanticsAction.tap], - )) { - final bool isTextField = node.hasFlag(SemanticsFlag.isTextField); - final bool hasLabel = node.label.isNotEmpty; - final bool hasTooltip = node.tooltip.isNotEmpty; - final bool hasValue = node.value.isNotEmpty; - expect(isTextField || hasLabel || hasTooltip || hasValue, isTrue); - } - semantics.dispose(); - }); - testWidgets('Check SearchBar opacity when disabled', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp(