Unverified Commit 6b32c069 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Add checkbox and radio menu buttons (#112821)

parent 91b5079f
This diff is collapsed.
// 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)),
);
}
}
// 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()),
);
}
}
// 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);
});
}
// 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
......
......@@ -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({
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment