[A11y] TextField prefix icon and suffix icon create a sibling node' (#173312)

prefix/suffix icon should have their own merge group. 

Noted a textfield can have suffix and suffix icon the same time so they
should be different merge group.

Co-authored-by: chunhtai <47866232+chunhtai@users.noreply.github.com>
This commit is contained in:
Hannah Jin 2025-08-07 14:31:24 -07:00 committed by GitHub
parent 052b903e8e
commit cb44b5a386
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 74 additions and 16 deletions

View File

@ -13,6 +13,7 @@ library;
import 'dart:math' as math;
import 'dart:ui' show lerpDouble;
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
@ -1685,25 +1686,28 @@ class _RenderDecoration extends RenderBox
) {
final ChildSemanticsConfigurationsResultBuilder builder =
ChildSemanticsConfigurationsResultBuilder();
List<SemanticsConfiguration>? prefixMergeGroup;
List<SemanticsConfiguration>? suffixMergeGroup;
final Map<SemanticsTag, List<SemanticsConfiguration>> mergeGroups =
<SemanticsTag, List<SemanticsConfiguration>>{};
final Set<SemanticsTag> tags = <SemanticsTag>{
_InputDecoratorState._kPrefixSemanticsTag,
_InputDecoratorState._kPrefixIconSemanticsTag,
_InputDecoratorState._kSuffixSemanticsTag,
_InputDecoratorState._kSuffixIconSemanticsTag,
};
for (final SemanticsConfiguration childConfig in childConfigs) {
if (childConfig.tagsChildrenWith(_InputDecoratorState._kPrefixSemanticsTag)) {
prefixMergeGroup ??= <SemanticsConfiguration>[];
prefixMergeGroup.add(childConfig);
} else if (childConfig.tagsChildrenWith(_InputDecoratorState._kSuffixSemanticsTag)) {
suffixMergeGroup ??= <SemanticsConfiguration>[];
suffixMergeGroup.add(childConfig);
final SemanticsTag? tag = tags.firstWhereOrNull(
(SemanticsTag tag) => childConfig.tagsChildrenWith(tag),
);
if (tag != null) {
mergeGroups.putIfAbsent(tag, () => <SemanticsConfiguration>[]).add(childConfig);
} else {
builder.markAsMergeUp(childConfig);
}
}
if (prefixMergeGroup != null) {
builder.markAsSiblingMergeGroup(prefixMergeGroup);
}
if (suffixMergeGroup != null) {
builder.markAsSiblingMergeGroup(suffixMergeGroup);
}
mergeGroups.values.forEach(builder.markAsSiblingMergeGroup);
return builder.build();
}
@ -1985,7 +1989,13 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
name: hashCode.toString(),
);
static const SemanticsTag _kPrefixSemanticsTag = SemanticsTag('_InputDecoratorState.prefix');
static const SemanticsTag _kPrefixIconSemanticsTag = SemanticsTag(
'_InputDecoratorState.prefixIcon',
);
static const SemanticsTag _kSuffixSemanticsTag = SemanticsTag('_InputDecoratorState.suffix');
static const SemanticsTag _kSuffixIconSemanticsTag = SemanticsTag(
'_InputDecoratorState.suffixIcon',
);
@override
void initState() {
@ -2466,7 +2476,10 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
iconSize: WidgetStatePropertyAll<double>(iconSize),
).merge(iconButtonTheme.style),
),
child: Semantics(child: decoration.prefixIcon),
child: Semantics(
tagForChildren: _kPrefixIconSemanticsTag,
child: decoration.prefixIcon,
),
),
),
),
@ -2503,7 +2516,10 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
iconSize: WidgetStatePropertyAll<double>(iconSize),
).merge(iconButtonTheme.style),
),
child: Semantics(child: decoration.suffixIcon),
child: Semantics(
tagForChildren: _kSuffixIconSemanticsTag,
child: decoration.suffixIcon,
),
),
),
),

View File

@ -5675,6 +5675,48 @@ void main() {
semantics.dispose();
});
testWidgets('TextField prefix icon and suffix icon create a sibling node', (
WidgetTester tester,
) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
overlay(
child: TextField(
controller: _textEditingController(text: 'some text'),
decoration: const InputDecoration(prefixIcon: Text('Prefix'), suffixIcon: Text('Suffix')),
),
),
);
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
id: 1,
textDirection: TextDirection.ltr,
value: 'some text',
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
inputType: ui.SemanticsInputType.text,
currentValueLength: 9,
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
],
),
TestSemantics.rootChild(id: 2, textDirection: TextDirection.ltr, label: 'Prefix'),
TestSemantics.rootChild(id: 3, textDirection: TextDirection.ltr, label: 'Suffix'),
],
),
ignoreTransform: true,
ignoreRect: true,
),
);
semantics.dispose();
});
testWidgets('TextField with specified suffixStyle', (WidgetTester tester) async {
final TextStyle suffixStyle = TextStyle(color: Colors.pink[500], fontSize: 10.0);