From 7d3efe46434dd2ed33b58e7277c8b1c25930b4b3 Mon Sep 17 00:00:00 2001 From: flutteractionsbot <154381524+flutteractionsbot@users.noreply.github.com> Date: Wed, 28 May 2025 09:39:01 -0700 Subject: [PATCH] [CP-stable]Fixes tab semantics gets dropped if the child produce a semantics node (#169362) This pull request is created by [automatic cherry pick workflow](https://github.com/flutter/flutter/blob/main/docs/releases/Flutter-Cherrypick-Process.md#automatically-creates-a-cherry-pick-request) Please fill in the form below, and a flutter domain expert will evaluate this cherry pick request. ### Issue Link: What is the link to the issue this cherry-pick is addressing? https://github.com/flutter/flutter/issues/169175 ### Changelog Description: Explain this cherry pick in one line that is accessible to most Flutter developers. See [best practices](https://github.com/flutter/flutter/blob/main/docs/releases/Hotfix-Documentation-Best-Practices.md) for examples Fixed unexpected crash when using Tab and TabBar widgets. ### Impact Description: What is the impact (ex. visual jank on Samsung phones, app crash, cannot ship an iOS app)? Does it impact development (ex. flutter doctor crashes when Android Studio is installed), or the shipping production app (the app crashes on launch) app crash ### Workaround: Is there a workaround for this issue? Wrap the Tab widget with a MergeSemantics widget will mitigate the issue. ### Risk: What is the risk level of this cherry-pick? - [O] Low ### Test Coverage: Are you confident that your fix is well-tested by automated tests? - [O] Yes ### Validation Steps: What are the steps to validate that this fix works? create a TabBar that has a Tab with image widget ```dart TabBar( tabs: [ Tab(icon: Image.network('https://some-url')), Tab(icon: Icon(Icons.beach_access_sharp)), Tab(icon: Icon(Icons.brightness_5_sharp)), ], ), ``` --- packages/flutter/lib/src/material/tabs.dart | 1 + .../flutter/lib/src/semantics/semantics.dart | 3 +++ packages/flutter/test/material/tabs_test.dart | 18 +++++++++++++++-- .../test/semantics/semantics_test.dart | 20 +++++++++++++++++++ 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/packages/flutter/lib/src/material/tabs.dart b/packages/flutter/lib/src/material/tabs.dart index 084b3b0a76..c85ef0caf7 100644 --- a/packages/flutter/lib/src/material/tabs.dart +++ b/packages/flutter/lib/src/material/tabs.dart @@ -1976,6 +1976,7 @@ class _TabBarState extends State { ), ), ); + wrappedTabs[index] = MergeSemantics(child: wrappedTabs[index]); if (!widget.isScrollable && effectiveTabAlignment == TabAlignment.fill) { wrappedTabs[index] = Expanded(child: wrappedTabs[index]); } diff --git a/packages/flutter/lib/src/semantics/semantics.dart b/packages/flutter/lib/src/semantics/semantics.dart index f4b5e0fe20..c8fedd4b4b 100644 --- a/packages/flutter/lib/src/semantics/semantics.dart +++ b/packages/flutter/lib/src/semantics/semantics.dart @@ -4300,6 +4300,9 @@ class SemanticsOwner extends ChangeNotifier { return null; } if (node.mergeAllDescendantsIntoThisNode) { + if (node._canPerformAction(action)) { + return node._actions[action]; + } SemanticsNode? result; node._visitDescendants((SemanticsNode child) { if (child._canPerformAction(action)) { diff --git a/packages/flutter/test/material/tabs_test.dart b/packages/flutter/test/material/tabs_test.dart index 0e71d0a9ca..ba1bb8385d 100644 --- a/packages/flutter/test/material/tabs_test.dart +++ b/packages/flutter/test/material/tabs_test.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui'; - import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -156,6 +154,22 @@ void main() { expect(tester.renderObject(find.byType(CustomPaint)).debugNeedsPaint, true); }); + testWidgets('tab semantics role test', (WidgetTester tester) async { + // Regressing test for https://github.com/flutter/flutter/issues/169175 + // Creates an image semantics node with zero size. + await tester.pumpWidget( + boilerplate( + child: DefaultTabController( + length: 1, + child: TabBar( + tabs: [Tab(icon: Semantics(image: true, child: const SizedBox.shrink()))], + ), + ), + ), + ); + expect(find.byType(Tab), findsOneWidget); + }); + testWidgets('Tab sizing - icon', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( diff --git a/packages/flutter/test/semantics/semantics_test.dart b/packages/flutter/test/semantics/semantics_test.dart index f30ec10080..c5c103404f 100644 --- a/packages/flutter/test/semantics/semantics_test.dart +++ b/packages/flutter/test/semantics/semantics_test.dart @@ -918,6 +918,26 @@ void main() { expect(newNode.id, expectId); }); + test('performActionAt can hit test on merged semantics node', () { + bool tapped = false; + final SemanticsOwner owner = SemanticsOwner(onSemanticsUpdate: (SemanticsUpdate update) {}); + final SemanticsNode root = SemanticsNode.root(owner: owner) + ..rect = const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0); + final SemanticsNode merged = SemanticsNode()..rect = const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0); + final SemanticsConfiguration mergeConfig = + SemanticsConfiguration() + ..isSemanticBoundary = true + ..isMergingSemanticsOfDescendants = true + ..onTap = () => tapped = true; + final SemanticsConfiguration rootConfig = SemanticsConfiguration()..isSemanticBoundary = true; + + merged.updateWith(config: mergeConfig, childrenInInversePaintOrder: []); + root.updateWith(config: rootConfig, childrenInInversePaintOrder: [merged]); + + owner.performActionAt(const Offset(5, 5), SemanticsAction.tap); + expect(tapped, isTrue); + }); + test('Tags show up in debug properties', () { final SemanticsNode actionNode = SemanticsNode()..tags = {RenderViewport.useTwoPaneSemantics};