Skip to content

InputDecorator prefix and suffix text isn't included in semantics when TextField is wrapped in MergeSemantics. #160281

Closed

Description

@gspencergoog

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

  1. maheshj01 commented on Dec 13, 2024

    @maheshj01
    Member

    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 right
    Container: 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

    Container: false Container: true
    Screen.Recording.2024-12-13.at.18.43.23.mov
    Screen.Recording.2024-12-13.at.18.37.52.mov
    flutter doctor -v
    [✓] Flutter (Channel stable, 3.27.0, on macOS 15.1.1 24B2091 darwin-arm64, locale en-US)
        • Flutter version 3.27.0 on channel stable at /Users/mahesh/Development/flutter
        • Upstream repository https://github.com/flutter/flutter.git
        • Framework revision 8495dee1fd (3 days ago), 2024-12-10 14:23:39 -0800
        • Engine revision 83bacfc525
        • Dart version 3.6.0
        • DevTools version 2.40.2
    
    [✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
        • Android SDK at /Users/mahesh/Library/Android/sdk
        • Platform android-35, build-tools 34.0.0
        • ANDROID_HOME = /Users/mahesh/Library/Android/sdk
        • Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java
        • Java version OpenJDK Runtime Environment (build 17.0.11+0-17.0.11b1207.24-11852314)
        • All Android licenses accepted.
    
    [✓] Xcode - develop for iOS and macOS (Xcode 16.0)
        • Xcode at /Applications/Xcode.app/Contents/Developer
        • Build 16A242d
        • CocoaPods version 1.16.2
    
    [✓] Chrome - develop for the web
        • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
    
    [✓] Android Studio (version 2024.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 17.0.11+0-17.0.11b1207.24-11852314)
    
    [✓] IntelliJ IDEA Community Edition (version 2021.2.1)
        • IntelliJ at /Applications/IntelliJ IDEA CE.app
        • Flutter plugin version 61.2.4
        • Dart plugin version 212.5080.8
    
    [✓] VS Code (version 1.94.0)
        • VS Code at /Applications/Visual Studio Code.app/Contents
        • Flutter extension version 3.100.0
    
    [✓] Connected device (3 available)
        • macOS (desktop)                 • macos                 • darwin-arm64   • macOS 15.1.1 24B2091 darwin-arm64
        • Mac Designed for iPad (desktop) • mac-designed-for-ipad • darwin         • macOS 15.1.1 24B2091 darwin-arm64
        • Chrome (web)                    • chrome                • web-javascript • Google Chrome 128.0.6613.85
    
    [✓] Network resources
        • All expected network resources are available.
    
    • No issues found!
    
  2. added
    c: proposalA detailed proposal for a change to Flutter
    and removed
    in triagePresently being triaged by the triage team
    on Dec 13, 2024
  3. maheshj01 commented on Dec 14, 2024

    @maheshj01
    Member

    Wondering would it make sense to also merge sematiclabels for prefixIcon and suffixIcon for textfields.

  4. gspencergoog commented on Dec 16, 2024

    @gspencergoog
    ContributorAuthor

    Also note that with Container: False the prefix and suffix text are not shown unless focused.

    This is true regardless of the setting of container. Suffix and prefix text are only shown when the field is focused.

  5. chunhtai commented on Dec 16, 2024

    @chunhtai
    Contributor

    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.

  6. added
    team-accessibilityOwned by Framework Accessibility team (i.e. responsible for accessibility code in flutter/flutter)
    and removed on Dec 16, 2024
  7. added
    P2Important issues not at the top of the work list
    on Jan 9, 2025
  8. 7 remaining items

  9. chunhtai commented on Mar 16, 2026

    @chunhtai
    Contributor

    just want to double check what the expected behavior is, if lets say you have something like this

            Semantics(
                label: 'Room Height',
                child: TextField(
                  controller: TextEditingController('123'),
                  keyboardType: TextInputType.text,
                  decoration: const InputDecoration(
                    prefixText: 'height',
                    suffixText: 'feet',
                  ),
                ),
              ),

    This will generate three semantics nodes in this order (different from traversal order which determined by sort key)

    SemanticsNode(label: 'height', sortKey: 1),
    SemanticsNode(label: 'feet', sortKey: 3),
    SemanticsNode(label: 'Room Height', value: '123', isTextField: true, sortKey:2),

    If I were to fix it as is, with the merge semantics this will create a merged node looks like

    Semantics(label: 'height\nfeet\nRoom Height', value: '123', isTextField: true)

    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

  10. chunhtai commented on Mar 16, 2026

    @chunhtai
    Contributor

    perhaps instead of using mergesemantics, something like this may fit the use case better?

    final controller =TextEditingController('123')
    final prefix = 'height';
    final suffix = 'feet';
            Semantics(
                label: '$prefix, ${controller.text}, $suffix',
                child: TextField(
                  controller: TextEditingController('123'),
                  keyboardType: TextInputType.text,
                  decoration: const InputDecoration(
                    prefixText: prefix,
                    suffixText: suffix,
                  ),
                ),
              ),
    

    which would generate

    SemanticsNode(label: 'height', sortKey: 1),
    SemanticsNode(label: 'feet', sortKey: 3),
    SemanticsNode(label: 'height, 123, feet', value: '123', isTextField: true, sortKey:2),

    If you don't want the addional prefix and suffix nodes, you can use

                  decoration: const InputDecoration(
                    prefix: ExcludeSemantics(Text(prefix)),
                    suffix: ExcludeSemantics(Text(suffix)),
                  ),
  11. derekchandler6521-stack commented on Mar 17, 2026

    @derekchandler6521-stack
  12. math1man commented on Mar 17, 2026

    @math1man
    Contributor

    I totally agree that MergeSemantics isn'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 value of 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 generating

    SemanticsNode(label: 'height', sortKey: 1),
    SemanticsNode(label: 'feet', sortKey: 3),
    SemanticsNode(label: 'Room Height', value: '123', isTextField: true, sortKey:2),

    We would want it to generate

    SemanticsNode(label: 'Room Height', value: '123 feet', isTextField: true, sortKey:2),

    or

    SemanticsNode(label: 'Room Height', value: 'height 123 feet', isTextField: true, sortKey:2),

    Would it be possible to add a String Function(String) valueSemantics parameter to TextField that allows us to customize the SemanticsNode.value field?

  13. chunhtai commented on Mar 17, 2026

    @chunhtai
    Contributor

    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

  14. math1man commented on Mar 17, 2026

    @math1man
    Contributor

    I see, thanks! If this is infeasible at the Android framework level, I can use that as justification to close our bug.

  15. chunhtai commented on Mar 17, 2026

    @chunhtai
    Contributor

    filed #183808, please comment there if that is blocking the upstream issue

  16. added
    P2Important issues not at the top of the work list
    and removed
    P1High-priority issues at the top of the work list
    on Mar 17, 2026
  17. math1man commented on Mar 23, 2026

    @math1man
    Contributor

    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 TextField at all. Wrapping it in Semantics(label: 'foo') doesn't do anything. I don't think the label field of the decoration helps either, though our design does not include this field so that's beside the point. Filed #184012

  18. github-actions commented on May 13, 2026

    @github-actions

    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 -v and a minimal reproduction of the issue.

  19. locked as resolved and limited conversation to collaborators on May 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

P2Important issues not at the top of the work lista: accessibilityAccessibility, e.g. VoiceOver or TalkBack. (aka a11y)c: proposalA detailed proposal for a change to Fluttercustomer: huggsy (g3)customer: quake (g3)frameworkflutter/packages/flutter repository. See also f: labels.team-accessibilityOwned by Framework Accessibility team (i.e. responsible for accessibility code in flutter/flutter)triaged-accessibilityTriaged by Framework Accessibility team

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions