[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: <Widget>[
      Tab(icon: Image.network('https://some-url')),
      Tab(icon: Icon(Icons.beach_access_sharp)),
      Tab(icon: Icon(Icons.brightness_5_sharp)),
  ],
),
```
This commit is contained in:
flutteractionsbot 2025-05-28 09:39:01 -07:00 committed by GitHub
parent c56879b5f5
commit 7d3efe4643
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 40 additions and 2 deletions

View File

@ -1976,6 +1976,7 @@ class _TabBarState extends State<TabBar> {
), ),
), ),
); );
wrappedTabs[index] = MergeSemantics(child: wrappedTabs[index]);
if (!widget.isScrollable && effectiveTabAlignment == TabAlignment.fill) { if (!widget.isScrollable && effectiveTabAlignment == TabAlignment.fill) {
wrappedTabs[index] = Expanded(child: wrappedTabs[index]); wrappedTabs[index] = Expanded(child: wrappedTabs[index]);
} }

View File

@ -4300,6 +4300,9 @@ class SemanticsOwner extends ChangeNotifier {
return null; return null;
} }
if (node.mergeAllDescendantsIntoThisNode) { if (node.mergeAllDescendantsIntoThisNode) {
if (node._canPerformAction(action)) {
return node._actions[action];
}
SemanticsNode? result; SemanticsNode? result;
node._visitDescendants((SemanticsNode child) { node._visitDescendants((SemanticsNode child) {
if (child._canPerformAction(action)) { if (child._canPerformAction(action)) {

View File

@ -2,8 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:ui';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -156,6 +154,22 @@ void main() {
expect(tester.renderObject(find.byType(CustomPaint)).debugNeedsPaint, true); 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: <Widget>[Tab(icon: Semantics(image: true, child: const SizedBox.shrink()))],
),
),
),
);
expect(find.byType(Tab), findsOneWidget);
});
testWidgets('Tab sizing - icon', (WidgetTester tester) async { testWidgets('Tab sizing - icon', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
const MaterialApp( const MaterialApp(

View File

@ -918,6 +918,26 @@ void main() {
expect(newNode.id, expectId); 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: <SemanticsNode>[]);
root.updateWith(config: rootConfig, childrenInInversePaintOrder: <SemanticsNode>[merged]);
owner.performActionAt(const Offset(5, 5), SemanticsAction.tap);
expect(tapped, isTrue);
});
test('Tags show up in debug properties', () { test('Tags show up in debug properties', () {
final SemanticsNode actionNode = final SemanticsNode actionNode =
SemanticsNode()..tags = <SemanticsTag>{RenderViewport.useTwoPaneSemantics}; SemanticsNode()..tags = <SemanticsTag>{RenderViewport.useTwoPaneSemantics};