Unverified Commit a8b3d1b7 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Add toggleable attribute to Radio (#53846)

This adds a new toggleable attribute to the Radio widget. This allows a radio group to be set back to an indeterminate state if the selected radio button is selected again.

Fixes #53791
parent e97c385c
...@@ -40,8 +40,10 @@ bool _isRadioSelected(int index) => ...@@ -40,8 +40,10 @@ bool _isRadioSelected(int index) =>
List<Radio<Location>> get _radios => List<Radio<Location>>.from( List<Radio<Location>> get _radios => List<Radio<Location>>.from(
_radioFinder.evaluate().map<Widget>((Element e) => e.widget)); _radioFinder.evaluate().map<Widget>((Element e) => e.widget));
// [find.byType] and [find.widgetWithText] do not match subclasses; `Radio` is not sufficient to find a `Radio<_Location>`. // [find.byType] and [find.widgetWithText] do not match subclasses; `Radio` is
// Another approach is to grab the `runtimeType` of a dummy instance; see packages/flutter/test/material/control_list_tile_test.dart. // not sufficient to find a `Radio<_Location>`. Another approach is to grab the
// `runtimeType` of a dummy instance; see
// packages/flutter/test/material/radio_list_tile_test.dart.
Finder get _radioFinder => Finder get _radioFinder =>
find.byWidgetPredicate((Widget w) => w is Radio<Location>); find.byWidgetPredicate((Widget w) => w is Radio<Location>);
......
...@@ -108,6 +108,7 @@ class Radio<T> extends StatefulWidget { ...@@ -108,6 +108,7 @@ class Radio<T> extends StatefulWidget {
@required this.value, @required this.value,
@required this.groupValue, @required this.groupValue,
@required this.onChanged, @required this.onChanged,
this.toggleable = false,
this.activeColor, this.activeColor,
this.focusColor, this.focusColor,
this.hoverColor, this.hoverColor,
...@@ -116,6 +117,7 @@ class Radio<T> extends StatefulWidget { ...@@ -116,6 +117,7 @@ class Radio<T> extends StatefulWidget {
this.focusNode, this.focusNode,
this.autofocus = false, this.autofocus = false,
}) : assert(autofocus != null), }) : assert(autofocus != null),
assert(toggleable != null),
super(key: key); super(key: key);
/// The value represented by this radio button. /// The value represented by this radio button.
...@@ -155,6 +157,69 @@ class Radio<T> extends StatefulWidget { ...@@ -155,6 +157,69 @@ class Radio<T> extends StatefulWidget {
/// ``` /// ```
final ValueChanged<T> onChanged; final ValueChanged<T> onChanged;
/// Set to true if this radio button is allowed to be returned to an
/// indeterminate state by selecting it again when selected.
///
/// To indicate returning to an indeterminate state, [onChanged] will be
/// called with null.
///
/// If true, [onChanged] can be called with [value] when selected while
/// [groupValue] != [value], or with null when selected again while
/// [groupValue] == [value].
///
/// If false, [onChanged] will be called with [value] when it is selected
/// while [groupValue] != [value], and only by selecting another radio button
/// in the group (i.e. changing the value of [groupValue]) can this radio
/// button be unselected.
///
/// The default is false.
///
/// {@tool dartpad --template=stateful_widget_scaffold}
/// This example shows how to enable deselecting a radio button by setting the
/// [toggleable] attribute.
///
/// ```dart
/// int groupValue;
/// static const List<String> selections = <String>[
/// 'Hercules Mulligan',
/// 'Eliza Hamilton',
/// 'Philip Schuyler',
/// 'Maria Reynolds',
/// 'Samuel Seabury',
/// ];
///
/// @override
/// Widget build(BuildContext context) {
/// return Scaffold(
/// body: ListView.builder(
/// itemBuilder: (context, index) {
/// return Row(
/// mainAxisSize: MainAxisSize.min,
/// crossAxisAlignment: CrossAxisAlignment.center,
/// children: <Widget>[
/// Radio<int>(
/// value: index,
/// groupValue: groupValue,
/// // TRY THIS: Try setting the toggleable value to false and
/// // see how that changes the behavior of the widget.
/// toggleable: true,
/// onChanged: (int value) {
/// setState(() {
/// groupValue = value;
/// });
/// }),
/// Text(selections[index]),
/// ],
/// );
/// },
/// itemCount: selections.length,
/// ),
/// );
/// }
/// ```
/// {@end-tool}
final bool toggleable;
/// The color to use when this radio button is selected. /// The color to use when this radio button is selected.
/// ///
/// Defaults to [ThemeData.toggleableActiveColor]. /// Defaults to [ThemeData.toggleableActiveColor].
...@@ -207,7 +272,7 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin { ...@@ -207,7 +272,7 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin {
}; };
} }
void _actionHandler(FocusNode node, Intent intent){ void _actionHandler(FocusNode node, Intent intent) {
if (widget.onChanged != null) { if (widget.onChanged != null) {
widget.onChanged(widget.value); widget.onChanged(widget.value);
} }
...@@ -241,9 +306,14 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin { ...@@ -241,9 +306,14 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin {
} }
void _handleChanged(bool selected) { void _handleChanged(bool selected) {
if (selected) if (selected == null) {
widget.onChanged(null);
return;
}
if (selected) {
widget.onChanged(widget.value); widget.onChanged(widget.value);
} }
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
...@@ -276,6 +346,7 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin { ...@@ -276,6 +346,7 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin {
focusColor: widget.focusColor ?? themeData.focusColor, focusColor: widget.focusColor ?? themeData.focusColor,
hoverColor: widget.hoverColor ?? themeData.hoverColor, hoverColor: widget.hoverColor ?? themeData.hoverColor,
onChanged: enabled ? _handleChanged : null, onChanged: enabled ? _handleChanged : null,
toggleable: widget.toggleable,
additionalConstraints: additionalConstraints, additionalConstraints: additionalConstraints,
vsync: this, vsync: this,
hasFocus: _focused, hasFocus: _focused,
...@@ -297,6 +368,7 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -297,6 +368,7 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget {
@required this.hoverColor, @required this.hoverColor,
@required this.additionalConstraints, @required this.additionalConstraints,
this.onChanged, this.onChanged,
@required this.toggleable,
@required this.vsync, @required this.vsync,
@required this.hasFocus, @required this.hasFocus,
@required this.hovering, @required this.hovering,
...@@ -304,6 +376,7 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -304,6 +376,7 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget {
assert(activeColor != null), assert(activeColor != null),
assert(inactiveColor != null), assert(inactiveColor != null),
assert(vsync != null), assert(vsync != null),
assert(toggleable != null),
super(key: key); super(key: key);
final bool selected; final bool selected;
...@@ -314,6 +387,7 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -314,6 +387,7 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget {
final Color focusColor; final Color focusColor;
final Color hoverColor; final Color hoverColor;
final ValueChanged<bool> onChanged; final ValueChanged<bool> onChanged;
final bool toggleable;
final TickerProvider vsync; final TickerProvider vsync;
final BoxConstraints additionalConstraints; final BoxConstraints additionalConstraints;
...@@ -325,6 +399,7 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -325,6 +399,7 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget {
focusColor: focusColor, focusColor: focusColor,
hoverColor: hoverColor, hoverColor: hoverColor,
onChanged: onChanged, onChanged: onChanged,
tristate: toggleable,
vsync: vsync, vsync: vsync,
additionalConstraints: additionalConstraints, additionalConstraints: additionalConstraints,
hasFocus: hasFocus, hasFocus: hasFocus,
...@@ -340,6 +415,7 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -340,6 +415,7 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget {
..focusColor = focusColor ..focusColor = focusColor
..hoverColor = hoverColor ..hoverColor = hoverColor
..onChanged = onChanged ..onChanged = onChanged
..tristate = toggleable
..additionalConstraints = additionalConstraints ..additionalConstraints = additionalConstraints
..vsync = vsync ..vsync = vsync
..hasFocus = hasFocus ..hasFocus = hasFocus
...@@ -355,18 +431,19 @@ class _RenderRadio extends RenderToggleable { ...@@ -355,18 +431,19 @@ class _RenderRadio extends RenderToggleable {
Color focusColor, Color focusColor,
Color hoverColor, Color hoverColor,
ValueChanged<bool> onChanged, ValueChanged<bool> onChanged,
bool tristate,
BoxConstraints additionalConstraints, BoxConstraints additionalConstraints,
@required TickerProvider vsync, @required TickerProvider vsync,
bool hasFocus, bool hasFocus,
bool hovering, bool hovering,
}) : super( }) : super(
value: value, value: value,
tristate: false,
activeColor: activeColor, activeColor: activeColor,
inactiveColor: inactiveColor, inactiveColor: inactiveColor,
focusColor: focusColor, focusColor: focusColor,
hoverColor: hoverColor, hoverColor: hoverColor,
onChanged: onChanged, onChanged: onChanged,
tristate: tristate,
additionalConstraints: additionalConstraints, additionalConstraints: additionalConstraints,
vsync: vsync, vsync: vsync,
hasFocus: hasFocus, hasFocus: hasFocus,
......
...@@ -309,6 +309,7 @@ class RadioListTile<T> extends StatelessWidget { ...@@ -309,6 +309,7 @@ class RadioListTile<T> extends StatelessWidget {
@required this.value, @required this.value,
@required this.groupValue, @required this.groupValue,
@required this.onChanged, @required this.onChanged,
this.toggleable = false,
this.activeColor, this.activeColor,
this.title, this.title,
this.subtitle, this.subtitle,
...@@ -317,7 +318,9 @@ class RadioListTile<T> extends StatelessWidget { ...@@ -317,7 +318,9 @@ class RadioListTile<T> extends StatelessWidget {
this.secondary, this.secondary,
this.selected = false, this.selected = false,
this.controlAffinity = ListTileControlAffinity.platform, this.controlAffinity = ListTileControlAffinity.platform,
}) : assert(isThreeLine != null),
}) : assert(toggleable != null),
assert(isThreeLine != null),
assert(!isThreeLine || subtitle != null), assert(!isThreeLine || subtitle != null),
assert(selected != null), assert(selected != null),
assert(controlAffinity != null), assert(controlAffinity != null),
...@@ -361,6 +364,62 @@ class RadioListTile<T> extends StatelessWidget { ...@@ -361,6 +364,62 @@ class RadioListTile<T> extends StatelessWidget {
/// ``` /// ```
final ValueChanged<T> onChanged; final ValueChanged<T> onChanged;
/// Set to true if this radio list tile is allowed to be returned to an
/// indeterminate state by selecting it again when selected.
///
/// To indicate returning to an indeterminate state, [onChanged] will be
/// called with null.
///
/// If true, [onChanged] can be called with [value] when selected while
/// [groupValue] != [value], or with null when selected again while
/// [groupValue] == [value].
///
/// If false, [onChanged] will be called with [value] when it is selected
/// while [groupValue] != [value], and only by selecting another radio button
/// in the group (i.e. changing the value of [groupValue]) can this radio
/// list tile be unselected.
///
/// The default is false.
///
/// {@tool dartpad --template=stateful_widget_scaffold}
/// This example shows how to enable deselecting a radio button by setting the
/// [toggleable] attribute.
///
/// ```dart
/// int groupValue;
/// static const List<String> selections = <String>[
/// 'Hercules Mulligan',
/// 'Eliza Hamilton',
/// 'Philip Schuyler',
/// 'Maria Reynolds',
/// 'Samuel Seabury',
/// ];
///
/// @override
/// Widget build(BuildContext context) {
/// return Scaffold(
/// body: ListView.builder(
/// itemBuilder: (context, index) {
/// return RadioListTile<int>(
/// value: index,
/// groupValue: groupValue,
/// toggleable: true,
/// title: Text(selections[index]),
/// onChanged: (int value) {
/// setState(() {
/// groupValue = value;
/// });
/// },
/// );
/// },
/// itemCount: selections.length,
/// ),
/// );
/// }
/// ```
/// {@end-tool}
final bool toggleable;
/// The color to use when this radio button is selected. /// The color to use when this radio button is selected.
/// ///
/// Defaults to accent color of the current [Theme]. /// Defaults to accent color of the current [Theme].
...@@ -416,6 +475,7 @@ class RadioListTile<T> extends StatelessWidget { ...@@ -416,6 +475,7 @@ class RadioListTile<T> extends StatelessWidget {
value: value, value: value,
groupValue: groupValue, groupValue: groupValue,
onChanged: onChanged, onChanged: onChanged,
toggleable: toggleable,
activeColor: activeColor, activeColor: activeColor,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
); );
...@@ -442,7 +502,15 @@ class RadioListTile<T> extends StatelessWidget { ...@@ -442,7 +502,15 @@ class RadioListTile<T> extends StatelessWidget {
isThreeLine: isThreeLine, isThreeLine: isThreeLine,
dense: dense, dense: dense,
enabled: onChanged != null, enabled: onChanged != null,
onTap: onChanged != null && !checked ? () { onChanged(value); } : null, onTap: onChanged != null ? () {
if (toggleable && checked) {
onChanged(null);
return;
}
if (!checked) {
onChanged(value);
}
} : null,
selected: selected, selected: selected,
), ),
), ),
......
...@@ -2,13 +2,17 @@ ...@@ -2,13 +2,17 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import '../widgets/semantics_tester.dart'; import '../widgets/semantics_tester.dart';
Widget wrap({ Widget child }) { Widget wrap({Widget child}) {
return MediaQuery( return MediaQuery(
data: const MediaQueryData(), data: const MediaQueryData(),
child: Directionality( child: Directionality(
...@@ -19,7 +23,8 @@ Widget wrap({ Widget child }) { ...@@ -19,7 +23,8 @@ Widget wrap({ Widget child }) {
} }
void main() { void main() {
testWidgets('RadioListTile should initialize according to groupValue', (WidgetTester tester) async { testWidgets('RadioListTile should initialize according to groupValue',
(WidgetTester tester) async {
final List<int> values = <int>[0, 1, 2]; final List<int> values = <int>[0, 1, 2];
int selectedValue; int selectedValue;
// Constructor parameters are required for [RadioListTile], but they are // Constructor parameters are required for [RadioListTile], but they are
...@@ -47,7 +52,9 @@ void main() { ...@@ -47,7 +52,9 @@ void main() {
itemCount: values.length, itemCount: values.length,
itemBuilder: (BuildContext context, int index) => RadioListTile<int>( itemBuilder: (BuildContext context, int index) => RadioListTile<int>(
onChanged: (int value) { onChanged: (int value) {
setState(() { selectedValue = value; }); setState(() {
selectedValue = value;
});
}, },
value: values[index], value: values[index],
groupValue: selectedValue, groupValue: selectedValue,
...@@ -77,6 +84,78 @@ void main() { ...@@ -77,6 +84,78 @@ void main() {
expect(generatedRadioListTiles[2].checked, equals(false)); expect(generatedRadioListTiles[2].checked, equals(false));
}); });
testWidgets('RadioListTile simple control test', (WidgetTester tester) async {
final Key key = UniqueKey();
final Key titleKey = UniqueKey();
final List<int> log = <int>[];
await tester.pumpWidget(
wrap(
child: RadioListTile<int>(
key: key,
value: 1,
groupValue: 2,
onChanged: log.add,
title: Text('Title', key: titleKey),
),
),
);
await tester.tap(find.byKey(key));
expect(log, equals(<int>[1]));
log.clear();
await tester.pumpWidget(
wrap(
child: RadioListTile<int>(
key: key,
value: 1,
groupValue: 1,
onChanged: log.add,
activeColor: Colors.green[500],
title: Text('Title', key: titleKey),
),
),
);
await tester.tap(find.byKey(key));
expect(log, isEmpty);
await tester.pumpWidget(
wrap(
child: RadioListTile<int>(
key: key,
value: 1,
groupValue: 2,
onChanged: null,
title: Text('Title', key: titleKey),
),
),
);
await tester.tap(find.byKey(key));
expect(log, isEmpty);
await tester.pumpWidget(
wrap(
child: RadioListTile<int>(
key: key,
value: 1,
groupValue: 2,
onChanged: log.add,
title: Text('Title', key: titleKey),
),
),
);
await tester.tap(find.byKey(titleKey));
expect(log, equals(<int>[1]));
});
testWidgets('RadioListTile control tests', (WidgetTester tester) async { testWidgets('RadioListTile control tests', (WidgetTester tester) async {
final List<int> values = <int>[0, 1, 2]; final List<int> values = <int>[0, 1, 2];
int selectedValue; int selectedValue;
...@@ -99,7 +178,9 @@ void main() { ...@@ -99,7 +178,9 @@ void main() {
itemBuilder: (BuildContext context, int index) => RadioListTile<int>( itemBuilder: (BuildContext context, int index) => RadioListTile<int>(
onChanged: (int value) { onChanged: (int value) {
log.add(value); log.add(value);
setState(() { selectedValue = value; }); setState(() {
selectedValue = value;
});
}, },
value: values[index], value: values[index],
groupValue: selectedValue, groupValue: selectedValue,
...@@ -165,7 +246,9 @@ void main() { ...@@ -165,7 +246,9 @@ void main() {
itemBuilder: (BuildContext context, int index) => RadioListTile<int>( itemBuilder: (BuildContext context, int index) => RadioListTile<int>(
onChanged: (int value) { onChanged: (int value) {
log.add(value); log.add(value);
setState(() { selectedValue = value; }); setState(() {
selectedValue = value;
});
}, },
value: values[index], value: values[index],
groupValue: selectedValue, groupValue: selectedValue,
...@@ -184,6 +267,7 @@ void main() { ...@@ -184,6 +267,7 @@ void main() {
expect(log, equals(<int>[0])); expect(log, equals(<int>[0]));
await tester.tap(find.text('0')); await tester.tap(find.text('0'));
await tester.pump();
expect(log, equals(<int>[0])); expect(log, equals(<int>[0]));
await tester.tap(find.byType(radioType).at(0)); await tester.tap(find.byType(radioType).at(0));
...@@ -191,98 +275,301 @@ void main() { ...@@ -191,98 +275,301 @@ void main() {
expect(log, equals(<int>[0])); expect(log, equals(<int>[0]));
}); });
testWidgets('SwitchListTile control test', (WidgetTester tester) async { testWidgets('Selected RadioListTile should trigger onChanged when toggleable',
(WidgetTester tester) async {
final List<int> values = <int>[0, 1, 2];
int selectedValue;
// Constructor parameters are required for [Radio], but they are irrelevant
// when searching with [find.byType].
final Type radioType = const Radio<int>(
value: 0,
groupValue: 0,
onChanged: null,
).runtimeType;
final List<dynamic> log = <dynamic>[]; final List<dynamic> log = <dynamic>[];
await tester.pumpWidget(wrap(
child: SwitchListTile( Widget buildFrame() {
value: true, return wrap(
onChanged: (bool value) { log.add(value); }, child: StatefulBuilder(
title: const Text('Hello'), builder: (BuildContext context, StateSetter setState) {
return Scaffold(
body: ListView.builder(
itemCount: values.length,
itemBuilder: (BuildContext context, int index) {
return RadioListTile<int>(
onChanged: (int value) {
log.add(value);
setState(() {
selectedValue = value;
});
},
toggleable: true,
value: values[index],
groupValue: selectedValue,
title: Text(values[index].toString()),
);
},
),
);
},
),
);
}
await tester.pumpWidget(buildFrame());
await tester.tap(find.text('0'));
await tester.pump();
expect(log, equals(<int>[0]));
await tester.tap(find.text('0'));
await tester.pump();
expect(log, equals(<int>[0, null]));
await tester.tap(find.byType(radioType).at(0));
await tester.pump();
expect(log, equals(<int>[0, null, 0]));
});
testWidgets('RadioListTile can be toggled when toggleable is set', (WidgetTester tester) async {
final Key key = UniqueKey();
final List<int> log = <int>[];
await tester.pumpWidget(Material(
child: Center(
child: Radio<int>(
key: key,
value: 1,
groupValue: 2,
onChanged: log.add,
toggleable: true,
),
), ),
)); ));
await tester.tap(find.text('Hello'));
log.add('-'); await tester.tap(find.byKey(key));
await tester.tap(find.byType(Switch));
expect(log, equals(<dynamic>[false, '-', false])); expect(log, equals(<int>[1]));
log.clear();
await tester.pumpWidget(Material(
child: Center(
child: Radio<int>(
key: key,
value: 1,
groupValue: 1,
onChanged: log.add,
toggleable: true,
),
),
));
await tester.tap(find.byKey(key));
expect(log, equals(<int>[null]));
log.clear();
await tester.pumpWidget(Material(
child: Center(
child: Radio<int>(
key: key,
value: 1,
groupValue: null,
onChanged: log.add,
toggleable: true,
),
),
));
await tester.tap(find.byKey(key));
expect(log, equals(<int>[1]));
}); });
testWidgets('SwitchListTile control test', (WidgetTester tester) async { testWidgets('RadioListTile semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester); final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(wrap(
child: Column( await tester.pumpWidget(
children: <Widget>[ wrap(
SwitchListTile( child: RadioListTile<int>(
value: true, value: 1,
onChanged: (bool value) { }, groupValue: 2,
title: const Text('AAA'), onChanged: (int i) {},
secondary: const Text('aaa'), title: const Text('Title'),
),
CheckboxListTile(
value: true,
onChanged: (bool value) { },
title: const Text('BBB'),
secondary: const Text('bbb'),
),
RadioListTile<bool>(
value: true,
groupValue: false,
onChanged: (bool value) { },
title: const Text('CCC'),
secondary: const Text('ccc'),
), ),
],
), ),
)); );
// This test verifies that the label and the control get merged. expect(
expect(semantics, hasSemantics(TestSemantics.root( semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[ children: <TestSemantics>[
TestSemantics.rootChild( TestSemantics(
id: 1, id: 1,
rect: const Rect.fromLTWH(0.0, 0.0, 800.0, 56.0),
transform: null,
flags: <SemanticsFlag>[ flags: <SemanticsFlag>[
SemanticsFlag.hasCheckedState,
SemanticsFlag.hasEnabledState, SemanticsFlag.hasEnabledState,
SemanticsFlag.hasToggledState,
SemanticsFlag.isEnabled, SemanticsFlag.isEnabled,
SemanticsFlag.isInMutuallyExclusiveGroup,
SemanticsFlag.isFocusable, SemanticsFlag.isFocusable,
SemanticsFlag.isToggled,
], ],
actions: SemanticsAction.tap.index, actions: <SemanticsAction>[SemanticsAction.tap],
label: 'aaa\nAAA', label: 'Title',
textDirection: TextDirection.ltr,
),
],
), ),
TestSemantics.rootChild( ignoreRect: true,
id: 3, ignoreTransform: true,
rect: const Rect.fromLTWH(0.0, 0.0, 800.0, 56.0), ),
transform: Matrix4.translationValues(0.0, 56.0, 0.0), );
await tester.pumpWidget(
wrap(
child: RadioListTile<int>(
value: 2,
groupValue: 2,
onChanged: (int i) {},
title: const Text('Title'),
),
),
);
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
id: 1,
flags: <SemanticsFlag>[ flags: <SemanticsFlag>[
SemanticsFlag.hasCheckedState, SemanticsFlag.hasCheckedState,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isChecked, SemanticsFlag.isChecked,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled, SemanticsFlag.isEnabled,
SemanticsFlag.isInMutuallyExclusiveGroup,
SemanticsFlag.isFocusable, SemanticsFlag.isFocusable,
], ],
actions: SemanticsAction.tap.index, actions: <SemanticsAction>[SemanticsAction.tap],
label: 'bbb\nBBB', label: 'Title',
textDirection: TextDirection.ltr,
),
],
),
ignoreRect: true,
ignoreTransform: true,
),
);
await tester.pumpWidget(
wrap(
child: const RadioListTile<int>(
value: 1,
groupValue: 2,
onChanged: null,
title: Text('Title'),
),
), ),
TestSemantics.rootChild( );
id: 5,
rect: const Rect.fromLTWH(0.0, 0.0, 800.0, 56.0), expect(
transform: Matrix4.translationValues(0.0, 112.0, 0.0), semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
id: 1,
flags: <SemanticsFlag>[ flags: <SemanticsFlag>[
SemanticsFlag.hasCheckedState, SemanticsFlag.hasCheckedState,
SemanticsFlag.hasEnabledState, SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled, SemanticsFlag.isInMutuallyExclusiveGroup,
SemanticsFlag.isFocusable, SemanticsFlag.isFocusable,
],
label: 'Title',
textDirection: TextDirection.ltr,
),
],
),
ignoreId: true,
ignoreRect: true,
ignoreTransform: true,
),
);
await tester.pumpWidget(
wrap(
child: const RadioListTile<int>(
value: 2,
groupValue: 2,
onChanged: null,
title: Text('Title'),
),
),
);
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
id: 1,
flags: <SemanticsFlag>[
SemanticsFlag.hasCheckedState,
SemanticsFlag.isChecked,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isInMutuallyExclusiveGroup, SemanticsFlag.isInMutuallyExclusiveGroup,
], ],
actions: SemanticsAction.tap.index, label: 'Title',
label: 'CCC\nccc', textDirection: TextDirection.ltr,
), ),
], ],
))); ),
ignoreId: true,
ignoreRect: true,
ignoreTransform: true,
),
);
semantics.dispose(); semantics.dispose();
}); });
testWidgets('RadioListTile has semantic events', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final Key key = UniqueKey();
dynamic semanticEvent;
int radioValue = 2;
SystemChannels.accessibility.setMockMessageHandler((dynamic message) async {
semanticEvent = message;
});
await tester.pumpWidget(
wrap(
child: RadioListTile<int>(
key: key,
value: 1,
groupValue: radioValue,
onChanged: (int i) {
radioValue = i;
},
title: const Text('Title'),
),
),
);
await tester.tap(find.byKey(key));
await tester.pump();
final RenderObject object = tester.firstRenderObject(find.byKey(key));
expect(radioValue, 1);
expect(semanticEvent, <String, dynamic>{
'type': 'tap',
'nodeId': object.debugSemantics.id,
'data': <String, dynamic>{},
});
expect(object.debugSemantics.getSemanticsData().hasAction(SemanticsAction.tap), true);
semantics.dispose();
SystemChannels.accessibility.setMockMessageHandler(null);
});
} }
...@@ -66,6 +66,61 @@ void main() { ...@@ -66,6 +66,61 @@ void main() {
expect(log, isEmpty); expect(log, isEmpty);
}); });
testWidgets('Radio can be toggled when toggleable is set', (WidgetTester tester) async {
final Key key = UniqueKey();
final List<int> log = <int>[];
await tester.pumpWidget(Material(
child: Center(
child: Radio<int>(
key: key,
value: 1,
groupValue: 2,
onChanged: log.add,
toggleable: true,
),
),
));
await tester.tap(find.byKey(key));
expect(log, equals(<int>[1]));
log.clear();
await tester.pumpWidget(Material(
child: Center(
child: Radio<int>(
key: key,
value: 1,
groupValue: 1,
onChanged: log.add,
toggleable: true,
),
),
));
await tester.tap(find.byKey(key));
expect(log, equals(<int>[null]));
log.clear();
await tester.pumpWidget(Material(
child: Center(
child: Radio<int>(
key: key,
value: 1,
groupValue: null,
onChanged: log.add,
toggleable: true,
),
),
));
await tester.tap(find.byKey(key));
expect(log, equals(<int>[1]));
});
testWidgets('Radio size is configurable by ThemeData.materialTapTargetSize', (WidgetTester tester) async { testWidgets('Radio size is configurable by ThemeData.materialTapTargetSize', (WidgetTester tester) async {
final Key key1 = UniqueKey(); final Key key1 = UniqueKey();
await tester.pumpWidget( await tester.pumpWidget(
...@@ -443,7 +498,7 @@ void main() { ...@@ -443,7 +498,7 @@ void main() {
); );
}); });
testWidgets('Radio can be toggled by keyboard shortcuts', (WidgetTester tester) async { testWidgets('Radio can be controlled by keyboard shortcuts', (WidgetTester tester) async {
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
int groupValue = 1; int groupValue = 1;
const Key radioKey0 = Key('radio0'); const Key radioKey0 = Key('radio0');
......
...@@ -8,7 +8,113 @@ import 'package:flutter/material.dart'; ...@@ -8,7 +8,113 @@ import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart'; import '../rendering/mock_canvas.dart';
import '../widgets/semantics_tester.dart';
Widget wrap({ Widget child }) {
return MediaQuery(
data: const MediaQueryData(),
child: Directionality(
textDirection: TextDirection.ltr,
child: Material(child: child),
),
);
}
void main() { void main() {
testWidgets('SwitchListTile control test', (WidgetTester tester) async {
final List<dynamic> log = <dynamic>[];
await tester.pumpWidget(wrap(
child: SwitchListTile(
value: true,
onChanged: (bool value) { log.add(value); },
title: const Text('Hello'),
),
));
await tester.tap(find.text('Hello'));
log.add('-');
await tester.tap(find.byType(Switch));
expect(log, equals(<dynamic>[false, '-', false]));
});
testWidgets('SwitchListTile control test', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(wrap(
child: Column(
children: <Widget>[
SwitchListTile(
value: true,
onChanged: (bool value) { },
title: const Text('AAA'),
secondary: const Text('aaa'),
),
CheckboxListTile(
value: true,
onChanged: (bool value) { },
title: const Text('BBB'),
secondary: const Text('bbb'),
),
RadioListTile<bool>(
value: true,
groupValue: false,
onChanged: (bool value) { },
title: const Text('CCC'),
secondary: const Text('ccc'),
),
],
),
));
// This test verifies that the label and the control get merged.
expect(semantics, hasSemantics(TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
id: 1,
rect: const Rect.fromLTWH(0.0, 0.0, 800.0, 56.0),
transform: null,
flags: <SemanticsFlag>[
SemanticsFlag.hasEnabledState,
SemanticsFlag.hasToggledState,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
SemanticsFlag.isToggled,
],
actions: SemanticsAction.tap.index,
label: 'aaa\nAAA',
),
TestSemantics.rootChild(
id: 3,
rect: const Rect.fromLTWH(0.0, 0.0, 800.0, 56.0),
transform: Matrix4.translationValues(0.0, 56.0, 0.0),
flags: <SemanticsFlag>[
SemanticsFlag.hasCheckedState,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isChecked,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
actions: SemanticsAction.tap.index,
label: 'bbb\nBBB',
),
TestSemantics.rootChild(
id: 5,
rect: const Rect.fromLTWH(0.0, 0.0, 800.0, 56.0),
transform: Matrix4.translationValues(0.0, 112.0, 0.0),
flags: <SemanticsFlag>[
SemanticsFlag.hasCheckedState,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
SemanticsFlag.isInMutuallyExclusiveGroup,
],
actions: SemanticsAction.tap.index,
label: 'CCC\nccc',
),
],
)));
semantics.dispose();
});
testWidgets('SwitchListTile has the right colors', (WidgetTester tester) async { testWidgets('SwitchListTile has the right colors', (WidgetTester tester) async {
bool value = false; bool value = false;
await tester.pumpWidget( await tester.pumpWidget(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment