Add checkbox and radio menu buttons (#112821)
This commit is contained in:
@@ -211,10 +211,7 @@ class _ControlsState extends State<_Controls> {
|
||||
alignmentOffset: const Offset(100, -8),
|
||||
menuChildren: <Widget>[
|
||||
MenuItemButton(
|
||||
shortcut: const SingleActivator(
|
||||
LogicalKeyboardKey.keyB,
|
||||
control: true,
|
||||
),
|
||||
shortcut: TestMenu.standaloneMenu1.shortcut,
|
||||
onPressed: () {
|
||||
_itemSelected(TestMenu.standaloneMenu1);
|
||||
},
|
||||
@@ -426,6 +423,9 @@ class _TestMenus extends StatefulWidget {
|
||||
|
||||
class _TestMenusState extends State<_TestMenus> {
|
||||
final TextEditingController textController = TextEditingController();
|
||||
bool? checkboxState = false;
|
||||
TestMenu? radioValue;
|
||||
ShortcutRegistryEntry? _shortcutsEntry;
|
||||
|
||||
void _itemSelected(TestMenu item) {
|
||||
debugPrint('App: Selected item ${item.label}');
|
||||
@@ -439,6 +439,79 @@ class _TestMenusState extends State<_TestMenus> {
|
||||
debugPrint('App: Closed item ${item.label}');
|
||||
}
|
||||
|
||||
void _setRadio(TestMenu item) {
|
||||
debugPrint('App: Set Radio item ${item.label}');
|
||||
setState(() {
|
||||
radioValue = item;
|
||||
});
|
||||
}
|
||||
|
||||
void _setCheck(TestMenu item) {
|
||||
debugPrint('App: Set Checkbox item ${item.label}');
|
||||
setState(() {
|
||||
switch (checkboxState) {
|
||||
case false:
|
||||
checkboxState = true;
|
||||
break;
|
||||
case true:
|
||||
checkboxState = null;
|
||||
break;
|
||||
case null:
|
||||
checkboxState = false;
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_shortcutsEntry?.dispose();
|
||||
final Map<ShortcutActivator, Intent> shortcuts = <ShortcutActivator, Intent>{};
|
||||
for (final TestMenu item in TestMenu.values) {
|
||||
if (item.shortcut == null) {
|
||||
continue;
|
||||
}
|
||||
switch (item) {
|
||||
case TestMenu.radioMenu1:
|
||||
case TestMenu.radioMenu2:
|
||||
case TestMenu.radioMenu3:
|
||||
shortcuts[item.shortcut!] = VoidCallbackIntent(() => _setRadio(item));
|
||||
break;
|
||||
case TestMenu.subMenu1:
|
||||
shortcuts[item.shortcut!] = VoidCallbackIntent(() => _setCheck(item));
|
||||
break;
|
||||
case TestMenu.mainMenu1:
|
||||
case TestMenu.mainMenu2:
|
||||
case TestMenu.mainMenu3:
|
||||
case TestMenu.mainMenu4:
|
||||
case TestMenu.subMenu2:
|
||||
case TestMenu.subMenu3:
|
||||
case TestMenu.subMenu4:
|
||||
case TestMenu.subMenu5:
|
||||
case TestMenu.subMenu6:
|
||||
case TestMenu.subMenu7:
|
||||
case TestMenu.subMenu8:
|
||||
case TestMenu.subSubMenu1:
|
||||
case TestMenu.subSubMenu2:
|
||||
case TestMenu.subSubMenu3:
|
||||
case TestMenu.subSubSubMenu1:
|
||||
case TestMenu.testButton:
|
||||
case TestMenu.standaloneMenu1:
|
||||
case TestMenu.standaloneMenu2:
|
||||
shortcuts[item.shortcut!] = VoidCallbackIntent(() => _itemSelected(item));
|
||||
break;
|
||||
}
|
||||
}
|
||||
_shortcutsEntry = ShortcutRegistry.of(context).addAll(shortcuts);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_shortcutsEntry?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
@@ -455,19 +528,61 @@ class _TestMenusState extends State<_TestMenus> {
|
||||
_closeItem(TestMenu.mainMenu1);
|
||||
},
|
||||
menuChildren: <Widget>[
|
||||
MenuItemButton(
|
||||
shortcut: const SingleActivator(
|
||||
LogicalKeyboardKey.keyB,
|
||||
control: true,
|
||||
),
|
||||
leadingIcon:
|
||||
widget.addItem ? const Icon(Icons.check_box) : const Icon(Icons.check_box_outline_blank),
|
||||
CheckboxMenuButton(
|
||||
value: checkboxState,
|
||||
tristate: true,
|
||||
shortcut: TestMenu.subMenu1.shortcut,
|
||||
trailingIcon: const Icon(Icons.assessment),
|
||||
onPressed: () {
|
||||
onChanged: (bool? value) {
|
||||
setState(() {
|
||||
checkboxState = value;
|
||||
});
|
||||
_itemSelected(TestMenu.subMenu1);
|
||||
},
|
||||
child: Text(TestMenu.subMenu1.label),
|
||||
),
|
||||
RadioMenuButton<TestMenu>(
|
||||
value: TestMenu.radioMenu1,
|
||||
groupValue: radioValue,
|
||||
toggleable: true,
|
||||
shortcut: TestMenu.radioMenu1.shortcut,
|
||||
trailingIcon: const Icon(Icons.assessment),
|
||||
onChanged: (TestMenu? value) {
|
||||
setState(() {
|
||||
radioValue = value;
|
||||
});
|
||||
_itemSelected(TestMenu.radioMenu1);
|
||||
},
|
||||
child: Text(TestMenu.radioMenu1.label),
|
||||
),
|
||||
RadioMenuButton<TestMenu>(
|
||||
value: TestMenu.radioMenu2,
|
||||
groupValue: radioValue,
|
||||
toggleable: true,
|
||||
shortcut: TestMenu.radioMenu2.shortcut,
|
||||
trailingIcon: const Icon(Icons.assessment),
|
||||
onChanged: (TestMenu? value) {
|
||||
setState(() {
|
||||
radioValue = value;
|
||||
});
|
||||
_itemSelected(TestMenu.radioMenu2);
|
||||
},
|
||||
child: Text(TestMenu.radioMenu2.label),
|
||||
),
|
||||
RadioMenuButton<TestMenu>(
|
||||
value: TestMenu.radioMenu3,
|
||||
groupValue: radioValue,
|
||||
toggleable: true,
|
||||
shortcut: TestMenu.radioMenu3.shortcut,
|
||||
trailingIcon: const Icon(Icons.assessment),
|
||||
onChanged: (TestMenu? value) {
|
||||
setState(() {
|
||||
radioValue = value;
|
||||
});
|
||||
_itemSelected(TestMenu.radioMenu3);
|
||||
},
|
||||
child: Text(TestMenu.radioMenu3.label),
|
||||
),
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(Icons.send),
|
||||
trailingIcon: const Icon(Icons.mail),
|
||||
@@ -495,10 +610,7 @@ class _TestMenusState extends State<_TestMenus> {
|
||||
},
|
||||
),
|
||||
MenuItemButton(
|
||||
shortcut: const SingleActivator(
|
||||
LogicalKeyboardKey.enter,
|
||||
control: true,
|
||||
),
|
||||
shortcut: TestMenu.subMenu3.shortcut,
|
||||
onPressed: () {
|
||||
_itemSelected(TestMenu.subMenu3);
|
||||
},
|
||||
@@ -542,10 +654,6 @@ class _TestMenusState extends State<_TestMenus> {
|
||||
)
|
||||
},
|
||||
child: MenuItemButton(
|
||||
shortcut: const SingleActivator(
|
||||
LogicalKeyboardKey.keyA,
|
||||
control: true,
|
||||
),
|
||||
onPressed: () {
|
||||
debugPrint('Activated text input item with ${textController.text} as a value.');
|
||||
},
|
||||
@@ -569,15 +677,7 @@ class _TestMenusState extends State<_TestMenus> {
|
||||
},
|
||||
menuChildren: <Widget>[
|
||||
MenuItemButton(
|
||||
shortcut: widget.addItem
|
||||
? const SingleActivator(
|
||||
LogicalKeyboardKey.f11,
|
||||
control: true,
|
||||
)
|
||||
: const SingleActivator(
|
||||
LogicalKeyboardKey.f10,
|
||||
control: true,
|
||||
),
|
||||
shortcut: TestMenu.subSubMenu1.shortcut,
|
||||
onPressed: () {
|
||||
_itemSelected(TestMenu.subSubMenu1);
|
||||
},
|
||||
@@ -593,10 +693,11 @@ class _TestMenusState extends State<_TestMenus> {
|
||||
SubmenuButton(
|
||||
menuChildren: <Widget>[
|
||||
MenuItemButton(
|
||||
child: Text(TestMenu.subSubSubMenu1.label),
|
||||
shortcut: TestMenu.subSubSubMenu1.shortcut,
|
||||
onPressed: () {
|
||||
_itemSelected(TestMenu.subSubSubMenu1);
|
||||
},
|
||||
child: Text(TestMenu.subSubSubMenu1.label),
|
||||
),
|
||||
],
|
||||
child: Text(TestMenu.subSubMenu3.label),
|
||||
@@ -606,10 +707,7 @@ class _TestMenusState extends State<_TestMenus> {
|
||||
),
|
||||
MenuItemButton(
|
||||
// Disabled button
|
||||
shortcut: const SingleActivator(
|
||||
LogicalKeyboardKey.tab,
|
||||
control: true,
|
||||
),
|
||||
shortcut: TestMenu.subMenu6.shortcut,
|
||||
child: Text(TestMenu.subMenu6.label),
|
||||
),
|
||||
MenuItemButton(
|
||||
@@ -646,22 +744,26 @@ enum TestMenu {
|
||||
mainMenu2('Menu 2'),
|
||||
mainMenu3('Menu 3'),
|
||||
mainMenu4('Menu 4'),
|
||||
subMenu1('Sub Menu 1'),
|
||||
radioMenu1('Radio Menu One', SingleActivator(LogicalKeyboardKey.digit1, control: true)),
|
||||
radioMenu2('Radio Menu Two', SingleActivator(LogicalKeyboardKey.digit2, control: true)),
|
||||
radioMenu3('Radio Menu Three', SingleActivator(LogicalKeyboardKey.digit3, control: true)),
|
||||
subMenu1('Sub Menu 1', SingleActivator(LogicalKeyboardKey.keyB, control: true)),
|
||||
subMenu2('Sub Menu 2'),
|
||||
subMenu3('Sub Menu 3'),
|
||||
subMenu3('Sub Menu 3', SingleActivator(LogicalKeyboardKey.enter, control: true)),
|
||||
subMenu4('Sub Menu 4'),
|
||||
subMenu5('Sub Menu 5'),
|
||||
subMenu6('Sub Menu 6'),
|
||||
subMenu6('Sub Menu 6', SingleActivator(LogicalKeyboardKey.tab, control: true)),
|
||||
subMenu7('Sub Menu 7'),
|
||||
subMenu8('Sub Menu 8'),
|
||||
subSubMenu1('Sub Sub Menu 1'),
|
||||
subSubMenu1('Sub Sub Menu 1', SingleActivator(LogicalKeyboardKey.f10, control: true)),
|
||||
subSubMenu2('Sub Sub Menu 2'),
|
||||
subSubMenu3('Sub Sub Menu 3'),
|
||||
subSubSubMenu1('Sub Sub Sub Menu 1'),
|
||||
subSubSubMenu1('Sub Sub Sub Menu 1', SingleActivator(LogicalKeyboardKey.f11, control: true)),
|
||||
testButton('TEST button'),
|
||||
standaloneMenu1('Standalone Menu 1'),
|
||||
standaloneMenu1('Standalone Menu 1', SingleActivator(LogicalKeyboardKey.keyC, control: true)),
|
||||
standaloneMenu2('Standalone Menu 2');
|
||||
|
||||
const TestMenu(this.label);
|
||||
const TestMenu(this.label, [this.shortcut]);
|
||||
final String label;
|
||||
final MenuSerializableShortcut? shortcut;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
// 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.
|
||||
|
||||
/// Flutter code sample for [CheckboxMenuButton].
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
void main() => runApp(const MenuApp());
|
||||
|
||||
class MyCheckboxMenu extends StatefulWidget {
|
||||
const MyCheckboxMenu({super.key, required this.message});
|
||||
|
||||
final String message;
|
||||
|
||||
@override
|
||||
State<MyCheckboxMenu> createState() => _MyCheckboxMenuState();
|
||||
}
|
||||
|
||||
class _MyCheckboxMenuState extends State<MyCheckboxMenu> {
|
||||
final FocusNode _buttonFocusNode = FocusNode(debugLabel: 'Menu Button');
|
||||
static const SingleActivator _showShortcut = SingleActivator(LogicalKeyboardKey.keyS, control: true);
|
||||
bool _showingMessage = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_buttonFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _setMessageVisibility(bool visible) {
|
||||
setState(() {
|
||||
_showingMessage = visible;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CallbackShortcuts(
|
||||
bindings: <ShortcutActivator, VoidCallback>{
|
||||
_showShortcut: () {
|
||||
_setMessageVisibility(!_showingMessage);
|
||||
},
|
||||
},
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
MenuAnchor(
|
||||
childFocusNode: _buttonFocusNode,
|
||||
menuChildren: <Widget>[
|
||||
CheckboxMenuButton(
|
||||
value: _showingMessage,
|
||||
onChanged: (bool? value) {
|
||||
_setMessageVisibility(value!);
|
||||
},
|
||||
child: const Text('Show Message'),
|
||||
),
|
||||
],
|
||||
builder: (BuildContext context, MenuController controller, Widget? child) {
|
||||
return TextButton(
|
||||
focusNode: _buttonFocusNode,
|
||||
onPressed: () {
|
||||
if (controller.isOpen) {
|
||||
controller.close();
|
||||
} else {
|
||||
controller.open();
|
||||
}
|
||||
},
|
||||
child: const Text('OPEN MENU'),
|
||||
);
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Text(
|
||||
_showingMessage ? widget.message : '',
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MenuApp extends StatelessWidget {
|
||||
const MenuApp({super.key});
|
||||
|
||||
static const String kMessage = '"Talk less. Smile more." - A. Burr';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const MaterialApp(
|
||||
home: Scaffold(body: MyCheckboxMenu(message: kMessage)),
|
||||
);
|
||||
}
|
||||
}
|
||||
114
examples/api/lib/material/menu_anchor/radio_menu_button.0.dart
Normal file
114
examples/api/lib/material/menu_anchor/radio_menu_button.0.dart
Normal file
@@ -0,0 +1,114 @@
|
||||
// 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.
|
||||
|
||||
/// Flutter code sample for [RadioMenuButton].
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
void main() => runApp(const MenuApp());
|
||||
|
||||
class MyRadioMenu extends StatefulWidget {
|
||||
const MyRadioMenu({super.key});
|
||||
|
||||
@override
|
||||
State<MyRadioMenu> createState() => _MyRadioMenuState();
|
||||
}
|
||||
|
||||
class _MyRadioMenuState extends State<MyRadioMenu> {
|
||||
final FocusNode _buttonFocusNode = FocusNode(debugLabel: 'Menu Button');
|
||||
Color _backgroundColor = Colors.red;
|
||||
late ShortcutRegistryEntry _entry;
|
||||
|
||||
static const SingleActivator _redShortcut = SingleActivator(LogicalKeyboardKey.keyR, control: true);
|
||||
static const SingleActivator _greenShortcut = SingleActivator(LogicalKeyboardKey.keyG, control: true);
|
||||
static const SingleActivator _blueShortcut = SingleActivator(LogicalKeyboardKey.keyB, control: true);
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_entry = ShortcutRegistry.of(context).addAll(<ShortcutActivator, VoidCallbackIntent>{
|
||||
_redShortcut: VoidCallbackIntent(() => _setBackgroundColor(Colors.red)),
|
||||
_greenShortcut: VoidCallbackIntent(() => _setBackgroundColor(Colors.green)),
|
||||
_blueShortcut: VoidCallbackIntent(() => _setBackgroundColor(Colors.blue)),
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_buttonFocusNode.dispose();
|
||||
_entry.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _setBackgroundColor(Color? color) {
|
||||
setState(() {
|
||||
_backgroundColor = color!;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
MenuAnchor(
|
||||
childFocusNode: _buttonFocusNode,
|
||||
menuChildren: <Widget>[
|
||||
RadioMenuButton<Color>(
|
||||
value: Colors.red,
|
||||
shortcut: _redShortcut,
|
||||
groupValue: _backgroundColor,
|
||||
onChanged: _setBackgroundColor,
|
||||
child: const Text('Red Background'),
|
||||
),
|
||||
RadioMenuButton<Color>(
|
||||
value: Colors.green,
|
||||
shortcut: _greenShortcut,
|
||||
groupValue: _backgroundColor,
|
||||
onChanged: _setBackgroundColor,
|
||||
child: const Text('Green Background'),
|
||||
),
|
||||
RadioMenuButton<Color>(
|
||||
value: Colors.blue,
|
||||
shortcut: _blueShortcut,
|
||||
groupValue: _backgroundColor,
|
||||
onChanged: _setBackgroundColor,
|
||||
child: const Text('Blue Background'),
|
||||
),
|
||||
],
|
||||
builder: (BuildContext context, MenuController controller, Widget? child) {
|
||||
return TextButton(
|
||||
focusNode: _buttonFocusNode,
|
||||
onPressed: () {
|
||||
if (controller.isOpen) {
|
||||
controller.close();
|
||||
} else {
|
||||
controller.open();
|
||||
}
|
||||
},
|
||||
child: const Text('OPEN MENU'),
|
||||
);
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
color: _backgroundColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MenuApp extends StatelessWidget {
|
||||
const MenuApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const MaterialApp(
|
||||
home: Scaffold(body: MyRadioMenu()),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// 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/material.dart';
|
||||
import 'package:flutter_api_samples/material/menu_anchor/checkbox_menu_button.0.dart' as example;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Can open menu and show message', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const example.MenuApp(),
|
||||
);
|
||||
|
||||
await tester.tap(find.byType(TextButton));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('Show Message'), findsOneWidget);
|
||||
expect(find.text(example.MenuApp.kMessage), findsNothing);
|
||||
|
||||
await tester.tap(find.text('Show Message'));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('Show Message'), findsNothing);
|
||||
expect(find.text(example.MenuApp.kMessage), findsOneWidget);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// 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/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_api_samples/material/menu_anchor/radio_menu_button.0.dart' as example;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Can open menu', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const example.MenuApp(),
|
||||
);
|
||||
|
||||
await tester.tap(find.byType(TextButton));
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('Red Background'), findsOneWidget);
|
||||
expect(find.text('Green Background'), findsOneWidget);
|
||||
expect(find.text('Blue Background'), findsOneWidget);
|
||||
expect(find.byType(Radio<Color>), findsNWidgets(3));
|
||||
expect(tester.widget<Container>(find.byType(Container)).color, equals(Colors.red));
|
||||
|
||||
await tester.tap(find.text('Green Background'));
|
||||
await tester.pump();
|
||||
|
||||
expect(tester.widget<Container>(find.byType(Container)).color, equals(Colors.green));
|
||||
});
|
||||
|
||||
testWidgets('Shortcuts work', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const example.MenuApp(),
|
||||
);
|
||||
|
||||
// Open the menu so we can watch state changes resulting from the shortcuts
|
||||
// firing.
|
||||
await tester.tap(find.byType(TextButton));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('Red Background'), findsOneWidget);
|
||||
expect(find.text('Green Background'), findsOneWidget);
|
||||
expect(find.text('Blue Background'), findsOneWidget);
|
||||
expect(find.byType(Radio<Color>), findsNWidgets(3));
|
||||
expect(tester.widget<Container>(find.byType(Container)).color, equals(Colors.red));
|
||||
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.keyG);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
|
||||
await tester.pump();
|
||||
// Need to pump twice because of the one frame delay in the notification to
|
||||
// update the overlay entry.
|
||||
await tester.pump();
|
||||
|
||||
expect(tester.widget<Radio<Color>>(find.descendant(of: find.byType(RadioMenuButton<Color>).at(0), matching: find.byType(Radio<Color>))).groupValue, equals(Colors.green));
|
||||
expect(tester.widget<Container>(find.byType(Container)).color, equals(Colors.green));
|
||||
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.keyR);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
expect(tester.widget<Radio<Color>>(find.descendant(of: find.byType(RadioMenuButton<Color>).at(1), matching: find.byType(Radio<Color>))).groupValue, equals(Colors.red));
|
||||
expect(tester.widget<Container>(find.byType(Container)).color, equals(Colors.red));
|
||||
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.keyB);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
expect(tester.widget<Radio<Color>>(find.descendant(of: find.byType(RadioMenuButton<Color>).at(2), matching: find.byType(Radio<Color>))).groupValue, equals(Colors.blue));
|
||||
expect(tester.widget<Container>(find.byType(Container)).color, equals(Colors.blue));
|
||||
});
|
||||
}
|
||||
@@ -208,7 +208,7 @@ class Checkbox extends StatefulWidget {
|
||||
|
||||
/// If true the checkbox's [value] can be true, false, or null.
|
||||
///
|
||||
/// Checkbox displays a dash when its value is null.
|
||||
/// [Checkbox] displays a dash when its value is null.
|
||||
///
|
||||
/// When a tri-state checkbox ([tristate] is true) is tapped, its [onChanged]
|
||||
/// callback will be applied to true if the current value is false, to null if
|
||||
|
||||
@@ -12,6 +12,7 @@ import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'button_style.dart';
|
||||
import 'button_style_button.dart';
|
||||
import 'checkbox.dart';
|
||||
import 'color_scheme.dart';
|
||||
import 'colors.dart';
|
||||
import 'constants.dart';
|
||||
@@ -24,10 +25,18 @@ import 'menu_bar_theme.dart';
|
||||
import 'menu_button_theme.dart';
|
||||
import 'menu_style.dart';
|
||||
import 'menu_theme.dart';
|
||||
import 'radio.dart';
|
||||
import 'text_button.dart';
|
||||
import 'theme.dart';
|
||||
import 'theme_data.dart';
|
||||
|
||||
// Examples can assume:
|
||||
// bool _throwShotAway = false;
|
||||
// late BuildContext context;
|
||||
// enum SingingCharacter { lafayette }
|
||||
// late SingingCharacter? _character;
|
||||
// late StateSetter setState;
|
||||
|
||||
// Enable if you want verbose logging about menu changes.
|
||||
const bool _kDebugMenus = false;
|
||||
|
||||
@@ -1069,6 +1078,393 @@ class _MenuItemButtonState extends State<MenuItemButton> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A menu item that combines a [Checkbox] widget with a [MenuItemButton].
|
||||
///
|
||||
/// To style the checkbox separately from the button, add a [CheckboxTheme]
|
||||
/// ancestor.
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This example shows a menu with a checkbox that shows a message in the body
|
||||
/// of the app if checked.
|
||||
///
|
||||
/// ** See code in examples/api/lib/material/menu_anchor/checkbox_menu_button.0.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// - [MenuBar], a widget that creates a menu bar of cascading menu items.
|
||||
/// - [MenuAnchor], a widget that defines a region which can host a cascading
|
||||
/// menu.
|
||||
class CheckboxMenuButton extends StatelessWidget {
|
||||
/// Creates a const [CheckboxMenuButton].
|
||||
///
|
||||
/// The [child], [value], and [onChanged] attributes are required.
|
||||
const CheckboxMenuButton({
|
||||
super.key,
|
||||
required this.value,
|
||||
this.tristate = false,
|
||||
this.isError = false,
|
||||
required this.onChanged,
|
||||
this.onHover,
|
||||
this.onFocusChange,
|
||||
this.focusNode,
|
||||
this.shortcut,
|
||||
this.style,
|
||||
this.statesController,
|
||||
this.clipBehavior = Clip.none,
|
||||
this.trailingIcon,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
/// Whether this checkbox is checked.
|
||||
///
|
||||
/// When [tristate] is true, a value of null corresponds to the mixed state.
|
||||
/// When [tristate] is false, this value must not be null.
|
||||
final bool? value;
|
||||
|
||||
/// If true, then the checkbox's [value] can be true, false, or null.
|
||||
///
|
||||
/// [CheckboxMenuButton] displays a dash when its value is null.
|
||||
///
|
||||
/// When a tri-state checkbox ([tristate] is true) is tapped, its [onChanged]
|
||||
/// callback will be applied to true if the current value is false, to null if
|
||||
/// value is true, and to false if value is null (i.e. it cycles through false
|
||||
/// => true => null => false when tapped).
|
||||
///
|
||||
/// If tristate is false (the default), [value] must not be null.
|
||||
final bool tristate;
|
||||
|
||||
/// True if this checkbox wants to show an error state.
|
||||
///
|
||||
/// The checkbox will have different default container color and check color when
|
||||
/// this is true. This is only used when [ThemeData.useMaterial3] is set to true.
|
||||
///
|
||||
/// Must not be null. Defaults to false.
|
||||
final bool isError;
|
||||
|
||||
/// Called when the value of the checkbox should change.
|
||||
///
|
||||
/// The checkbox passes the new value to the callback but does not actually
|
||||
/// change state until the parent widget rebuilds the checkbox with the new
|
||||
/// value.
|
||||
///
|
||||
/// If this callback is null, the menu item will be displayed as disabled
|
||||
/// and will not respond to input gestures.
|
||||
///
|
||||
/// When the checkbox is tapped, if [tristate] is false (the default) then the
|
||||
/// [onChanged] callback will be applied to `!value`. If [tristate] is true
|
||||
/// this callback cycle from false to true to null and then back to false
|
||||
/// again.
|
||||
///
|
||||
/// The callback provided to [onChanged] should update the state of the parent
|
||||
/// [StatefulWidget] using the [State.setState] method, so that the parent
|
||||
/// gets rebuilt; for example:
|
||||
///
|
||||
/// ```dart
|
||||
/// CheckboxMenuButton(
|
||||
/// value: _throwShotAway,
|
||||
/// child: const Text('THROW'),
|
||||
/// onChanged: (bool? newValue) {
|
||||
/// setState(() {
|
||||
/// _throwShotAway = newValue!;
|
||||
/// });
|
||||
/// },
|
||||
/// )
|
||||
/// ```
|
||||
final ValueChanged<bool?>? onChanged;
|
||||
|
||||
/// Called when a pointer enters or exits the button response area.
|
||||
///
|
||||
/// The value passed to the callback is true if a pointer has entered button
|
||||
/// area and false if a pointer has exited.
|
||||
final ValueChanged<bool>? onHover;
|
||||
|
||||
/// Handler called when the focus changes.
|
||||
///
|
||||
/// Called with true if this widget's node gains focus, and false if it loses
|
||||
/// focus.
|
||||
final ValueChanged<bool>? onFocusChange;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.focusNode}
|
||||
final FocusNode? focusNode;
|
||||
|
||||
/// The optional shortcut that selects this [MenuItemButton].
|
||||
///
|
||||
/// {@macro flutter.material.menu_bar.shortcuts_note}
|
||||
final MenuSerializableShortcut? shortcut;
|
||||
|
||||
/// Customizes this button's appearance.
|
||||
///
|
||||
/// Non-null properties of this style override the corresponding properties in
|
||||
/// [MenuItemButton.themeStyleOf] and [MenuItemButton.defaultStyleOf].
|
||||
/// [MaterialStateProperty]s that resolve to non-null values will similarly
|
||||
/// override the corresponding [MaterialStateProperty]s in
|
||||
/// [MenuItemButton.themeStyleOf] and [MenuItemButton.defaultStyleOf].
|
||||
///
|
||||
/// Null by default.
|
||||
final ButtonStyle? style;
|
||||
|
||||
/// {@macro flutter.material.inkwell.statesController}
|
||||
final MaterialStatesController? statesController;
|
||||
|
||||
/// {@macro flutter.material.Material.clipBehavior}
|
||||
///
|
||||
/// Defaults to [Clip.none].
|
||||
final Clip clipBehavior;
|
||||
|
||||
/// An optional icon to display after the [child] label.
|
||||
final Widget? trailingIcon;
|
||||
|
||||
/// The widget displayed in the center of this button.
|
||||
///
|
||||
/// Typically this is the button's label, using a [Text] widget.
|
||||
///
|
||||
/// {@macro flutter.widgets.ProxyWidget.child}
|
||||
final Widget? child;
|
||||
|
||||
/// Whether the button is enabled or disabled.
|
||||
///
|
||||
/// To enable a button, set its [onChanged] property to a non-null value.
|
||||
bool get enabled => onChanged != null;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MenuItemButton(
|
||||
key: key,
|
||||
onPressed: onChanged == null ? null : () {
|
||||
switch (value) {
|
||||
case false:
|
||||
onChanged!.call(true);
|
||||
break;
|
||||
case true:
|
||||
onChanged!.call(tristate ? null : false);
|
||||
break;
|
||||
case null:
|
||||
onChanged!.call(false);
|
||||
break;
|
||||
}
|
||||
},
|
||||
onHover: onHover,
|
||||
onFocusChange: onFocusChange,
|
||||
focusNode: focusNode,
|
||||
style: style,
|
||||
shortcut: shortcut,
|
||||
statesController: statesController,
|
||||
leadingIcon: ExcludeFocus(
|
||||
child: IgnorePointer(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxHeight: Checkbox.width,
|
||||
maxWidth: Checkbox.width,
|
||||
),
|
||||
child: Checkbox(
|
||||
tristate: tristate,
|
||||
value: value,
|
||||
onChanged: onChanged,
|
||||
isError: isError,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
clipBehavior: clipBehavior,
|
||||
trailingIcon: trailingIcon,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A menu item that combines a [Radio] widget with a [MenuItemButton].
|
||||
///
|
||||
/// To style the radio button separately from the overall button, add a
|
||||
/// [RadioTheme] ancestor.
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This example shows a menu with three radio buttons with shortcuts that
|
||||
/// changes the background color of the body when the buttons are selected.
|
||||
///
|
||||
/// ** See code in examples/api/lib/material/menu_anchor/radio_menu_button.0.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// - [MenuBar], a widget that creates a menu bar of cascading menu items.
|
||||
/// - [MenuAnchor], a widget that defines a region which can host a cascading
|
||||
/// menu.
|
||||
class RadioMenuButton<T> extends StatelessWidget {
|
||||
/// Creates a const [RadioMenuButton].
|
||||
///
|
||||
/// The [child] attribute is required.
|
||||
const RadioMenuButton({
|
||||
super.key,
|
||||
required this.value,
|
||||
required this.groupValue,
|
||||
required this.onChanged,
|
||||
this.toggleable = false,
|
||||
this.onHover,
|
||||
this.onFocusChange,
|
||||
this.focusNode,
|
||||
this.shortcut,
|
||||
this.style,
|
||||
this.statesController,
|
||||
this.clipBehavior = Clip.none,
|
||||
this.trailingIcon,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
/// The value represented by this radio button.
|
||||
///
|
||||
/// This radio button is considered selected if its [value] matches the
|
||||
/// [groupValue].
|
||||
final T value;
|
||||
|
||||
/// The currently selected value for a group of radio buttons.
|
||||
///
|
||||
/// This radio button is considered selected if its [value] matches the
|
||||
/// [groupValue].
|
||||
final T? groupValue;
|
||||
|
||||
/// 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.
|
||||
final bool toggleable;
|
||||
|
||||
/// Called when the user selects this radio button.
|
||||
///
|
||||
/// The radio button passes [value] as a parameter to this callback. The radio
|
||||
/// button does not actually change state until the parent widget rebuilds the
|
||||
/// radio button with the new [groupValue].
|
||||
///
|
||||
/// If null, the radio button will be displayed as disabled.
|
||||
///
|
||||
/// The provided callback will not be invoked if this radio button is already
|
||||
/// selected.
|
||||
///
|
||||
/// The callback provided to [onChanged] should update the state of the parent
|
||||
/// [StatefulWidget] using the [State.setState] method, so that the parent
|
||||
/// gets rebuilt; for example:
|
||||
///
|
||||
/// ```dart
|
||||
/// RadioMenuButton<SingingCharacter>(
|
||||
/// value: SingingCharacter.lafayette,
|
||||
/// groupValue: _character,
|
||||
/// onChanged: (SingingCharacter? newValue) {
|
||||
/// setState(() {
|
||||
/// _character = newValue;
|
||||
/// });
|
||||
/// },
|
||||
/// child: const Text('Lafayette'),
|
||||
/// )
|
||||
/// ```
|
||||
final ValueChanged<T?>? onChanged;
|
||||
|
||||
/// Called when a pointer enters or exits the button response area.
|
||||
///
|
||||
/// The value passed to the callback is true if a pointer has entered button
|
||||
/// area and false if a pointer has exited.
|
||||
final ValueChanged<bool>? onHover;
|
||||
|
||||
/// Handler called when the focus changes.
|
||||
///
|
||||
/// Called with true if this widget's node gains focus, and false if it loses
|
||||
/// focus.
|
||||
final ValueChanged<bool>? onFocusChange;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.focusNode}
|
||||
final FocusNode? focusNode;
|
||||
|
||||
/// The optional shortcut that selects this [MenuItemButton].
|
||||
///
|
||||
/// {@macro flutter.material.menu_bar.shortcuts_note}
|
||||
final MenuSerializableShortcut? shortcut;
|
||||
|
||||
/// Customizes this button's appearance.
|
||||
///
|
||||
/// Non-null properties of this style override the corresponding properties in
|
||||
/// [MenuItemButton.themeStyleOf] and [MenuItemButton.defaultStyleOf].
|
||||
/// [MaterialStateProperty]s that resolve to non-null values will similarly
|
||||
/// override the corresponding [MaterialStateProperty]s in
|
||||
/// [MenuItemButton.themeStyleOf] and [MenuItemButton.defaultStyleOf].
|
||||
///
|
||||
/// Null by default.
|
||||
final ButtonStyle? style;
|
||||
|
||||
/// {@macro flutter.material.inkwell.statesController}
|
||||
final MaterialStatesController? statesController;
|
||||
|
||||
/// {@macro flutter.material.Material.clipBehavior}
|
||||
///
|
||||
/// Defaults to [Clip.none].
|
||||
final Clip clipBehavior;
|
||||
|
||||
/// An optional icon to display after the [child] label.
|
||||
final Widget? trailingIcon;
|
||||
|
||||
/// The widget displayed in the center of this button.
|
||||
///
|
||||
/// Typically this is the button's label, using a [Text] widget.
|
||||
///
|
||||
/// {@macro flutter.widgets.ProxyWidget.child}
|
||||
final Widget? child;
|
||||
|
||||
/// Whether the button is enabled or disabled.
|
||||
///
|
||||
/// To enable a button, set its [onChanged] property to a non-null value.
|
||||
bool get enabled => onChanged != null;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MenuItemButton(
|
||||
key: key,
|
||||
onPressed: onChanged == null ? null : () {
|
||||
if (toggleable && groupValue == value) {
|
||||
onChanged!.call(null);
|
||||
return;
|
||||
}
|
||||
onChanged!.call(value);
|
||||
},
|
||||
onHover: onHover,
|
||||
onFocusChange: onFocusChange,
|
||||
focusNode: focusNode,
|
||||
style: style,
|
||||
shortcut: shortcut,
|
||||
statesController: statesController,
|
||||
leadingIcon: ExcludeFocus(
|
||||
child: IgnorePointer(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxHeight: Checkbox.width,
|
||||
maxWidth: Checkbox.width,
|
||||
),
|
||||
child: Radio<T>(
|
||||
value: value,
|
||||
groupValue: groupValue,
|
||||
onChanged: onChanged,
|
||||
toggleable: toggleable,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
clipBehavior: clipBehavior,
|
||||
trailingIcon: trailingIcon,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// A menu button that displays a cascading menu.
|
||||
///
|
||||
/// It can be used as part of a [MenuBar], or as a standalone widget.
|
||||
|
||||
@@ -148,7 +148,7 @@ void main() {
|
||||
|
||||
final Rect tallerWidget = checkboxRect.height > titleRect.height ? checkboxRect : titleRect;
|
||||
|
||||
// Check the offsets of CheckBox and title after padding is applied.
|
||||
// Check the offsets of Checkbox and title after padding is applied.
|
||||
expect(paddingRect.right, checkboxRect.right + 4);
|
||||
expect(paddingRect.left, titleRect.left - 10);
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ void main() {
|
||||
expect(tester.getSize(find.byType(Checkbox)), const Size(40.0, 40.0));
|
||||
});
|
||||
|
||||
testWidgets('CheckBox semantics', (WidgetTester tester) async {
|
||||
testWidgets('Checkbox semantics', (WidgetTester tester) async {
|
||||
final SemanticsHandle handle = tester.ensureSemantics();
|
||||
|
||||
await tester.pumpWidget(Theme(
|
||||
@@ -193,7 +193,7 @@ void main() {
|
||||
handle.dispose();
|
||||
});
|
||||
|
||||
testWidgets('Can wrap CheckBox with Semantics', (WidgetTester tester) async {
|
||||
testWidgets('Can wrap Checkbox with Semantics', (WidgetTester tester) async {
|
||||
final SemanticsHandle handle = tester.ensureSemantics();
|
||||
|
||||
await tester.pumpWidget(Theme(
|
||||
@@ -222,7 +222,7 @@ void main() {
|
||||
handle.dispose();
|
||||
});
|
||||
|
||||
testWidgets('CheckBox tristate: true', (WidgetTester tester) async {
|
||||
testWidgets('Checkbox tristate: true', (WidgetTester tester) async {
|
||||
bool? checkBoxValue;
|
||||
|
||||
await tester.pumpWidget(
|
||||
@@ -388,7 +388,7 @@ void main() {
|
||||
semanticsTester.dispose();
|
||||
});
|
||||
|
||||
testWidgets('CheckBox tristate rendering, programmatic transitions', (WidgetTester tester) async {
|
||||
testWidgets('Checkbox tristate rendering, programmatic transitions', (WidgetTester tester) async {
|
||||
Widget buildFrame(bool? checkboxValue) {
|
||||
return Theme(
|
||||
data: theme,
|
||||
@@ -439,7 +439,7 @@ void main() {
|
||||
expect(getCheckboxRenderer(), paints..line()); // null is rendered as a line (a "dash")
|
||||
});
|
||||
|
||||
testWidgets('CheckBox color rendering', (WidgetTester tester) async {
|
||||
testWidgets('Checkbox color rendering', (WidgetTester tester) async {
|
||||
const Color borderColor = Color(0xff2196f3);
|
||||
Color checkColor = const Color(0xffFFFFFF);
|
||||
Color activeColor;
|
||||
|
||||
@@ -1642,6 +1642,129 @@ void main() {
|
||||
expect(find.text(charExpected), findsOneWidget);
|
||||
}, variant: TargetPlatformVariant.all());
|
||||
});
|
||||
|
||||
group('CheckboxMenuButton', () {
|
||||
testWidgets('tapping toggles checkbox', (WidgetTester tester) async {
|
||||
bool? checkBoxValue;
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setState) {
|
||||
return MenuBar(
|
||||
children: <Widget>[
|
||||
SubmenuButton(
|
||||
menuChildren: <Widget>[
|
||||
CheckboxMenuButton(
|
||||
value: checkBoxValue,
|
||||
onChanged: (bool? value) {
|
||||
setState(() {
|
||||
checkBoxValue = value;
|
||||
});
|
||||
},
|
||||
tristate: true,
|
||||
child: const Text('checkbox'),
|
||||
)
|
||||
],
|
||||
child: const Text('submenu'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byType(SubmenuButton));
|
||||
await tester.pump();
|
||||
|
||||
expect(tester.widget<CheckboxMenuButton>(find.byType(CheckboxMenuButton)).value, null);
|
||||
|
||||
await tester.tap(find.byType(CheckboxMenuButton));
|
||||
await tester.pumpAndSettle();
|
||||
expect(checkBoxValue, false);
|
||||
|
||||
await tester.tap(find.byType(SubmenuButton));
|
||||
await tester.pump();
|
||||
await tester.tap(find.byType(CheckboxMenuButton));
|
||||
await tester.pumpAndSettle();
|
||||
expect(checkBoxValue, true);
|
||||
|
||||
await tester.tap(find.byType(SubmenuButton));
|
||||
await tester.pump();
|
||||
await tester.tap(find.byType(CheckboxMenuButton));
|
||||
await tester.pumpAndSettle();
|
||||
expect(checkBoxValue, null);
|
||||
});
|
||||
});
|
||||
|
||||
group('RadioMenuButton', () {
|
||||
testWidgets('tapping toggles radio button', (WidgetTester tester) async {
|
||||
int? radioValue;
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setState) {
|
||||
return MenuBar(
|
||||
children: <Widget>[
|
||||
SubmenuButton(
|
||||
menuChildren: <Widget>[
|
||||
RadioMenuButton<int>(
|
||||
value: 0,
|
||||
groupValue: radioValue,
|
||||
onChanged: (int? value) {
|
||||
setState(() {
|
||||
radioValue = value;
|
||||
});
|
||||
},
|
||||
toggleable: true,
|
||||
child: const Text('radio 0'),
|
||||
),
|
||||
RadioMenuButton<int>(
|
||||
value: 1,
|
||||
groupValue: radioValue,
|
||||
onChanged: (int? value) {
|
||||
setState(() {
|
||||
radioValue = value;
|
||||
});
|
||||
},
|
||||
toggleable: true,
|
||||
child: const Text('radio 1'),
|
||||
)
|
||||
],
|
||||
child: const Text('submenu'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byType(SubmenuButton));
|
||||
await tester.pump();
|
||||
|
||||
expect(
|
||||
tester.widget<RadioMenuButton<int>>(find.byType(RadioMenuButton<int>).first).groupValue,
|
||||
null,
|
||||
);
|
||||
|
||||
await tester.tap(find.byType(RadioMenuButton<int>).first);
|
||||
await tester.pumpAndSettle();
|
||||
expect(radioValue, 0);
|
||||
|
||||
await tester.tap(find.byType(SubmenuButton));
|
||||
await tester.pump();
|
||||
await tester.tap(find.byType(RadioMenuButton<int>).first);
|
||||
await tester.pumpAndSettle();
|
||||
expect(radioValue, null);
|
||||
|
||||
await tester.tap(find.byType(SubmenuButton));
|
||||
await tester.pump();
|
||||
await tester.tap(find.byType(RadioMenuButton<int>).last);
|
||||
await tester.pumpAndSettle();
|
||||
expect(radioValue, 1);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
List<Widget> createTestMenus({
|
||||
|
||||
Reference in New Issue
Block a user