mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Add controller argument to SubmenuButton (#125000)
## Description This adds an optional argument to the `SubmenuButton` that allows the creator to supply a `MenuController` for controlling the menu. ## Related Issues - Fixes https://github.com/flutter/flutter/issues/124988 ## Tests - Added tests for new argument.
This commit is contained in:
parent
98dfdf1b6c
commit
4b39f071f3
@ -1548,6 +1548,7 @@ class SubmenuButton extends StatefulWidget {
|
||||
this.onFocusChange,
|
||||
this.onOpen,
|
||||
this.onClose,
|
||||
this.controller,
|
||||
this.style,
|
||||
this.menuStyle,
|
||||
this.alignmentOffset,
|
||||
@ -1578,6 +1579,9 @@ class SubmenuButton extends StatefulWidget {
|
||||
/// A callback that is invoked when the menu is closed.
|
||||
final VoidCallback? onClose;
|
||||
|
||||
/// An optional [MenuController] for this submenu.
|
||||
final MenuController? controller;
|
||||
|
||||
/// Customizes this button's appearance.
|
||||
///
|
||||
/// Non-null properties of this style override the corresponding properties in
|
||||
@ -1760,7 +1764,8 @@ class SubmenuButton extends StatefulWidget {
|
||||
class _SubmenuButtonState extends State<SubmenuButton> {
|
||||
FocusNode? _internalFocusNode;
|
||||
bool _waitingToFocusMenu = false;
|
||||
final MenuController _menuController = MenuController();
|
||||
MenuController? _internalMenuController;
|
||||
MenuController get _menuController => widget.controller ?? _internalMenuController!;
|
||||
_MenuAnchorState? get _anchor => _MenuAnchorState._maybeOf(context);
|
||||
FocusNode get _buttonFocusNode => widget.focusNode ?? _internalFocusNode!;
|
||||
bool get _enabled => widget.menuChildren.isNotEmpty;
|
||||
@ -1777,12 +1782,15 @@ class _SubmenuButtonState extends State<SubmenuButton> {
|
||||
return true;
|
||||
}());
|
||||
}
|
||||
if (widget.controller == null) {
|
||||
_internalMenuController = MenuController();
|
||||
}
|
||||
_buttonFocusNode.addListener(_handleFocusChange);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_internalFocusNode?.removeListener(_handleFocusChange);
|
||||
_buttonFocusNode.removeListener(_handleFocusChange);
|
||||
_internalFocusNode?.dispose();
|
||||
_internalFocusNode = null;
|
||||
super.dispose();
|
||||
@ -1810,6 +1818,9 @@ class _SubmenuButtonState extends State<SubmenuButton> {
|
||||
}
|
||||
_buttonFocusNode.addListener(_handleFocusChange);
|
||||
}
|
||||
if (widget.controller != oldWidget.controller) {
|
||||
_internalMenuController = (oldWidget.controller == null) ? null : MenuController();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@ -1836,7 +1847,16 @@ class _SubmenuButtonState extends State<SubmenuButton> {
|
||||
alignmentOffset: menuPaddingOffset,
|
||||
clipBehavior: widget.clipBehavior,
|
||||
onClose: widget.onClose,
|
||||
onOpen: widget.onOpen,
|
||||
onOpen: () {
|
||||
if (!_waitingToFocusMenu) {
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
_menuController._anchor?._focusButton();
|
||||
_waitingToFocusMenu = false;
|
||||
});
|
||||
_waitingToFocusMenu = true;
|
||||
}
|
||||
widget.onOpen?.call();
|
||||
},
|
||||
style: widget.menuStyle,
|
||||
builder: (BuildContext context, MenuController controller, Widget? child) {
|
||||
// Since we don't want to use the theme style or default style from the
|
||||
@ -1857,16 +1877,6 @@ class _SubmenuButtonState extends State<SubmenuButton> {
|
||||
controller.close();
|
||||
} else {
|
||||
controller.open();
|
||||
if (!_waitingToFocusMenu) {
|
||||
// Only schedule this if it's not already scheduled.
|
||||
SchedulerBinding.instance.addPostFrameCallback((Duration _) {
|
||||
// This has to happen in the next frame because the menu bar is
|
||||
// not focusable until the first menu is open.
|
||||
controller._anchor?._focusButton();
|
||||
_waitingToFocusMenu = false;
|
||||
});
|
||||
_waitingToFocusMenu = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -142,19 +142,21 @@ void main() {
|
||||
}
|
||||
|
||||
testWidgets('Menu responds to density changes', (WidgetTester tester) async {
|
||||
Widget buildMenu({VisualDensity? visualDensity = VisualDensity.standard}) => MaterialApp(
|
||||
theme: ThemeData(visualDensity: visualDensity),
|
||||
home: Material(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
MenuBar(
|
||||
children: createTestMenus(onPressed: onPressed),
|
||||
),
|
||||
const Expanded(child: Placeholder()),
|
||||
],
|
||||
Widget buildMenu({VisualDensity? visualDensity = VisualDensity.standard}) {
|
||||
return MaterialApp(
|
||||
theme: ThemeData(visualDensity: visualDensity),
|
||||
home: Material(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
MenuBar(
|
||||
children: createTestMenus(onPressed: onPressed),
|
||||
),
|
||||
const Expanded(child: Placeholder()),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(buildMenu());
|
||||
await tester.pump();
|
||||
@ -947,27 +949,27 @@ void main() {
|
||||
|
||||
testWidgets('MenuAnchor clip behavior', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Material(
|
||||
child: Center(
|
||||
child: MenuAnchor(
|
||||
menuChildren: const <Widget> [
|
||||
MenuItemButton(
|
||||
child: Text('Button 1'),
|
||||
),
|
||||
],
|
||||
builder: (BuildContext context, MenuController controller, Widget? child) {
|
||||
return FilledButton(
|
||||
onPressed: () {
|
||||
controller.open();
|
||||
},
|
||||
child: const Text('Tap me'),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
MaterialApp(
|
||||
home: Material(
|
||||
child: Center(
|
||||
child: MenuAnchor(
|
||||
menuChildren: const <Widget>[
|
||||
MenuItemButton(
|
||||
child: Text('Button 1'),
|
||||
),
|
||||
],
|
||||
builder: (BuildContext context, MenuController controller, Widget? child) {
|
||||
return FilledButton(
|
||||
onPressed: () {
|
||||
controller.open();
|
||||
},
|
||||
child: const Text('Tap me'),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.tap(find.text('Tap me'));
|
||||
await tester.pump();
|
||||
@ -977,28 +979,28 @@ void main() {
|
||||
await tester.tapAt(const Offset(10.0, 10.0));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Material(
|
||||
child: Center(
|
||||
child: MenuAnchor(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
menuChildren: const <Widget> [
|
||||
MenuItemButton(
|
||||
child: Text('Button 1'),
|
||||
),
|
||||
],
|
||||
builder: (BuildContext context, MenuController controller, Widget? child) {
|
||||
return FilledButton(
|
||||
onPressed: () {
|
||||
controller.open();
|
||||
},
|
||||
child: const Text('Tap me'),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
MaterialApp(
|
||||
home: Material(
|
||||
child: Center(
|
||||
child: MenuAnchor(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
menuChildren: const <Widget>[
|
||||
MenuItemButton(
|
||||
child: Text('Button 1'),
|
||||
),
|
||||
],
|
||||
builder: (BuildContext context, MenuController controller, Widget? child) {
|
||||
return FilledButton(
|
||||
onPressed: () {
|
||||
controller.open();
|
||||
},
|
||||
child: const Text('Tap me'),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.tap(find.text('Tap me'));
|
||||
await tester.pump();
|
||||
@ -1589,14 +1591,23 @@ void main() {
|
||||
int acceleratorIndex = -1;
|
||||
int count = 0;
|
||||
for (final String key in expected.keys) {
|
||||
expect(MenuAcceleratorLabel.stripAcceleratorMarkers(key, setIndex: (int index) {
|
||||
expect(
|
||||
MenuAcceleratorLabel.stripAcceleratorMarkers(key, setIndex: (int index) {
|
||||
acceleratorIndex = index;
|
||||
}), equals(expected[key]),
|
||||
reason: "'$key' label doesn't match ${expected[key]}");
|
||||
expect(acceleratorIndex, equals(expectedIndices[count]),
|
||||
reason: "'$key' index doesn't match ${expectedIndices[count]}");
|
||||
expect(MenuAcceleratorLabel(key).hasAccelerator, equals(expectedHasAccelerator[count]),
|
||||
reason: "'$key' hasAccelerator isn't ${expectedHasAccelerator[count]}");
|
||||
}),
|
||||
equals(expected[key]),
|
||||
reason: "'$key' label doesn't match ${expected[key]}",
|
||||
);
|
||||
expect(
|
||||
acceleratorIndex,
|
||||
equals(expectedIndices[count]),
|
||||
reason: "'$key' index doesn't match ${expectedIndices[count]}",
|
||||
);
|
||||
expect(
|
||||
MenuAcceleratorLabel(key).hasAccelerator,
|
||||
equals(expectedHasAccelerator[count]),
|
||||
reason: "'$key' hasAccelerator isn't ${expectedHasAccelerator[count]}",
|
||||
);
|
||||
count += 1;
|
||||
}
|
||||
});
|
||||
@ -2064,6 +2075,63 @@ void main() {
|
||||
expect(find.text('trailingIcon'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('SubmenuButton uses supplied controller', (WidgetTester tester) async {
|
||||
final MenuController submenuController = MenuController();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Material(
|
||||
child: MenuBar(
|
||||
controller: controller,
|
||||
children: <Widget>[
|
||||
SubmenuButton(
|
||||
controller: submenuController,
|
||||
menuChildren: <Widget>[
|
||||
MenuItemButton(
|
||||
child: Text(TestMenu.subMenu00.label),
|
||||
),
|
||||
],
|
||||
child: Text(TestMenu.mainMenu0.label),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
submenuController.open();
|
||||
await tester.pump();
|
||||
expect(find.text(TestMenu.subMenu00.label), findsOneWidget);
|
||||
|
||||
submenuController.close();
|
||||
await tester.pump();
|
||||
expect(find.text(TestMenu.subMenu00.label), findsNothing);
|
||||
|
||||
// Now remove the controller and try to control it.
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Material(
|
||||
child: MenuBar(
|
||||
controller: controller,
|
||||
children: <Widget>[
|
||||
SubmenuButton(
|
||||
menuChildren: <Widget>[
|
||||
MenuItemButton(
|
||||
child: Text(TestMenu.subMenu00.label),
|
||||
),
|
||||
],
|
||||
child: Text(TestMenu.mainMenu0.label),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await expectLater(() => submenuController.open(), throwsAssertionError);
|
||||
await tester.pump();
|
||||
expect(find.text(TestMenu.subMenu00.label), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('diagnostics', (WidgetTester tester) async {
|
||||
final ButtonStyle style = ButtonStyle(
|
||||
shape: MaterialStateProperty.all<OutlinedBorder?>(const StadiumBorder()),
|
||||
@ -2123,6 +2191,78 @@ void main() {
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('MenuItemButton respects closeOnActivate property', (WidgetTester tester) async {
|
||||
final MenuController controller = MenuController();
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: Material(
|
||||
child: Center(
|
||||
child: MenuAnchor(
|
||||
controller: controller,
|
||||
menuChildren: <Widget>[
|
||||
MenuItemButton(
|
||||
onPressed: () {},
|
||||
child: const Text('Button 1'),
|
||||
),
|
||||
],
|
||||
builder: (BuildContext context, MenuController controller, Widget? child) {
|
||||
return FilledButton(
|
||||
onPressed: () {
|
||||
controller.open();
|
||||
},
|
||||
child: const Text('Tap me'),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
await tester.tap(find.text('Tap me'));
|
||||
await tester.pump();
|
||||
expect(find.byType(MenuItemButton), findsNWidgets(1));
|
||||
|
||||
// Taps the MenuItemButton which should close the menu
|
||||
await tester.tap(find.text('Button 1'));
|
||||
await tester.pump();
|
||||
expect(find.byType(MenuItemButton), findsNWidgets(0));
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: Material(
|
||||
child: Center(
|
||||
child: MenuAnchor(
|
||||
controller: controller,
|
||||
menuChildren: <Widget>[
|
||||
MenuItemButton(
|
||||
closeOnActivate: false,
|
||||
onPressed: () {},
|
||||
child: const Text('Button 1'),
|
||||
),
|
||||
],
|
||||
builder: (BuildContext context, MenuController controller, Widget? child) {
|
||||
return FilledButton(
|
||||
onPressed: () {
|
||||
controller.open();
|
||||
},
|
||||
child: const Text('Tap me'),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
await tester.tap(find.text('Tap me'));
|
||||
await tester.pump();
|
||||
expect(find.byType(MenuItemButton), findsNWidgets(1));
|
||||
|
||||
// Taps the MenuItemButton which shouldn't close the menu
|
||||
await tester.tap(find.text('Button 1'));
|
||||
await tester.pump();
|
||||
expect(find.byType(MenuItemButton), findsNWidgets(1));
|
||||
});
|
||||
});
|
||||
|
||||
group('Layout', () {
|
||||
@ -2338,18 +2478,17 @@ void main() {
|
||||
child: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: MenuAnchor(
|
||||
menuChildren: const <Widget> [
|
||||
menuChildren: const <Widget>[
|
||||
SubmenuButton(
|
||||
alignmentOffset: Offset(10, 0),
|
||||
menuChildren: <Widget> [
|
||||
menuChildren: <Widget>[
|
||||
SubmenuButton(
|
||||
menuChildren: <Widget> [
|
||||
menuChildren: <Widget>[
|
||||
SubmenuButton(
|
||||
alignmentOffset: Offset(10, 0),
|
||||
menuChildren: <Widget> [
|
||||
menuChildren: <Widget>[
|
||||
SubmenuButton(
|
||||
menuChildren: <Widget> [
|
||||
],
|
||||
menuChildren: <Widget>[],
|
||||
child: Text('SubMenuButton4'),
|
||||
),
|
||||
],
|
||||
@ -2415,18 +2554,17 @@ void main() {
|
||||
child: Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: MenuAnchor(
|
||||
menuChildren: const <Widget> [
|
||||
menuChildren: const <Widget>[
|
||||
SubmenuButton(
|
||||
alignmentOffset: Offset(10, 0),
|
||||
menuChildren: <Widget> [
|
||||
menuChildren: <Widget>[
|
||||
SubmenuButton(
|
||||
menuChildren: <Widget> [
|
||||
menuChildren: <Widget>[
|
||||
SubmenuButton(
|
||||
alignmentOffset: Offset(10, 0),
|
||||
menuChildren: <Widget> [
|
||||
menuChildren: <Widget>[
|
||||
SubmenuButton(
|
||||
menuChildren: <Widget> [
|
||||
],
|
||||
menuChildren: <Widget>[],
|
||||
child: Text('SubMenuButton4'),
|
||||
),
|
||||
],
|
||||
@ -2492,8 +2630,9 @@ void main() {
|
||||
child: Align(
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: MenuAnchor(
|
||||
menuChildren: const <Widget> [
|
||||
MenuItemButton(child: Text('Button1'),
|
||||
menuChildren: const <Widget>[
|
||||
MenuItemButton(
|
||||
child: Text('Button1'),
|
||||
),
|
||||
],
|
||||
builder: (BuildContext context, MenuController controller, Widget? child) {
|
||||
@ -2530,7 +2669,8 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('vertically constrained menus are positioned above the anchor with the provided offset', (WidgetTester tester) async {
|
||||
testWidgets('vertically constrained menus are positioned above the anchor with the provided offset',
|
||||
(WidgetTester tester) async {
|
||||
await changeSurfaceSize(tester, const Size(800, 600));
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
@ -2542,8 +2682,9 @@ void main() {
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: MenuAnchor(
|
||||
alignmentOffset: const Offset(0, 50),
|
||||
menuChildren: const <Widget> [
|
||||
MenuItemButton(child: Text('Button1'),
|
||||
menuChildren: const <Widget>[
|
||||
MenuItemButton(
|
||||
child: Text('Button1'),
|
||||
),
|
||||
],
|
||||
builder: (BuildContext context, MenuController controller, Widget? child) {
|
||||
@ -2580,7 +2721,8 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
Future<void> buildDensityPaddingApp(WidgetTester tester, {
|
||||
Future<void> buildDensityPaddingApp(
|
||||
WidgetTester tester, {
|
||||
required TextDirection textDirection,
|
||||
VisualDensity visualDensity = VisualDensity.standard,
|
||||
EdgeInsetsGeometry? menuPadding,
|
||||
@ -2595,8 +2737,8 @@ void main() {
|
||||
children: <Widget>[
|
||||
MenuBar(
|
||||
style: menuPadding != null
|
||||
? MenuStyle(padding: MaterialStatePropertyAll<EdgeInsetsGeometry>(menuPadding))
|
||||
: null,
|
||||
? MenuStyle(padding: MaterialStatePropertyAll<EdgeInsetsGeometry>(menuPadding))
|
||||
: null,
|
||||
children: createTestMenus(onPressed: onPressed),
|
||||
),
|
||||
const Expanded(child: Placeholder()),
|
||||
@ -2898,82 +3040,6 @@ void main() {
|
||||
expect(radioValue, 1);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('MenuItemButton respects closeOnActivate property', (WidgetTester tester) async {
|
||||
final MenuController controller = MenuController();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Material(
|
||||
child: Center(
|
||||
child: MenuAnchor(
|
||||
controller: controller,
|
||||
menuChildren: <Widget> [
|
||||
MenuItemButton(
|
||||
onPressed: () {},
|
||||
child: const Text('Button 1'),
|
||||
),
|
||||
],
|
||||
builder: (BuildContext context, MenuController controller, Widget? child) {
|
||||
return FilledButton(
|
||||
onPressed: () {
|
||||
controller.open();
|
||||
},
|
||||
child: const Text('Tap me'),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Tap me'));
|
||||
await tester.pump();
|
||||
expect(find.byType(MenuItemButton), findsNWidgets(1));
|
||||
|
||||
// Taps the MenuItemButton which should close the menu
|
||||
await tester.tap(find.text('Button 1'));
|
||||
await tester.pump();
|
||||
expect(find.byType(MenuItemButton), findsNWidgets(0));
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Material(
|
||||
child: Center(
|
||||
child: MenuAnchor(
|
||||
controller: controller,
|
||||
menuChildren: <Widget> [
|
||||
MenuItemButton(
|
||||
closeOnActivate: false,
|
||||
onPressed: () {},
|
||||
child: const Text('Button 1'),
|
||||
),
|
||||
],
|
||||
builder: (BuildContext context, MenuController controller, Widget? child) {
|
||||
return FilledButton(
|
||||
onPressed: () {
|
||||
controller.open();
|
||||
},
|
||||
child: const Text('Tap me'),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Tap me'));
|
||||
await tester.pump();
|
||||
expect(find.byType(MenuItemButton), findsNWidgets(1));
|
||||
|
||||
// Taps the MenuItemButton which shouldn't close the menu
|
||||
await tester.tap(find.text('Button 1'));
|
||||
await tester.pump();
|
||||
expect(find.byType(MenuItemButton), findsNWidgets(1));
|
||||
});
|
||||
}
|
||||
|
||||
List<Widget> createTestMenus({
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user