Skip to content

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

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 git@github.com:gspencergoog/flutter.git is not a standard remote.
      Set environment variable "FLUTTER_GIT_URL" to git@github.com: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.

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
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions