From 60cbfb76a247d683f7da61ae5039ed3aee7a5d1f Mon Sep 17 00:00:00 2001 From: chunhtai <47866232+chunhtai@users.noreply.github.com> Date: Mon, 9 Jun 2025 13:10:46 -0700 Subject: [PATCH] =?UTF-8?q?Fixes=20inputDecoration=20sibling=20explicit=20?= =?UTF-8?q?child=20not=20included=20in=20semantic=E2=80=A6=20(#170079)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …s tree fixes https://github.com/flutter/flutter/issues/169499 ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../flutter/lib/src/rendering/object.dart | 46 ++++++++++++++++--- .../test/material/text_field_test.dart | 36 +++++++++++++++ 2 files changed, 75 insertions(+), 7 deletions(-) diff --git a/packages/flutter/lib/src/rendering/object.dart b/packages/flutter/lib/src/rendering/object.dart index 0e533d68022..2b72048e7ab 100644 --- a/packages/flutter/lib/src/rendering/object.dart +++ b/packages/flutter/lib/src/rendering/object.dart @@ -4690,8 +4690,8 @@ class _SemanticsConfigurationProvider { /// /// This is typically use to recalculate certain properties when mutating /// [effective] since [effective] may contain stale data from previous update. - /// Examples are [SemanticsConfiguration.isBlockingUserActions] or - /// [SemanticsConfiguration.elevation]. Otherwise, use [effective] instead. + /// An example is [SemanticsConfiguration.isBlockingUserActions]. Otherwise, + /// use [effective] instead. SemanticsConfiguration get original { if (_originalConfiguration == null) { _effectiveConfiguration = _originalConfiguration = SemanticsConfiguration(); @@ -4822,8 +4822,7 @@ typedef _MergeUpAndSiblingMergeGroups = /// /// Merge all fragments from [mergeUp] and decide which [_RenderObjectSemantics] /// should form a node, i.e. [shouldFormSemanticsNode] is true. Stores the -/// [_RenderObjectSemantics] that should form a node with elevation adjustments -/// into [_children]. +/// [_RenderObjectSemantics] that should form a node into [_children]. /// /// At this point, walking the [_children] forms a tree /// that exactly resemble the resulting semantics node tree. @@ -5279,16 +5278,35 @@ class _RenderObjectSemantics extends _SemanticsFragment with DiagnosticableTreeM void _updateChildGeometry() { assert(geometry != null); + final _SemanticsGeometry parentGeometry = geometry!; for (final _RenderObjectSemantics child in _children) { final _SemanticsGeometry childGeometry = _SemanticsGeometry.computeChildGeometry( - parentPaintClipRect: geometry!.paintClipRect, - parentSemanticsClipRect: geometry!.semanticsClipRect, + parentPaintClipRect: parentGeometry.paintClipRect, + parentSemanticsClipRect: parentGeometry.semanticsClipRect, parentTransform: null, parent: this, child: child, ); child._updateGeometry(newGeometry: childGeometry); } + for (final _RenderObjectSemantics explicitSiblingChild in siblingMergeGroups + .expand<_SemanticsFragment>((List<_SemanticsFragment> group) => group) + .whereType<_RenderObjectSemantics>() + .expand( + (_RenderObjectSemantics siblingChild) => + siblingChild.shouldFormSemanticsNode + ? <_RenderObjectSemantics>[siblingChild] + : siblingChild._children, + )) { + final _SemanticsGeometry childGeometry = _SemanticsGeometry.computeChildGeometry( + parentPaintClipRect: parentGeometry.paintClipRect, + parentSemanticsClipRect: parentGeometry.semanticsClipRect, + parentTransform: parentGeometry.transform, + parent: this, + child: explicitSiblingChild, + ); + explicitSiblingChild._updateGeometry(newGeometry: childGeometry); + } } void _updateGeometry({required _SemanticsGeometry newGeometry}) { @@ -5433,7 +5451,16 @@ class _RenderObjectSemantics extends _SemanticsFragment with DiagnosticableTreeM for (final List<_SemanticsFragment> group in siblingMergeGroups) { SemanticsConfiguration? configuration; SemanticsNode? node; + final List<_RenderObjectSemantics> explicitChildren = <_RenderObjectSemantics>[]; for (final _SemanticsFragment fragment in group) { + if (fragment is _RenderObjectSemantics) { + if (fragment.shouldFormSemanticsNode) { + explicitChildren.add(fragment); + assert(fragment.configToMergeUp == null); + continue; + } + explicitChildren.addAll(fragment._children); + } if (fragment.configToMergeUp != null) { fragment.mergesToSibling = true; node ??= fragment.owner.cachedSemanticsNode; @@ -5441,6 +5468,11 @@ class _RenderObjectSemantics extends _SemanticsFragment with DiagnosticableTreeM configuration.absorb(fragment.configToMergeUp!); } } + final List childrenNodes = []; + for (final _RenderObjectSemantics explicitChild in explicitChildren) { + explicitChild._buildSemantics(usedSemanticsIds: usedSemanticsIds); + childrenNodes.addAll(explicitChild.semanticsNodes); + } // Can be null if all fragments in switchableFragments are marked as explicit. if (configuration != null) { if (node == null || usedSemanticsIds.contains(node.id)) { @@ -5453,7 +5485,7 @@ class _RenderObjectSemantics extends _SemanticsFragment with DiagnosticableTreeM fragment.owner.cachedSemanticsNode = node; } } - node.updateWith(config: configuration); + node.updateWith(config: configuration, childrenInInversePaintOrder: childrenNodes); _producedSiblingNodesAndOwners[node] = group; semanticsNodes.add(node); diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index bfa3f12636d..68a777e98c4 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -663,6 +663,42 @@ void main() { }, ); + testWidgets('suffix has correct semantics', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/169499. + final UniqueKey suffix = UniqueKey(); + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Scaffold( + body: Center( + child: TextField( + autofocus: true, + decoration: InputDecoration( + suffix: Semantics( + key: suffix, + identifier: 'myId', + container: true, + child: const SizedBox(width: 50, height: 50, child: Text('suffix')), + ), + ), + ), + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); // Wait for autofocus and suffix animation. + expect(find.text('suffix'), findsOneWidget); + expect( + tester.semantics.find(find.byKey(suffix)), + containsSemantics( + label: 'suffix', + identifier: 'myId', + rect: const Rect.fromLTWH(0, 0, 50, 50), + ), + ); + }); + testWidgets('sets cursorOpacityAnimates on EditableText correctly', (WidgetTester tester) async { // True