Skip to content

Flutter web - screen reader textfield typing issue #169128

Closed
@govarthananve

Description

@govarthananve

Steps to reproduce

  1. Enable screenreader
  2. Navigate to TextField and type value its announce the typing text but its not populate the value on textfield

Expected results

It should allow to type the text value

Actual results

Text input field does not work very well with screen reader. If screen reader is hovering, it sometimes says the user is typing but text is not actually coming up. This should be fixed so that, if a user is typing and the screen reader is reading it, the text should come up.

Code sample

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

void main() {
  runApp(const ScreenReaderIssueApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Screen Reader Issue Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const ScreenReaderIssuePage(),
    );
  }
}

class ScreenReaderIssuePage extends StatefulWidget {
  const ScreenReaderIssuePage({super.key});

  @override
  State<ScreenReaderIssuePage> createState() => _ScreenReaderIssuePageState();
}

class _ScreenReaderIssuePageState extends State<ScreenReaderIssuePage> {
  final TextEditingController _textController = TextEditingController();
  final FocusNode _focusNode = FocusNode();
  bool _isTyping = false;
  String _lastAnnounced = '';

  @override
  void initState() {
    super.initState();
    _textController.addListener(_onTextChanged);
    _focusNode.addListener(_onFocusChanged);
  }

  @override
  void dispose() {
    _textController.removeListener(_onTextChanged);
    _focusNode.removeListener(_onFocusChanged);
    _textController.dispose();
    _focusNode.dispose();
    super.dispose();
  }

  void _onFocusChanged() {
    setState(() {
      // Update UI based on focus changes
      if (_focusNode.hasFocus) {
        _isTyping = true;
      } else {
        _isTyping = false;
      }
    });
  }

  void _onTextChanged() {
    // This demonstrates the issue - sometimes the screen reader announces text
    // changes but the text doesn't actually update in the field
    if (_textController.text != _lastAnnounced && _focusNode.hasFocus) {
      _lastAnnounced = _textController.text;

      // Log for debugging
      debugPrint('Text changed to: ${_textController.text}');

      // In the actual issue, sometimes this text would be announced by the screen reader
      // but wouldn't appear in the field
      SchedulerBinding.instance.addPostFrameCallback((_) {
        // Simulate the issue where screen reader announces but text doesn't appear
        if (_isTyping) {
          SemanticsService.announce(
            'Typing: ${_textController.text}',
            TextDirection.ltr,
          );
        }
      });
    }
  }

  void _sendMessage() {
    if (_textController.text.trim().isNotEmpty) {
      setState(() {
        // In a real app, this would send the message
        debugPrint('Message sent: ${_textController.text}');
        _textController.clear();
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Screen Reader Issue Demo'),
      ),
      body: Column(
        children: [
          Expanded(
            child: Center(
              child: Text(
                'This demo reproduces the screen reader issue where the screen reader '
                'announces typing but text doesn\'t appear in the field.\n\n'
                'Current text: "${_textController.text}"',
                textAlign: TextAlign.center,
                style: const TextStyle(fontSize: 16),
              ),
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: AccessibleSearchBar(
              controller: _textController,
              focusNode: _focusNode,
              onSubmit: (_) => _sendMessage(),
              isTyping: _isTyping,
            ),
          ),
        ],
      ),
    );
  }
}

class AccessibleSearchBar extends StatefulWidget {
  final TextEditingController controller;
  final FocusNode focusNode;
  final Function(String) onSubmit;
  final bool isTyping;

  const AccessibleSearchBar({
    super.key,
    required this.controller,
    required this.focusNode,
    required this.onSubmit,
    required this.isTyping,
  });

  @override
  State<AccessibleSearchBar> createState() => _AccessibleSearchBarState();
}

class _AccessibleSearchBarState extends State<AccessibleSearchBar> {
  int _calculateLines(String text, double maxWidth) {
    if (text.isEmpty) return 1;

    final TextPainter textPainter = TextPainter(
      text: TextSpan(text: text, style: const TextStyle(fontSize: 16)),
      textDirection: TextDirection.ltr,
      maxLines: 5,
    )..layout(maxWidth: maxWidth);

    return textPainter.computeLineMetrics().length;
  }

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        final lines =
            _calculateLines(widget.controller.text, constraints.maxWidth - 80);
        final currentLines = lines.clamp(1, 5);

        return Container(
          padding: const EdgeInsets.symmetric(horizontal: 16.0),
          decoration: BoxDecoration(
            color: Colors.grey[200],
            border: Border.all(color: Colors.grey[400]!),
            borderRadius: BorderRadius.circular(8.0),
          ),
          child: Row(
            crossAxisAlignment: CrossAxisAlignment.end,
            children: [
              Expanded(
                child: Semantics(
                  textField: true,
                  // This is part of the reproduction of the issue
                  // The semantic properties don't always match the actual text field
                  label: 'Type your message here',
                  value: widget.controller.text,
                  hint: 'Type your message here',
                  onDidGainAccessibilityFocus: () {
                    // This helps demonstrate the issue where screen reader focus
                    // doesn't correctly sync with the input field
                    widget.focusNode.requestFocus();
                  },
                  child: TextFormField(
                    controller: widget.controller,
                    focusNode: widget.focusNode,
                    maxLines: currentLines,
                    minLines: 1,
                    style: const TextStyle(fontSize: 16),
                    decoration: const InputDecoration(
                      border: InputBorder.none,
                      hintText: 'Type your message',
                      contentPadding: EdgeInsets.symmetric(vertical: 12.0),
                    ),
                    onFieldSubmitted: widget.onSubmit,
                  ),
                ),
              ),
              if (widget.controller.text.trim().isNotEmpty)
                Padding(
                  padding: const EdgeInsets.only(bottom: 8.0),
                  child: ElevatedButton(
                    onPressed: () => widget.onSubmit(widget.controller.text),
                    style: ElevatedButton.styleFrom(
                      shape: const CircleBorder(),
                      padding: const EdgeInsets.all(12),
                    ),
                    child: const Icon(Icons.send),
                  ),
                ),
            ],
          ),
        );
      },
    );
  }
}

Screenshots or Video

Image
Screenshots / Video demonstration

[Upload media here]

Logs

Logs
[Paste your logs here]

Flutter Doctor output

Doctor output
flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.24.4, on macOS 15.5 24F74 darwin-arm64, locale
    en-IN)
[✗] Android toolchain - develop for Android devices
    ✗ Unable to locate Android SDK.
      Install Android Studio from:
      https://developer.android.com/studio/index.html
      On first launch it will assist you in installing the Android SDK
      components.
      (or visit https://flutter.dev/to/macos-android-setup for detailed
      instructions).
      If the Android SDK has been installed to a custom location, please use
      `flutter config --android-sdk` to update to that location.

[!] Xcode - develop for iOS and macOS (Xcode 16.2)
    ✗ CocoaPods not installed.
        CocoaPods is a package manager for iOS or macOS platform code.
        Without CocoaPods, plugins will not work on iOS or macOS.
        For more info, see https://flutter.dev/to/platform-plugins
      For installation instructions, see
      https://guides.cocoapods.org/using/getting-started.html#installation
[✓] Chrome - develop for the web
[✓] Android Studio (version 2024.2)
[✓] VS Code (version 1.100.2)
[✓] Connected device (3 available)
[✓] Network resources

! Doctor found issues in 2 categories.

Metadata

Metadata

Assignees

No one assigned

    Labels

    r: timeoutIssue is closed due to author not providing the requested details in time

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions