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(
|
||||
_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>`.
|
||||
// Another approach is to grab the `runtimeType` of a dummy instance; see packages/flutter/test/material/control_list_tile_test.dart.
|
||||
// [find.byType] and [find.widgetWithText] do not match subclasses; `Radio` is
|
||||
// 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 =>
|
||||
find.byWidgetPredicate((Widget w) => w is Radio<Location>);
|
||||
|
||||
|
@ -108,6 +108,7 @@ class Radio<T> extends StatefulWidget {
|
||||
@required this.value,
|
||||
@required this.groupValue,
|
||||
@required this.onChanged,
|
||||
this.toggleable = false,
|
||||
this.activeColor,
|
||||
this.focusColor,
|
||||
this.hoverColor,
|
||||
@ -116,6 +117,7 @@ class Radio<T> extends StatefulWidget {
|
||||
this.focusNode,
|
||||
this.autofocus = false,
|
||||
}) : assert(autofocus != null),
|
||||
assert(toggleable != null),
|
||||
super(key: key);
|
||||
|
||||
/// The value represented by this radio button.
|
||||
@ -155,6 +157,69 @@ class Radio<T> extends StatefulWidget {
|
||||
/// ```
|
||||
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.
|
||||
///
|
||||
/// 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) {
|
||||
widget.onChanged(widget.value);
|
||||
}
|
||||
@ -241,8 +306,13 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin {
|
||||
}
|
||||
|
||||
void _handleChanged(bool selected) {
|
||||
if (selected)
|
||||
if (selected == null) {
|
||||
widget.onChanged(null);
|
||||
return;
|
||||
}
|
||||
if (selected) {
|
||||
widget.onChanged(widget.value);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@ -276,6 +346,7 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin {
|
||||
focusColor: widget.focusColor ?? themeData.focusColor,
|
||||
hoverColor: widget.hoverColor ?? themeData.hoverColor,
|
||||
onChanged: enabled ? _handleChanged : null,
|
||||
toggleable: widget.toggleable,
|
||||
additionalConstraints: additionalConstraints,
|
||||
vsync: this,
|
||||
hasFocus: _focused,
|
||||
@ -297,6 +368,7 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget {
|
||||
@required this.hoverColor,
|
||||
@required this.additionalConstraints,
|
||||
this.onChanged,
|
||||
@required this.toggleable,
|
||||
@required this.vsync,
|
||||
@required this.hasFocus,
|
||||
@required this.hovering,
|
||||
@ -304,6 +376,7 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget {
|
||||
assert(activeColor != null),
|
||||
assert(inactiveColor != null),
|
||||
assert(vsync != null),
|
||||
assert(toggleable != null),
|
||||
super(key: key);
|
||||
|
||||
final bool selected;
|
||||
@ -314,6 +387,7 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget {
|
||||
final Color focusColor;
|
||||
final Color hoverColor;
|
||||
final ValueChanged<bool> onChanged;
|
||||
final bool toggleable;
|
||||
final TickerProvider vsync;
|
||||
final BoxConstraints additionalConstraints;
|
||||
|
||||
@ -325,6 +399,7 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget {
|
||||
focusColor: focusColor,
|
||||
hoverColor: hoverColor,
|
||||
onChanged: onChanged,
|
||||
tristate: toggleable,
|
||||
vsync: vsync,
|
||||
additionalConstraints: additionalConstraints,
|
||||
hasFocus: hasFocus,
|
||||
@ -340,6 +415,7 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget {
|
||||
..focusColor = focusColor
|
||||
..hoverColor = hoverColor
|
||||
..onChanged = onChanged
|
||||
..tristate = toggleable
|
||||
..additionalConstraints = additionalConstraints
|
||||
..vsync = vsync
|
||||
..hasFocus = hasFocus
|
||||
@ -355,18 +431,19 @@ class _RenderRadio extends RenderToggleable {
|
||||
Color focusColor,
|
||||
Color hoverColor,
|
||||
ValueChanged<bool> onChanged,
|
||||
bool tristate,
|
||||
BoxConstraints additionalConstraints,
|
||||
@required TickerProvider vsync,
|
||||
bool hasFocus,
|
||||
bool hovering,
|
||||
}) : super(
|
||||
value: value,
|
||||
tristate: false,
|
||||
activeColor: activeColor,
|
||||
inactiveColor: inactiveColor,
|
||||
focusColor: focusColor,
|
||||
hoverColor: hoverColor,
|
||||
onChanged: onChanged,
|
||||
tristate: tristate,
|
||||
additionalConstraints: additionalConstraints,
|
||||
vsync: vsync,
|
||||
hasFocus: hasFocus,
|
||||
|
@ -309,6 +309,7 @@ class RadioListTile<T> extends StatelessWidget {
|
||||
@required this.value,
|
||||
@required this.groupValue,
|
||||
@required this.onChanged,
|
||||
this.toggleable = false,
|
||||
this.activeColor,
|
||||
this.title,
|
||||
this.subtitle,
|
||||
@ -317,7 +318,9 @@ class RadioListTile<T> extends StatelessWidget {
|
||||
this.secondary,
|
||||
this.selected = false,
|
||||
this.controlAffinity = ListTileControlAffinity.platform,
|
||||
}) : assert(isThreeLine != null),
|
||||
|
||||
}) : assert(toggleable != null),
|
||||
assert(isThreeLine != null),
|
||||
assert(!isThreeLine || subtitle != null),
|
||||
assert(selected != null),
|
||||
assert(controlAffinity != null),
|
||||
@ -361,6 +364,62 @@ class RadioListTile<T> extends StatelessWidget {
|
||||
/// ```
|
||||
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.
|
||||
///
|
||||
/// Defaults to accent color of the current [Theme].
|
||||
@ -416,6 +475,7 @@ class RadioListTile<T> extends StatelessWidget {
|
||||
value: value,
|
||||
groupValue: groupValue,
|
||||
onChanged: onChanged,
|
||||
toggleable: toggleable,
|
||||
activeColor: activeColor,
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
);
|
||||
@ -442,7 +502,15 @@ class RadioListTile<T> extends StatelessWidget {
|
||||
isThreeLine: isThreeLine,
|
||||
dense: dense,
|
||||
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,
|
||||
),
|
||||
),
|
||||
|
@ -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);
|
||||
});
|
||||
|
||||
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 {
|
||||
final Key key1 = UniqueKey();
|
||||
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;
|
||||
int groupValue = 1;
|
||||
const Key radioKey0 = Key('radio0');
|
||||
|
@ -8,7 +8,113 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.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() {
|
||||
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 {
|
||||
bool value = false;
|
||||
await tester.pumpWidget(
|
||||
|
Loading…
Reference in New Issue
Block a user