Repository navigation
InputDecorator prefix and suffix text isn't included in semantics when TextField is wrapped in MergeSemantics. #160281
Description
Description
When creating a TextField (or TextFormField), there isn't a good way to merge the semantics of the prefix/suffix fields of an InputDecorator. If you do the obvious thing:
MergeSemantics(
child: Semantics(
label: 'Room Height',
child: TextField(
controller: _heightController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
prefixText: 'height',
suffixText: 'feet',
prefixStyle: TextStyle(color: Colors.red),
),
),
),
)Semantic Tree
SemanticsNode#0
│ Rect.fromLTRB(0.0, 0.0, 1080.0, 2340.0)
│
└─SemanticsNode#1
│ Rect.fromLTRB(0.0, 0.0, 411.4, 891.4) scaled by 2.6x
│ textDirection: ltr
│
└─SemanticsNode#2
│ Rect.fromLTRB(0.0, 0.0, 411.4, 891.4)
│ sortKey: OrdinalSortKey#a7ceb(order: 0.0)
│
└─SemanticsNode#3
│ Rect.fromLTRB(0.0, 0.0, 411.4, 891.4)
│ flags: scopesRoute
│
├─SemanticsNode#6
│ Rect.fromLTRB(16.0, 24.5, 76.5, 55.5)
│ tags: []
│ label: "height"
│ textDirection: ltr
│ sortKey: OrdinalSortKey#c3609(name: "238345892", order: 0.0)
│
├─SemanticsNode#4
│ merge boundary ⛔️
│ Rect.fromLTRB(16.0, 16.0, 395.4, 64.0)
│ actions: focus, moveCursorBackwardByCharacter,
│ moveCursorBackwardByWord, setSelection, setText, tap
│ flags: isTextField, isFocused, hasEnabledState, isEnabled
│ label: "Room Height"
│ value: ""
│ textDirection: ltr
│ sortKey: OrdinalSortKey#59316(name: "238345892", order: 1.0)
│ text selection: [3, 3]
│ currentValueLength: 3
│
├─SemanticsNode#7
│ Rect.fromLTRB(357.6, 24.5, 395.4, 55.5)
│ tags: []
│ label: "feet"
│ textDirection: ltr
│ sortKey: OrdinalSortKey#390ca(name: "238345892", order: 2.0)
│
└─SemanticsNode#5
Rect.fromLTRB(161.1, 64.0, 250.3, 112.0)
actions: focus, tap
flags: isButton, hasEnabledState, isEnabled, isFocusable
label: "Dump"
textDirection: ltr
thickness: 1.0
Then the semantics label you get on Talkback when the field is focused is "Room Height", not "Room Height height feet" (or, more usefully: "Room Height prefix height suffix feet" or something similar).
This is probably a design choice because people often put controls in the suffix and prefix fields, but if those are text (via prefixText and suffixText), then it seems like a good idea to merge their semantics somehow if there is a MergeSemantics around the text field.
Below is a simple reproduction case. Also, if you uncomment the container: true, then it merges in the prefix and suffix to the semantics nodes as expected, but Talkback doesn't read them, and you can no longer navigate to them (because they are merged in, which makes sense).
There needs to be a way to merge in the semantics so that a suffix or prefix will be read, but without it appearing to be part of the user's text (in Talkback).
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Semantics Demo'),
),
body: const Center(
child: Padding(
padding: EdgeInsets.all(16.0),
child: HeightInput(),
),
),
),
);
}
}
class HeightInput extends StatefulWidget {
const HeightInput({super.key});
@override
State<HeightInput> createState() => _HeightInputState();
}
class _HeightInputState extends State<HeightInput> {
final TextEditingController _heightController = TextEditingController();
@override
void initState() {
super.initState();
_heightController.addListener(() {
if (_heightController.text.startsWith('d')) {
debugDumpSemanticsTree();
}
});
}
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
MergeSemantics(
child: Semantics(
label: 'Room Height',
// container: true, // Uncomment this to see container effect.
child: TextField(
controller: _heightController,
keyboardType: TextInputType.text,
decoration: const InputDecoration(
prefixText: 'height',
suffixText: 'feet',
),
),
),
),
const ElevatedButton(
onPressed: debugDumpSemanticsTree,
child: Text('Dump'),
)
],
);
}
@override
void dispose() {
_heightController.dispose();
super.dispose();
}
}
Semantic Tree with container:true
SemanticsNode#0
│ Rect.fromLTRB(0.0, 0.0, 1080.0, 2340.0)
│
└─SemanticsNode#1
│ Rect.fromLTRB(0.0, 0.0, 411.4, 891.4) scaled by 2.6x
│ textDirection: ltr
│
└─SemanticsNode#2
│ Rect.fromLTRB(0.0, 0.0, 411.4, 891.4)
│ sortKey: OrdinalSortKey#317df(order: 0.0)
│
└─SemanticsNode#3
│ Rect.fromLTRB(0.0, 0.0, 411.4, 891.4)
│ flags: scopesRoute
│
├─SemanticsNode#7
│ │ Rect.fromLTRB(0.0, 0.0, 411.4, 97.1)
│ │
│ └─SemanticsNode#8
│ Rect.fromLTRB(16.0, 51.1, 233.0, 87.1)
│ flags: isHeader, namesRoute
│ label: "Semantics Demo"
│ textDirection: ltr
│
├─SemanticsNode#4
│ │ merge boundary ⛔️
│ │ Rect.fromLTRB(16.0, 113.1, 395.4, 161.1)
│ │
│ ├─SemanticsNode#9
│ │ merged up ⬆️
│ │ Rect.fromLTRB(0.0, 8.5, 60.5, 39.5)
│ │ tags: []
│ │ label: "height"
│ │ textDirection: ltr
│ │ sortKey: OrdinalSortKey#dbfb4(name: "859212319", order: 0.0)
│ │
│ ├─SemanticsNode#5
│ │ merged up ⬆️
│ │ Rect.fromLTRB(0.0, 0.0, 379.4, 48.0)
│ │ actions: focus, setSelection, setText, tap
│ │ flags: isTextField, isFocused, hasEnabledState, isEnabled
│ │ label: "Room Height"
│ │ textDirection: ltr
│ │ sortKey: OrdinalSortKey#3e8c4(name: "859212319", order: 1.0)
│ │ text selection: [0, 0]
│ │ currentValueLength: 0
│ │
│ └─SemanticsNode#10
│ merged up ⬆️
│ Rect.fromLTRB(341.6, 8.5, 379.4, 39.5)
│ tags: []
│ label: "feet"
│ textDirection: ltr
│ sortKey: OrdinalSortKey#bbdd6(name: "859212319", order: 2.0)
│
└─SemanticsNode#6
Rect.fromLTRB(161.1, 161.1, 250.3, 209.1)
actions: focus, tap
flags: isButton, hasEnabledState, isEnabled, isFocusable
label: "Dump"
textDirection: ltr
thickness: 1.0
I/flutter ( 8407):
flutter doctor -v
[!] Flutter (Channel [user-branch], 3.27.0-1.0.pre.523, on macOS 14.7.1 23H222 darwin-arm64, locale en)
! Flutter version 3.27.0-1.0.pre.523 on channel [user-branch] at /Users/user/code/flutter
Currently on an unknown channel. Run `flutter channel` to switch to an official channel.
If that doesn't fix the issue, reinstall Flutter by following instructions at https://flutter.dev/setup.
! Upstream repository [email protected]:gspencergoog/flutter.git is not a standard remote.
Set environment variable "FLUTTER_GIT_URL" to [email protected]:gspencergoog/flutter.git to dismiss this error.
• Framework revision 71faeb3b12 (4 weeks ago), 2024-11-15 13:58:02 -0800
• Engine revision 619804c0fb
• Dart version 3.7.0 (build 3.7.0-140.0.dev)
• DevTools version 2.41.0-dev.2
• If those were intentional, you can disregard the above warnings; however it is recommended to use "git" directly to
perform update checks and upgrades.
[✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
• Android SDK at /Users/user/Library/Android/sdk
• Platform android-35, build-tools 34.0.0
• ANDROID_HOME = /Users/user/Library/Android/sdk
• Java binary at: /opt/homebrew/opt/openjdk/bin/java
• Java version OpenJDK Runtime Environment Homebrew (build 23.0.1)
• All Android licenses accepted.
[✓] Xcode - develop for iOS and macOS (Xcode 15.0.1)
• Xcode at /Applications/Xcode.app/Contents/Developer
• Build 15A507
• CocoaPods version 1.16.2
[✓] Chrome - develop for the web
• Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
[✓] Android Studio (version 2022.1)
• Android Studio at /Applications/Android Studio.app/Contents
• Flutter plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/9212-flutter
• Dart plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/6351-dart
• Java version OpenJDK Runtime Environment (build 11.0.15+0-b2043.56-8887301)
[✓] Android Studio (version 4.0)
• Android Studio at /Users/user/Library/Application
Support/JetBrains/Toolbox/apps/AndroidStudio/ch-0/193.6514223/Android Studio.app/Contents
• Flutter plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/9212-flutter
• Dart plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/6351-dart
• Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b3-6222593)
[✓] IntelliJ IDEA Community Edition (version 2022.1.3)
• IntelliJ at /Applications/IntelliJ IDEA CE.app
• Flutter plugin version 70.0.4
• Dart plugin version 221.5921.27
[✓] VS Code (version 1.95.3)
• VS Code at /Applications/Visual Studio Code.app/Contents
• Flutter extension version 3.102.0
[✓] Connected device (4 available)
• Pixel 7 Pro (mobile) • 2A281FDH3004PL • android-arm64 • Android 15 (API 35)
• macOS (desktop) • macos • darwin-arm64 • macOS 14.7.1 23H222 darwin-arm64
• Mac Designed for iPad (desktop) • mac-designed-for-ipad • darwin • macOS 14.7.1 23H222 darwin-arm64
• Chrome (web) • chrome • web-javascript • Google Chrome 131.0.6778.140
[✓] Network resources
• All expected network resources are available.
! Doctor found issues in 1 category.
Activity
maheshj01 commentedon Dec 13, 2024
Hi @gspencergoog, Thanks for filing the issue. Labelling as a valid proposal.
Container: true: Semantics are merged and user cannot navigate to them by swiping left and rightContainer: false: Semantic are not merged and user can navigate to prefix and suffix text with a Swipe.Also note that with Container: False the prefix and suffix text are not shown unless focused.
Unmute for audio
Screen.Recording.2024-12-13.at.18.43.23.mov
Screen.Recording.2024-12-13.at.18.37.52.mov
flutter doctor -v
maheshj01 commentedon Dec 14, 2024
Wondering would it make sense to also merge sematiclabels for prefixIcon and suffixIcon for textfields.
gspencergoog commentedon Dec 16, 2024
This is true regardless of the setting of container. Suffix and prefix text are only shown when the field is focused.
chunhtai commentedon Dec 16, 2024
We explicitly makes the prefix and suffix to produce separate nodes to match native behavior.
because these two nodes are its sibling, a simple mergesemantics won't be able to merge them. I think this is probably something we should fix.
7 remaining items
chunhtai commentedon Mar 16, 2026
just want to double check what the expected behavior is, if lets say you have something like this
This will generate three semantics nodes in this order (different from traversal order which determined by sort key)
If I were to fix it as is, with the merge semantics this will create a merged node looks like
I doubt this is fit your use case. especially value '123' won't merge in between the labels. I would like to confirm if this is the expected result before I move forward with the implementation
chunhtai commentedon Mar 16, 2026
perhaps instead of using mergesemantics, something like this may fit the use case better?
which would generate
If you don't want the addional prefix and suffix nodes, you can use
derekchandler6521-stack commentedon Mar 17, 2026
math1man commentedon Mar 17, 2026
I totally agree that
MergeSemanticsisn't really the right solution, it's just very confusing that it seems like it could be but doesn't actually work as expected.I think the ideal solution here actually is to provide a way to customize the
valueof the TextField's semantics node. The goal is to have the field read "3 feet" or "height 3 feet" instead of just "3" when the entered value is "3". So instead of generatingWe would want it to generate
or
Would it be possible to add a
String Function(String) valueSemanticsparameter toTextFieldthat allows us to customize theSemanticsNode.valuefield?chunhtai commentedon Mar 17, 2026
the value is directly fed into the textfield accessibility API as the current text in the textfield, it will affect talkback to calculate selection, copy, and other text editing features. appending text as part of the value may break text editing.
There are some android API that can rearrange the announcement. Let me create a separate issue for that. I will keep this one open as the mergesemantics bug still valid
math1man commentedon Mar 17, 2026
I see, thanks! If this is infeasible at the Android framework level, I can use that as justification to close our bug.
chunhtai commentedon Mar 17, 2026
filed #183808, please comment there if that is blocking the upstream issue
math1man commentedon Mar 23, 2026
Hmm, I don't think #183808 captures the issue my app is facing that I've described here.
Also, looking more into it, I think my problem is significantly lower-level. I'm unable to provide a semantic label for a
TextFieldat all. Wrapping it inSemantics(label: 'foo')doesn't do anything. I don't think thelabelfield of the decoration helps either, though our design does not include this field so that's beside the point. Filed #184012Update merge semantics logic to merge sibling nodes (flutter#183745)
github-actions commentedon May 13, 2026
This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of
flutter doctor -vand a minimal reproduction of the issue.