Skip to content

[material_ui] SearchAnchor and SearchBar creates problematic semantics node #190884

Closed
flutter/packages
#12680

Description

@chunhtai

Steps to reproduce

  1. run the example and dump the semantics tree
SemanticsNode#0
 │ Rect.fromLTRB(0.0, 0.0, 800.0, 596.0)
 │
 └─SemanticsNode#1
   │ Rect.fromLTRB(0.0, 0.0, 800.0, 596.0)
   │ textDirection: ltr
   │
   └─SemanticsNode#2
     │ Rect.fromLTRB(0.0, 0.0, 800.0, 596.0)
     │ sortKey: OrdinalSortKey#d666d(order: 0.0)
     │
     └─SemanticsNode#3
       │ Rect.fromLTRB(0.0, 0.0, 800.0, 596.0)
       │ flags: scopesRoute
       │
       ├─SemanticsNode#12
       │ │ Rect.fromLTRB(0.0, 0.0, 800.0, 56.0)
       │ │
       │ └─SemanticsNode#13
       │     Rect.fromLTRB(346.8, 14.0, 453.2, 42.0)
       │     flags: isHeader
       │     label: "title Demo"
       │     textDirection: ltr
       │     headingLevel: 1
       │
       └─SemanticsNode#4
         │ Rect.fromLTRB(0.0, 56.0, 800.0, 596.0)
         │
         └─SemanticsNode#11
           │ Rect.fromLTRB(0.0, 0.0, 800.0, 540.0)
           │ flags: hasImplicitScrolling
           │ scrollChildren: 2
           │ scrollIndex: 0
           │ scrollExtentMin: 0.0
           │ scrollPosition: 0.0
           │ scrollExtentMax: 0.0
           │
           ├─SemanticsNode#5
           │ │ Rect.fromLTRB(0.0, 0.0, 800.0, 56.0)
           │ │ tags: RenderViewport.twoPane
           │ │ actions: tap
           │ │ label: "Enabled search bar"
           │ │ textDirection: ltr
           │ │ indexInParent: 0
           │ │
           │ └─SemanticsNode#6
           │   │ Rect.fromLTRB(0.0, 0.0, 800.0, 56.0)
           │   │ actions: focus, tap
           │   │ flags: isFocusable
           │   │
           │   ├─SemanticsNode#7
           │   │   Rect.fromLTRB(40.0, 16.0, 744.0, 40.0)
           │   │   actions: didGainAccessibilityFocus, didLoseAccessibilityFocus,
           │   │     focus, tap
           │   │   flags: isTextField, hasEnabledState, isEnabled, isFocusable
           │   │   label: "Search..."
           │   │   textDirection: ltr
           │   │   currentValueLength: 0
           │   │   inputType: search
           │   │
           │   └─SemanticsNode#8
           │       Rect.fromLTRB(752.0, 8.0, 792.0, 48.0)
           │       actions: focus, tap
           │       flags: isButton, hasEnabledState, isEnabled, isFocusable
           │       traversalParentIdentifier: _OverlayPortalState#f0e9b
           │       tooltip: "Clear"
           │       textDirection: ltr
           │
           └─SemanticsNode#9
             │ Rect.fromLTRB(0.0, 56.0, 800.0, 112.0)
             │ tags: RenderViewport.twoPane
             │ flags: isFocusable
             │ label: "Disabled search bar"
             │ textDirection: ltr
             │ indexInParent: 1
             │
             └─SemanticsNode#10
                 Rect.fromLTRB(0.0, 0.0, 800.0, 56.0)
                 actions: didGainAccessibilityFocus, didLoseAccessibilityFocus,
                   tap🚫️
                 flags: isTextField, hasEnabledState, isReadOnly, isFocusable
                 label: "Search..."
                 textDirection: ltr
                 currentValueLength: 0
                 inputType: search


Expected results

