Skip to content

[accessibility] VoiceOver on toggleable Material widgets on iOS #121636

@TahaTesser

Description

@TahaTesser

Internal: b/275699766

While fixing #120650 and investigating #74963, I noticed that regular ListTile doesn't announce any action and other toggleable material widgets with voice-over doesn't sound right compared to Android.

Since these toggleable widgets are integrable parts of list tile-dependent widgets such as CheckboxListTile, SwitchListTile, RadioListTile, and ExpansionTile. I wanna make sure the hints are consistent on iOS for all these widgets.

Here I created an example that showcases toggleable Material widgets and their list tile variants.

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

void main() => runApp(const MyApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(useMaterial3: true),
      home: const Example(),
    );
  }
}

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

  @override
  State<Example> createState() => _ExampleState();
}

class _ExampleState extends State<Example> {
  bool toggle = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: SingleChildScrollView(
          child: Column(
            children: <Widget>[
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                children: <Widget>[
                  Column(
                    children: <Widget>[
                      const DescriptionText(text: 'Switch'),
                      Switch(
                        value: toggle,
                        onChanged: (bool value) {
                          setState(() {
                            toggle = !toggle;
                          });
                        },
                      ),
                    ],
                  ),
                  Column(
                    children: <Widget>[
                      const DescriptionText(text: 'Checkbox'),
                      Checkbox(
                        value: toggle,
                        onChanged: (bool? value) {
                          setState(() {
                            toggle = !toggle;
                          });
                        },
                      ),
                    ],
                  ),
                  Column(
                    children: <Widget>[
                      const DescriptionText(text: 'Radio'),
                      Radio<int>(
                        value: toggle ? 1 : 2,
                        groupValue: 1,
                        toggleable: true,
                        onChanged: (int? value) {
                          setState(() {
                            toggle = !toggle;
                          });
                        },
                      ),
                    ],
                  )
                ],
              ),
              const Divider(),
              const DescriptionText(text: 'ListTile'),
              ListTile(
                title: const Text('Headline'),
                trailing: const Icon(Icons.favorite),
                selected: toggle,
                onTap: () {
                  setState(() {
                    toggle = !toggle;
                  });
                },
              ),
              const Divider(),
              const DescriptionText(text: 'SwitchListTile'),
              SwitchListTile(
                title: const Text('Headline'),
                value: toggle,
                onChanged: (bool value) {
                  setState(() {
                    toggle = !toggle;
                  });
                },
              ),
              const Divider(),
              const DescriptionText(text: 'CheckboxListTile'),
              CheckboxListTile(
                title: const Text('Headline'),
                value: toggle,
                onChanged: (bool? value) {
                  setState(() {
                    toggle = !toggle;
                  });
                },
              ),
              const Divider(),
              const DescriptionText(text: 'RadioListTile'),
              RadioListTile<int>(
                title: const Text('Headline'),
                value: toggle ? 1 : 2,
                groupValue: 1,
                toggleable: true,
                onChanged: (int? value) {
                  setState(() {
                    toggle = !toggle;
                  });
                },
              ),
              const Divider(),
              const DescriptionText(text: 'ExpansionTile'),
              const ExpansionTile(
                title: Text('Headline'),
                children: <Widget>[FlutterLogo(size: 50)],
              ),
              const Divider(),
            ],
          ),
        ),
      ),
    );
  }
}

class DescriptionText extends StatelessWidget {
  const DescriptionText({super.key, required this.text});

  final String text;

  @override
  Widget build(BuildContext context) {
    return ExcludeSemantics(child: Text(text));
  }
}

Preview

IMG_2211.MP4
IMG_2212.MP4

Observations

Fix voiceover issues for the following widgets:

Metadata

Metadata

Assignees

Labels

P0Critical issues such as a build break or regressiona: accessibilityAccessibility, e.g. VoiceOver or TalkBack. (aka a11y)customer: chalk (g3)engineflutter/engine related. See also e: labels.f: material designflutter/packages/flutter/material repository.frameworkflutter/packages/flutter repository. See also f: labels.platform-iosiOS applications specificallywaiting for PR to land (fixed)A fix is in flight

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions