[A11y] Add radio group role (#164154)
This adds a new "radio group" accessibility role to `dart:ui` and the
Flutter web engine.
This does not update existing widgets to use this new role. Currently,
users must manually add a `Semantics` widget to use this accessibility
role. See the example app below.
Part of: https://github.com/flutter/flutter/issues/162093
## Example app
<details>
<summary>Example app that uses the radio group role...</summary>
```dart
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/semantics.dart';
void main() {
runApp(const RadioExampleApp());
SemanticsBinding.instance.ensureSemantics();
}
class RadioExampleApp extends StatelessWidget {
const RadioExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Radio Sample')),
body: const Center(child: RadioExample()),
),
);
}
}
class RadioExample extends StatefulWidget {
const RadioExample({super.key});
@override
State<RadioExample> createState() => _RadioExampleState();
}
class _RadioExampleState extends State<RadioExample> {
int _groupValue = 0;
@override
Widget build(BuildContext context) {
return Semantics(
label: 'Radio group',
role: SemanticsRole.radioGroup,
explicitChildNodes: true,
child: Column(
children: <Widget>[
ListTile(
title: const Text('Foo'),
leading: Radio<int>(
value: 0,
groupValue: _groupValue,
onChanged: (int? value) => setState(() => _groupValue = value ?? 0),
),
),
ListTile(
title: const Text('Bar'),
leading: Radio<int>(
value: 1,
groupValue: _groupValue,
onChanged: (int? value) => setState(() => _groupValue = value ?? 0),
),
),
],
),
);
}
}
```
</details>
<details>
<summary>Accessibility tree...</summary>
```
SemanticsNode#0
│ Rect.fromLTRB(0.0, 0.0, 1200.0, 924.0)
│
└─SemanticsNode#1
│ Rect.fromLTRB(0.0, 0.0, 1200.0, 924.0)
│ textDirection: ltr
│
└─SemanticsNode#2
│ Rect.fromLTRB(0.0, 0.0, 1200.0, 924.0)
│ sortKey: OrdinalSortKey#83a1d(order: 0.0)
│
└─SemanticsNode#3
│ Rect.fromLTRB(0.0, 0.0, 1200.0, 924.0)
│ flags: scopesRoute
│
├─SemanticsNode#9
│ │ Rect.fromLTRB(0.0, 0.0, 1200.0, 56.0)
│ │
│ └─SemanticsNode#10
│ Rect.fromLTRB(532.6, 14.0, 667.4, 42.0)
│ flags: isHeader
│ label: "Radio Sample"
│ textDirection: ltr
│
└─SemanticsNode#4
│ Rect.fromLTRB(0.0, 56.0, 1200.0, 924.0)
│ label: "Radio group"
│ textDirection: ltr
│ role: radioGroup
│
├─SemanticsNode#5
│ │ Rect.fromLTRB(0.0, 0.0, 1200.0, 48.0)
│ │ flags: hasSelectedState, hasEnabledState, isEnabled
│ │ label: "Foo"
│ │ textDirection: ltr
│ │
│ └─SemanticsNode#6
│ Rect.fromLTRB(16.0, 8.0, 48.0, 40.0)
│ actions: focus, tap
│ flags: hasCheckedState, hasSelectedState, hasEnabledState,
│ isEnabled, isInMutuallyExclusiveGroup, isFocusable
│
└─SemanticsNode#7
│ Rect.fromLTRB(0.0, 48.0, 1200.0, 96.0)
│ flags: hasSelectedState, hasEnabledState, isEnabled
│ label: "Bar"
│ textDirection: ltr
│
└─SemanticsNode#8
Rect.fromLTRB(16.0, 8.0, 48.0, 40.0)
actions: focus, tap
flags: hasCheckedState, isChecked, hasSelectedState, isSelected,
hasEnabledState, isEnabled, isInMutuallyExclusiveGroup,
isFocusable
```
</details>
<details>
<summary>HTML generated by Flutter web...</summary>
```html
<html>
...
<body flt-embedding="full-page" flt-renderer="canvaskit" flt-build-mode="debug" spellcheck="false" style="...">
...
<flt-announcement-host>
<flt-announcement-polite aria-live="polite" style="...">
<flt-announcement-assertive aria-live="assertive" style="...">
<flutter-view style="...">
<flt-glass-pane></flt-glass-pane>
<flt-text-editing-host></flt-text-editing-host>
<flt-semantics-host style="...">
<flt-semantics id="flt-semantic-node-0" style="...">
<flt-semantics-container style="...">
<flt-semantics id="flt-semantic-node-1" style="...">
<flt-semantics-container style="...">
<flt-semantics id="flt-semantic-node-2" style="...">
<flt-semantics-container style="...">
<flt-semantics id="flt-semantic-node-3" role="dialog" style="...">
<flt-semantics-container style="...">
<flt-semantics id="flt-semantic-node-9" style="...">
<flt-semantics-container style="...">
<h2 id="flt-semantic-node-10" tabindex="-1" style="...">
Radio Sample</h2>
</flt-semantics-container>
</flt-semantics>
<flt-semantics id="flt-semantic-node-4" role="radiogroup" aria-label="Radio group"
style="...">
<flt-semantics-container style="...">
<flt-semantics id="flt-semantic-node-5" role="group" aria-label="Foo"
aria-selected="false" style="...">
<flt-semantics-container style="...">
<flt-semantics id="flt-semantic-node-6" tabindex="0" flt-tappable="" role="radio"
aria-checked="false" style="...">
</flt-semantics>
</flt-semantics-container>
</flt-semantics>
<flt-semantics id="flt-semantic-node-7" role="group" aria-label="Bar"
aria-selected="false" style="...">
<flt-semantics-container style="...">
<flt-semantics id="flt-semantic-node-8" tabindex="0" flt-tappable="" role="radio"
aria-checked="true" style="...">
</flt-semantics>
</flt-semantics-container>
</flt-semantics>
</flt-semantics-container>
</flt-semantics>
</flt-semantics-container>
</flt-semantics>
</flt-semantics-container>
</flt-semantics>
</flt-semantics-container>
</flt-semantics>
</flt-semantics-container>
</flt-semantics>
</flt-semantics-host>
</body>
</html>
```
</details>
## Pre-launch Checklist
- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.
If you need help, consider asking for advice on the #hackers-new channel
on [Discord].
<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md