mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
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
This commit is contained in:
parent
e97c385c1b
commit
a8b3d1b74f
@ -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 {
|
|||||||
@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 {
|
|||||||
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 {
|
|||||||
/// ```
|
/// ```
|
||||||
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 {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
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,8 +306,13 @@ 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
|
||||||
@ -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 {
|
|||||||
@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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
..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 {
|
|||||||
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 {
|
|||||||
@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 {
|
|||||||
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 {
|
|||||||
/// ```
|
/// ```
|
||||||
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 {
|
|||||||
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 {
|
|||||||
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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -1,288 +0,0 @@
|
|||||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style license that can be
|
|
||||||
// found in the LICENSE file.
|
|
||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/rendering.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() {
|
|
||||||
testWidgets('RadioListTile should initialize according to groupValue', (WidgetTester tester) async {
|
|
||||||
final List<int> values = <int>[0, 1, 2];
|
|
||||||
int selectedValue;
|
|
||||||
// Constructor parameters are required for [RadioListTile], but they are
|
|
||||||
// irrelevant when searching with [find.byType].
|
|
||||||
final Type radioListTileType = const RadioListTile<int>(
|
|
||||||
value: 0,
|
|
||||||
groupValue: 0,
|
|
||||||
onChanged: null,
|
|
||||||
).runtimeType;
|
|
||||||
|
|
||||||
List<RadioListTile<int>> generatedRadioListTiles;
|
|
||||||
List<RadioListTile<int>> findTiles() => find
|
|
||||||
.byType(radioListTileType)
|
|
||||||
.evaluate()
|
|
||||||
.map<Widget>((Element element) => element.widget)
|
|
||||||
.cast<RadioListTile<int>>()
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
Widget buildFrame() {
|
|
||||||
return wrap(
|
|
||||||
child: StatefulBuilder(
|
|
||||||
builder: (BuildContext context, StateSetter setState) {
|
|
||||||
return Scaffold(
|
|
||||||
body: ListView.builder(
|
|
||||||
itemCount: values.length,
|
|
||||||
itemBuilder: (BuildContext context, int index) => RadioListTile<int>(
|
|
||||||
onChanged: (int value) {
|
|
||||||
setState(() { selectedValue = value; });
|
|
||||||
},
|
|
||||||
value: values[index],
|
|
||||||
groupValue: selectedValue,
|
|
||||||
title: Text(values[index].toString()),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await tester.pumpWidget(buildFrame());
|
|
||||||
generatedRadioListTiles = findTiles();
|
|
||||||
|
|
||||||
expect(generatedRadioListTiles[0].checked, equals(false));
|
|
||||||
expect(generatedRadioListTiles[1].checked, equals(false));
|
|
||||||
expect(generatedRadioListTiles[2].checked, equals(false));
|
|
||||||
|
|
||||||
selectedValue = 1;
|
|
||||||
|
|
||||||
await tester.pumpWidget(buildFrame());
|
|
||||||
generatedRadioListTiles = findTiles();
|
|
||||||
|
|
||||||
expect(generatedRadioListTiles[0].checked, equals(false));
|
|
||||||
expect(generatedRadioListTiles[1].checked, equals(true));
|
|
||||||
expect(generatedRadioListTiles[2].checked, equals(false));
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('RadioListTile control tests', (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>[];
|
|
||||||
|
|
||||||
Widget buildFrame() {
|
|
||||||
return wrap(
|
|
||||||
child: StatefulBuilder(
|
|
||||||
builder: (BuildContext context, StateSetter setState) {
|
|
||||||
return Scaffold(
|
|
||||||
body: ListView.builder(
|
|
||||||
itemCount: values.length,
|
|
||||||
itemBuilder: (BuildContext context, int index) => RadioListTile<int>(
|
|
||||||
onChanged: (int value) {
|
|
||||||
log.add(value);
|
|
||||||
setState(() { selectedValue = value; });
|
|
||||||
},
|
|
||||||
value: values[index],
|
|
||||||
groupValue: selectedValue,
|
|
||||||
title: Text(values[index].toString()),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests for tapping between [Radio] and [ListTile]
|
|
||||||
await tester.pumpWidget(buildFrame());
|
|
||||||
await tester.tap(find.text('1'));
|
|
||||||
log.add('-');
|
|
||||||
await tester.tap(find.byType(radioType).at(2));
|
|
||||||
expect(log, equals(<dynamic>[1, '-', 2]));
|
|
||||||
log.add('-');
|
|
||||||
await tester.tap(find.text('1'));
|
|
||||||
|
|
||||||
log.clear();
|
|
||||||
selectedValue = null;
|
|
||||||
|
|
||||||
// Tests for tapping across [Radio]s exclusively
|
|
||||||
await tester.pumpWidget(buildFrame());
|
|
||||||
await tester.tap(find.byType(radioType).at(1));
|
|
||||||
log.add('-');
|
|
||||||
await tester.tap(find.byType(radioType).at(2));
|
|
||||||
expect(log, equals(<dynamic>[1, '-', 2]));
|
|
||||||
|
|
||||||
log.clear();
|
|
||||||
selectedValue = null;
|
|
||||||
|
|
||||||
// Tests for tapping across [ListTile]s exclusively
|
|
||||||
await tester.pumpWidget(buildFrame());
|
|
||||||
await tester.tap(find.text('1'));
|
|
||||||
log.add('-');
|
|
||||||
await tester.tap(find.text('2'));
|
|
||||||
expect(log, equals(<dynamic>[1, '-', 2]));
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('Selected RadioListTile should not trigger onChanged', (WidgetTester tester) async {
|
|
||||||
// Regression test for https://github.com/flutter/flutter/issues/30311
|
|
||||||
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>[];
|
|
||||||
|
|
||||||
Widget buildFrame() {
|
|
||||||
return wrap(
|
|
||||||
child: StatefulBuilder(
|
|
||||||
builder: (BuildContext context, StateSetter setState) {
|
|
||||||
return Scaffold(
|
|
||||||
body: ListView.builder(
|
|
||||||
itemCount: values.length,
|
|
||||||
itemBuilder: (BuildContext context, int index) => RadioListTile<int>(
|
|
||||||
onChanged: (int value) {
|
|
||||||
log.add(value);
|
|
||||||
setState(() { selectedValue = value; });
|
|
||||||
},
|
|
||||||
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'));
|
|
||||||
expect(log, equals(<int>[0]));
|
|
||||||
|
|
||||||
await tester.tap(find.byType(radioType).at(0));
|
|
||||||
await tester.pump();
|
|
||||||
expect(log, equals(<int>[0]));
|
|
||||||
});
|
|
||||||
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
575
packages/flutter/test/material/radio_list_tile_test.dart
Normal file
575
packages/flutter/test/material/radio_list_tile_test.dart
Normal file
@ -0,0 +1,575 @@
|
|||||||
|
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// 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/material.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() {
|
||||||
|
testWidgets('RadioListTile should initialize according to groupValue',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final List<int> values = <int>[0, 1, 2];
|
||||||
|
int selectedValue;
|
||||||
|
// Constructor parameters are required for [RadioListTile], but they are
|
||||||
|
// irrelevant when searching with [find.byType].
|
||||||
|
final Type radioListTileType = const RadioListTile<int>(
|
||||||
|
value: 0,
|
||||||
|
groupValue: 0,
|
||||||
|
onChanged: null,
|
||||||
|
).runtimeType;
|
||||||
|
|
||||||
|
List<RadioListTile<int>> generatedRadioListTiles;
|
||||||
|
List<RadioListTile<int>> findTiles() => find
|
||||||
|
.byType(radioListTileType)
|
||||||
|
.evaluate()
|
||||||
|
.map<Widget>((Element element) => element.widget)
|
||||||
|
.cast<RadioListTile<int>>()
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
Widget buildFrame() {
|
||||||
|
return wrap(
|
||||||
|
child: StatefulBuilder(
|
||||||
|
builder: (BuildContext context, StateSetter setState) {
|
||||||
|
return Scaffold(
|
||||||
|
body: ListView.builder(
|
||||||
|
itemCount: values.length,
|
||||||
|
itemBuilder: (BuildContext context, int index) => RadioListTile<int>(
|
||||||
|
onChanged: (int value) {
|
||||||
|
setState(() {
|
||||||
|
selectedValue = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
value: values[index],
|
||||||
|
groupValue: selectedValue,
|
||||||
|
title: Text(values[index].toString()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await tester.pumpWidget(buildFrame());
|
||||||
|
generatedRadioListTiles = findTiles();
|
||||||
|
|
||||||
|
expect(generatedRadioListTiles[0].checked, equals(false));
|
||||||
|
expect(generatedRadioListTiles[1].checked, equals(false));
|
||||||
|
expect(generatedRadioListTiles[2].checked, equals(false));
|
||||||
|
|
||||||
|
selectedValue = 1;
|
||||||
|
|
||||||
|
await tester.pumpWidget(buildFrame());
|
||||||
|
generatedRadioListTiles = findTiles();
|
||||||
|
|
||||||
|
expect(generatedRadioListTiles[0].checked, equals(false));
|
||||||
|
expect(generatedRadioListTiles[1].checked, equals(true));
|
||||||
|
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 {
|
||||||
|
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>[];
|
||||||
|
|
||||||
|
Widget buildFrame() {
|
||||||
|
return wrap(
|
||||||
|
child: StatefulBuilder(
|
||||||
|
builder: (BuildContext context, StateSetter setState) {
|
||||||
|
return Scaffold(
|
||||||
|
body: ListView.builder(
|
||||||
|
itemCount: values.length,
|
||||||
|
itemBuilder: (BuildContext context, int index) => RadioListTile<int>(
|
||||||
|
onChanged: (int value) {
|
||||||
|
log.add(value);
|
||||||
|
setState(() {
|
||||||
|
selectedValue = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
value: values[index],
|
||||||
|
groupValue: selectedValue,
|
||||||
|
title: Text(values[index].toString()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests for tapping between [Radio] and [ListTile]
|
||||||
|
await tester.pumpWidget(buildFrame());
|
||||||
|
await tester.tap(find.text('1'));
|
||||||
|
log.add('-');
|
||||||
|
await tester.tap(find.byType(radioType).at(2));
|
||||||
|
expect(log, equals(<dynamic>[1, '-', 2]));
|
||||||
|
log.add('-');
|
||||||
|
await tester.tap(find.text('1'));
|
||||||
|
|
||||||
|
log.clear();
|
||||||
|
selectedValue = null;
|
||||||
|
|
||||||
|
// Tests for tapping across [Radio]s exclusively
|
||||||
|
await tester.pumpWidget(buildFrame());
|
||||||
|
await tester.tap(find.byType(radioType).at(1));
|
||||||
|
log.add('-');
|
||||||
|
await tester.tap(find.byType(radioType).at(2));
|
||||||
|
expect(log, equals(<dynamic>[1, '-', 2]));
|
||||||
|
|
||||||
|
log.clear();
|
||||||
|
selectedValue = null;
|
||||||
|
|
||||||
|
// Tests for tapping across [ListTile]s exclusively
|
||||||
|
await tester.pumpWidget(buildFrame());
|
||||||
|
await tester.tap(find.text('1'));
|
||||||
|
log.add('-');
|
||||||
|
await tester.tap(find.text('2'));
|
||||||
|
expect(log, equals(<dynamic>[1, '-', 2]));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Selected RadioListTile should not trigger onChanged', (WidgetTester tester) async {
|
||||||
|
// Regression test for https://github.com/flutter/flutter/issues/30311
|
||||||
|
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>[];
|
||||||
|
|
||||||
|
Widget buildFrame() {
|
||||||
|
return wrap(
|
||||||
|
child: StatefulBuilder(
|
||||||
|
builder: (BuildContext context, StateSetter setState) {
|
||||||
|
return Scaffold(
|
||||||
|
body: ListView.builder(
|
||||||
|
itemCount: values.length,
|
||||||
|
itemBuilder: (BuildContext context, int index) => RadioListTile<int>(
|
||||||
|
onChanged: (int value) {
|
||||||
|
log.add(value);
|
||||||
|
setState(() {
|
||||||
|
selectedValue = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
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]));
|
||||||
|
|
||||||
|
await tester.tap(find.byType(radioType).at(0));
|
||||||
|
await tester.pump();
|
||||||
|
expect(log, equals(<int>[0]));
|
||||||
|
});
|
||||||
|
|
||||||
|
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>[];
|
||||||
|
|
||||||
|
Widget buildFrame() {
|
||||||
|
return wrap(
|
||||||
|
child: StatefulBuilder(
|
||||||
|
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.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('RadioListTile semantics', (WidgetTester tester) async {
|
||||||
|
final SemanticsTester semantics = SemanticsTester(tester);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
wrap(
|
||||||
|
child: RadioListTile<int>(
|
||||||
|
value: 1,
|
||||||
|
groupValue: 2,
|
||||||
|
onChanged: (int i) {},
|
||||||
|
title: const Text('Title'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
semantics,
|
||||||
|
hasSemantics(
|
||||||
|
TestSemantics.root(
|
||||||
|
children: <TestSemantics>[
|
||||||
|
TestSemantics(
|
||||||
|
id: 1,
|
||||||
|
flags: <SemanticsFlag>[
|
||||||
|
SemanticsFlag.hasCheckedState,
|
||||||
|
SemanticsFlag.hasEnabledState,
|
||||||
|
SemanticsFlag.isEnabled,
|
||||||
|
SemanticsFlag.isInMutuallyExclusiveGroup,
|
||||||
|
SemanticsFlag.isFocusable,
|
||||||
|
],
|
||||||
|
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||||
|
label: 'Title',
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
ignoreRect: true,
|
||||||
|
ignoreTransform: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
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>[
|
||||||
|
SemanticsFlag.hasCheckedState,
|
||||||
|
SemanticsFlag.isChecked,
|
||||||
|
SemanticsFlag.hasEnabledState,
|
||||||
|
SemanticsFlag.isEnabled,
|
||||||
|
SemanticsFlag.isInMutuallyExclusiveGroup,
|
||||||
|
SemanticsFlag.isFocusable,
|
||||||
|
],
|
||||||
|
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||||
|
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'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
semantics,
|
||||||
|
hasSemantics(
|
||||||
|
TestSemantics.root(
|
||||||
|
children: <TestSemantics>[
|
||||||
|
TestSemantics(
|
||||||
|
id: 1,
|
||||||
|
flags: <SemanticsFlag>[
|
||||||
|
SemanticsFlag.hasCheckedState,
|
||||||
|
SemanticsFlag.hasEnabledState,
|
||||||
|
SemanticsFlag.isInMutuallyExclusiveGroup,
|
||||||
|
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,
|
||||||
|
],
|
||||||
|
label: 'Title',
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
ignoreId: true,
|
||||||
|
ignoreRect: true,
|
||||||
|
ignoreTransform: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
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() {
|
|||||||
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() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
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';
|
|||||||
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(
|
||||||
|
Loading…
Reference in New Issue
Block a user