there should be a way to assigne label to node 6, or eliminate the node 6 because they are likely representing the same tap action

Actual results

no way to fix the labeltaptarget guideline violation because every node that has tap action must have a label

Code sample

Code sample
import 'package:flutter/material.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized().ensureSemantics();
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(home: _MainWidget());
  }
}

class _MainWidget extends StatefulWidget {
  @override
  State<_MainWidget> createState() => _MainWidgetState();
}

class _MainWidgetState extends State<_MainWidget> {
  final String pageTitle = 'title';
  String _statusMessage = '';

  static const List<String> _kOptions = <String>[
    'apple',
    'banana',
    'cherry',
    'date',
    'elderberry',
    'fig',
    'grape',
    'honeydew',
    'icicle',
    'jackfruit',
    'kiwi',
    'lemon',
    'mango',
    'nectarine',
    'orange',
    'papaya',
    'quince',
    'raspberry',
    'strawberry',
    'tangerine',
    'umbrella',
    'vanilla',
    'watermelon',
    'xylophone',
    'yuzu',
    'zucchini',
  ];

  void _handleSearch(String value) {
    setState(() {
      _statusMessage = 'Searched for "$value"';
    });
    ScaffoldMessenger.of(
      context,
    ).showSnackBar(SnackBar(content: Text('Searched for "$value"')));
  }

  Iterable<Widget> _getSuggestions(SearchController controller) {
    final String input = controller.value.text;
    return _kOptions
        .where((String option) => option.contains(input.toLowerCase()))
        .map(
          (String filteredOption) => ListTile(
            title: Text(filteredOption),
            onTap: () {
              controller.closeView(filteredOption);
              _handleSearch(filteredOption);
            },
          ),
        );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Semantics(headingLevel: 1, child: Text('$pageTitle Demo')),
      ),
      body: ListView(
        children: <Widget>[
          Semantics(
            label: 'Enabled search bar',
            child: SearchAnchor(
              builder: (BuildContext context, SearchController controller) {
                return SearchBar(
                  key: const Key('enabled search bar'),
                  controller: controller,
                  hintText: 'Search...',
                  onTap: () {
                    controller.openView();
                  },
                  onChanged: (_) {
                    controller.openView();
                  },
                  onSubmitted: (String value) {
                    _handleSearch(value);
                  },
                  leading: const Icon(Icons.search),
                  trailing: <Widget>[
                    IconButton(
                      tooltip: 'Clear',
                      icon: const Icon(Icons.clear),
                      onPressed: () {
                        controller.clear();
                        setState(() {
                          _statusMessage = 'Search cleared';
                        });
                      },
                    ),
                  ],
                );
              },
              suggestionsBuilder:
                  (BuildContext context, SearchController controller) {
                    return _getSuggestions(controller);
                  },
            ),
          ),
          Semantics(
            label: 'Disabled search bar',
            child: const SearchBar(
              key: Key('disabled search bar'),
              hintText: 'Search...',
              enabled: false,
            ),
          ),
          if (_statusMessage.isNotEmpty)
            Semantics(
              liveRegion: true,
              child: Padding(
                padding: const EdgeInsets.all(8.0),
                child: Text(_statusMessage, key: const Key('status message')),
              ),
            ),
        ],
      ),
    );
  }
}

Screenshots or Video

Screenshots / Video demonstration

[Upload media here]

Logs

Logs
[Paste your logs here]

Flutter Doctor output

Doctor output
[Paste your output here]

Activity

  1. self-assigned this
    on Aug 10, 2026
  2. added
    P2Important issues not at the top of the work list
    team-designOwned by Design Languages team
    triaged-designTriaged by Design Languages team
    on Aug 10, 2026
  3. github-actions commented on Nov 2, 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.

  4. locked as resolved and limited conversation to collaborators on Nov 2, 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 listteam-designOwned by Design Languages teamtriaged-designTriaged by Design Languages team

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions