mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Create DropdownMenu
Widget to Support Material 3 (#116088)
* Created ComboBox * Fixed failing tests * Reverted the menu style tests change * Addressed comments * Updated documentation and rename foregroundColor variable * Remamed ComboBox to DropdownMenu * Removed a unused import * Removed unused import Co-authored-by: Qun Cheng <quncheng@google.com>
This commit is contained in:
parent
1cb16a1e39
commit
8b86d238b4
74
examples/api/lib/material/dropdown_menu/dropdown_menu.0.dart
Normal file
74
examples/api/lib/material/dropdown_menu/dropdown_menu.0.dart
Normal file
@ -0,0 +1,74 @@
|
||||
// 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 [DropdownMenu]s. The first dropdown menu has an outlined border
|
||||
/// which is the default configuration, and the second one has a filled input decoration.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
void main() => runApp(const DropdownMenuExample());
|
||||
|
||||
class DropdownMenuExample extends StatelessWidget {
|
||||
const DropdownMenuExample({super.key});
|
||||
|
||||
List<DropdownMenuEntry> getEntryList() {
|
||||
final List<DropdownMenuEntry> entries = <DropdownMenuEntry>[];
|
||||
|
||||
for (int index = 0; index < EntryLabel.values.length; index++) {
|
||||
// Disabled item 1, 2 and 6.
|
||||
final bool enabled = index != 1 && index != 2 && index != 6;
|
||||
entries.add(DropdownMenuEntry(label: EntryLabel.values[index].label, enabled: enabled));
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final List<DropdownMenuEntry> dropdownMenuEntries = getEntryList();
|
||||
|
||||
return MaterialApp(
|
||||
theme: ThemeData(
|
||||
useMaterial3: true,
|
||||
colorSchemeSeed: Colors.green
|
||||
),
|
||||
home: Scaffold(
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 20),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
DropdownMenu(
|
||||
label: const Text('Label'),
|
||||
dropdownMenuEntries: dropdownMenuEntries,
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
DropdownMenu(
|
||||
enableFilter: true,
|
||||
leadingIcon: const Icon(Icons.search),
|
||||
label: const Text('Label'),
|
||||
dropdownMenuEntries: dropdownMenuEntries,
|
||||
inputDecorationTheme: const InputDecorationTheme(filled: true),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum EntryLabel {
|
||||
item0('Item 0'),
|
||||
item1('Item 1'),
|
||||
item2('Item 2'),
|
||||
item3('Item 3'),
|
||||
item4('Item 4'),
|
||||
item5('Item 5'),
|
||||
item6('Item 6');
|
||||
|
||||
const EntryLabel(this.label);
|
||||
final String label;
|
||||
}
|
@ -77,6 +77,8 @@ export 'src/material/drawer.dart';
|
||||
export 'src/material/drawer_header.dart';
|
||||
export 'src/material/drawer_theme.dart';
|
||||
export 'src/material/dropdown.dart';
|
||||
export 'src/material/dropdown_menu.dart';
|
||||
export 'src/material/dropdown_menu_theme.dart';
|
||||
export 'src/material/elevated_button.dart';
|
||||
export 'src/material/elevated_button_theme.dart';
|
||||
export 'src/material/elevation_overlay.dart';
|
||||
|
751
packages/flutter/lib/src/material/dropdown_menu.dart
Normal file
751
packages/flutter/lib/src/material/dropdown_menu.dart
Normal file
@ -0,0 +1,751 @@
|
||||
// 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:math' as math;
|
||||
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'button_style.dart';
|
||||
import 'dropdown_menu_theme.dart';
|
||||
import 'icon_button.dart';
|
||||
import 'icons.dart';
|
||||
import 'input_border.dart';
|
||||
import 'input_decorator.dart';
|
||||
import 'material_state.dart';
|
||||
import 'menu_anchor.dart';
|
||||
import 'menu_style.dart';
|
||||
import 'text_field.dart';
|
||||
import 'theme.dart';
|
||||
|
||||
|
||||
// Navigation shortcuts to move the selected menu items up or down.
|
||||
Map<ShortcutActivator, Intent> _kMenuTraversalShortcuts = <ShortcutActivator, Intent> {
|
||||
LogicalKeySet(LogicalKeyboardKey.arrowUp): const _ArrowUpIntent(),
|
||||
LogicalKeySet(LogicalKeyboardKey.arrowDown): const _ArrowDownIntent(),
|
||||
};
|
||||
|
||||
const double _kMinimumWidth = 112.0;
|
||||
|
||||
const double _kDefaultHorizontalPadding = 12.0;
|
||||
|
||||
/// Defines a [DropdownMenu] menu button that represents one item view in the menu.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [DropdownMenu]
|
||||
class DropdownMenuEntry {
|
||||
/// Creates an entry that is used with [DropdownMenu.dropdownMenuEntries].
|
||||
///
|
||||
/// [label] must be non-null.
|
||||
const DropdownMenuEntry({
|
||||
required this.label,
|
||||
this.leadingIcon,
|
||||
this.trailingIcon,
|
||||
this.enabled = true,
|
||||
this.style,
|
||||
});
|
||||
|
||||
/// The label displayed in the center of the menu item.
|
||||
final String label;
|
||||
|
||||
/// An optional icon to display before the label.
|
||||
final Widget? leadingIcon;
|
||||
|
||||
/// An optional icon to display after the label.
|
||||
final Widget? trailingIcon;
|
||||
|
||||
/// Whether the menu item is enabled or disabled.
|
||||
///
|
||||
/// The default value is true. If true, the [DropdownMenuEntry.label] will be filled
|
||||
/// out in the text field of the [DropdownMenu] when this entry is clicked; otherwise,
|
||||
/// this entry is disabled.
|
||||
final bool enabled;
|
||||
|
||||
/// Customizes this menu item's appearance.
|
||||
///
|
||||
/// Null by default.
|
||||
final ButtonStyle? style;
|
||||
}
|
||||
|
||||
|
||||
/// A dropdown menu that can be opened from a [TextField]. The selected
|
||||
/// menu item is displayed in that field.
|
||||
///
|
||||
/// This widget is used to help people make a choice from a menu and put the
|
||||
/// selected item into the text input field. People can also filter the list based
|
||||
/// on the text input or search one item in the menu list.
|
||||
///
|
||||
/// The menu is composed of a list of [DropdownMenuEntry]s. People can provide information,
|
||||
/// such as: label, leading icon or trailing icon for each entry. The [TextField]
|
||||
/// will be updated based on the selection from the menu entries. The text field
|
||||
/// will stay empty if the selected entry is disabled.
|
||||
///
|
||||
/// The dropdown menu can be traversed by pressing the up or down key. During the
|
||||
/// process, the corresponding item will be highlighted and the text field will be updated.
|
||||
/// Disabled items will be skipped during traversal.
|
||||
///
|
||||
/// The menu can be scrollable if not all items in the list are displayed at once.
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This sample shows how to display outlined [DropdownMenu] and filled [DropdownMenu].
|
||||
///
|
||||
/// ** See code in examples/api/lib/material/dropdown_menu/dropdown_menu.0.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [MenuAnchor], which is a widget used to mark the "anchor" for a set of submenus.
|
||||
/// The [DropdownMenu] uses a [TextField] as the "anchor".
|
||||
/// * [TextField], which is a text input widget that uses an [InputDecoration].
|
||||
/// * [DropdownMenuEntry], which is used to build the [MenuItemButton] in the [DropdownMenu] list.
|
||||
class DropdownMenu extends StatefulWidget {
|
||||
/// Creates a const [DropdownMenu].
|
||||
///
|
||||
/// The leading and trailing icons in the text field can be customized by using
|
||||
/// [leadingIcon], [trailingIcon] and [selectedTrailingIcon] properties. They are
|
||||
/// passed down to the [InputDecoration] properties, and will override values
|
||||
/// in the [InputDecoration.prefixIcon] and [InputDecoration.suffixIcon].
|
||||
///
|
||||
/// Except leading and trailing icons, the text field can be configured by the
|
||||
/// [InputDecorationTheme] property. The menu can be configured by the [menuStyle].
|
||||
const DropdownMenu({
|
||||
super.key,
|
||||
this.enabled = true,
|
||||
this.width,
|
||||
this.menuHeight,
|
||||
this.leadingIcon,
|
||||
this.trailingIcon,
|
||||
this.label,
|
||||
this.hintText,
|
||||
this.selectedTrailingIcon,
|
||||
this.enableFilter = false,
|
||||
this.enableSearch = true,
|
||||
this.textStyle,
|
||||
this.inputDecorationTheme,
|
||||
this.menuStyle,
|
||||
required this.dropdownMenuEntries,
|
||||
});
|
||||
|
||||
/// Determine if the [DropdownMenu] is enabled.
|
||||
///
|
||||
/// Defaults to true.
|
||||
final bool enabled;
|
||||
|
||||
/// Determine the width of the [DropdownMenu].
|
||||
///
|
||||
/// If this is null, the width of the [DropdownMenu] will be the same as the width of the widest
|
||||
/// menu item plus the width of the leading/trailing icon.
|
||||
final double? width;
|
||||
|
||||
/// Determine the height of the menu.
|
||||
///
|
||||
/// If this is null, the menu will display as many items as possible on the screen.
|
||||
final double? menuHeight;
|
||||
|
||||
/// An optional Icon at the front of the text input field.
|
||||
///
|
||||
/// Defaults to null. If this is not null, the menu items will have extra paddings to be aligned
|
||||
/// with the text in the text field.
|
||||
final Widget? leadingIcon;
|
||||
|
||||
/// An optional icon at the end of the text field.
|
||||
///
|
||||
/// Defaults to an [Icon] with [Icons.arrow_drop_down].
|
||||
final Widget? trailingIcon;
|
||||
|
||||
/// Optional widget that describes the input field.
|
||||
///
|
||||
/// When the input field is empty and unfocused, the label is displayed on
|
||||
/// top of the input field (i.e., at the same location on the screen where
|
||||
/// text may be entered in the input field). When the input field receives
|
||||
/// focus (or if the field is non-empty), the label moves above, either
|
||||
/// vertically adjacent to, or to the center of the input field.
|
||||
///
|
||||
/// Defaults to null.
|
||||
final Widget? label;
|
||||
|
||||
/// Text that suggests what sort of input the field accepts.
|
||||
///
|
||||
/// Defaults to null;
|
||||
final String? hintText;
|
||||
|
||||
/// An optional icon at the end of the text field to indicate that the text
|
||||
/// field is pressed.
|
||||
///
|
||||
/// Defaults to an [Icon] with [Icons.arrow_drop_up].
|
||||
final Widget? selectedTrailingIcon;
|
||||
|
||||
/// Determine if the menu list can be filtered by the text input.
|
||||
///
|
||||
/// Defaults to false.
|
||||
final bool enableFilter;
|
||||
|
||||
/// Determine if the first item that matches the text input can be highlighted.
|
||||
///
|
||||
/// Defaults to true as the search function could be commonly used.
|
||||
final bool enableSearch;
|
||||
|
||||
/// The text style for the [TextField] of the [DropdownMenu];
|
||||
///
|
||||
/// Defaults to the overall theme's [TextTheme.labelLarge]
|
||||
/// if the dropdown menu theme's value is null.
|
||||
final TextStyle? textStyle;
|
||||
|
||||
/// Defines the default appearance of [InputDecoration] to show around the text field.
|
||||
///
|
||||
/// By default, shows a outlined text field.
|
||||
final InputDecorationTheme? inputDecorationTheme;
|
||||
|
||||
/// The [MenuStyle] that defines the visual attributes of the menu.
|
||||
///
|
||||
/// The default width of the menu is set to the width of the text field.
|
||||
final MenuStyle? menuStyle;
|
||||
|
||||
/// Descriptions of the menu items in the [DropdownMenu].
|
||||
///
|
||||
/// This is a required parameter. It is recommended that at least one [DropdownMenuEntry]
|
||||
/// is provided. If this is an empty list, the menu will be empty and only
|
||||
/// contain space for padding.
|
||||
final List<DropdownMenuEntry> dropdownMenuEntries;
|
||||
|
||||
@override
|
||||
State<DropdownMenu> createState() => _DropdownMenuState();
|
||||
}
|
||||
|
||||
class _DropdownMenuState extends State<DropdownMenu> {
|
||||
final MenuController _controller = MenuController();
|
||||
final GlobalKey _anchorKey = GlobalKey();
|
||||
final GlobalKey _leadingKey = GlobalKey();
|
||||
final FocusNode _textFocusNode = FocusNode();
|
||||
final TextEditingController _textEditingController = TextEditingController();
|
||||
late bool _enableFilter;
|
||||
late List<DropdownMenuEntry> filteredEntries;
|
||||
List<Widget>? _initialMenu;
|
||||
int? currentHighlight;
|
||||
double? leadingPadding;
|
||||
bool _menuHasEnabledItem = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_enableFilter = widget.enableFilter;
|
||||
filteredEntries = widget.dropdownMenuEntries;
|
||||
_menuHasEnabledItem = filteredEntries.any((DropdownMenuEntry entry) => entry.enabled);
|
||||
|
||||
refreshLeadingPadding();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(DropdownMenu oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.dropdownMenuEntries != widget.dropdownMenuEntries) {
|
||||
_menuHasEnabledItem = filteredEntries.any((DropdownMenuEntry entry) => entry.enabled);
|
||||
}
|
||||
if (oldWidget.leadingIcon != widget.leadingIcon) {
|
||||
refreshLeadingPadding();
|
||||
}
|
||||
}
|
||||
|
||||
void refreshLeadingPadding() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
setState(() {
|
||||
leadingPadding = getWidth(_leadingKey);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
double? getWidth(GlobalKey key) {
|
||||
final BuildContext? context = key.currentContext;
|
||||
if (context != null) {
|
||||
final RenderBox box = context.findRenderObject()! as RenderBox;
|
||||
return box.size.width;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
List<DropdownMenuEntry> filter(List<DropdownMenuEntry> entries, TextEditingController textEditingController) {
|
||||
final String filterText = textEditingController.text.toLowerCase();
|
||||
return entries
|
||||
.where((DropdownMenuEntry entry) => entry.label.toLowerCase().contains(filterText))
|
||||
.toList();
|
||||
}
|
||||
|
||||
int? search(List<DropdownMenuEntry> entries, TextEditingController textEditingController) {
|
||||
final String searchText = textEditingController.value.text.toLowerCase();
|
||||
if (searchText.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final int index = entries.indexWhere((DropdownMenuEntry entry) => entry.label.toLowerCase().contains(searchText));
|
||||
|
||||
return index != -1 ? index : null;
|
||||
}
|
||||
|
||||
List<Widget> _buildButtons(
|
||||
List<DropdownMenuEntry> filteredEntries,
|
||||
TextEditingController textEditingController,
|
||||
TextDirection textDirection,
|
||||
{ int? focusedIndex }
|
||||
) {
|
||||
final List<Widget> result = <Widget>[];
|
||||
final double padding = leadingPadding ?? _kDefaultHorizontalPadding;
|
||||
final ButtonStyle defaultStyle;
|
||||
switch (textDirection) {
|
||||
case TextDirection.rtl:
|
||||
defaultStyle = MenuItemButton.styleFrom(
|
||||
padding: EdgeInsets.only(left: _kDefaultHorizontalPadding, right: padding),
|
||||
);
|
||||
break;
|
||||
case TextDirection.ltr:
|
||||
defaultStyle = MenuItemButton.styleFrom(
|
||||
padding: EdgeInsets.only(left: padding, right: _kDefaultHorizontalPadding),
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
for (int i = 0; i < filteredEntries.length; i++) {
|
||||
final DropdownMenuEntry entry = filteredEntries[i];
|
||||
ButtonStyle effectiveStyle = entry.style ?? defaultStyle;
|
||||
final Color focusedBackgroundColor = effectiveStyle.foregroundColor?.resolve(<MaterialState>{MaterialState.focused})
|
||||
?? Theme.of(context).colorScheme.onSurface;
|
||||
|
||||
// Simulate the focused state because the text field should always be focused
|
||||
// during traversal. If the menu item has a custom foreground color, the "focused"
|
||||
// color will also change to foregroundColor.withOpacity(0.12).
|
||||
effectiveStyle = entry.enabled && i == focusedIndex
|
||||
? effectiveStyle.copyWith(
|
||||
backgroundColor: MaterialStatePropertyAll<Color>(focusedBackgroundColor.withOpacity(0.12))
|
||||
)
|
||||
: effectiveStyle;
|
||||
|
||||
final MenuItemButton menuItemButton = MenuItemButton(
|
||||
style: effectiveStyle,
|
||||
leadingIcon: entry.leadingIcon,
|
||||
trailingIcon: entry.trailingIcon,
|
||||
onPressed: entry.enabled
|
||||
? () {
|
||||
textEditingController.text = entry.label;
|
||||
textEditingController.selection =
|
||||
TextSelection.collapsed(offset: textEditingController.text.length);
|
||||
currentHighlight = widget.enableSearch ? i : -1;
|
||||
}
|
||||
: null,
|
||||
requestFocusOnHover: false,
|
||||
child: Text(entry.label),
|
||||
);
|
||||
result.add(menuItemButton);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void handleUpKeyInvoke(_) => setState(() {
|
||||
if (!_menuHasEnabledItem || !_controller.isOpen) {
|
||||
return;
|
||||
}
|
||||
_enableFilter = false;
|
||||
currentHighlight ??= 0;
|
||||
currentHighlight = (currentHighlight! - 1) % filteredEntries.length;
|
||||
while (!filteredEntries[currentHighlight!].enabled) {
|
||||
currentHighlight = (currentHighlight! - 1) % filteredEntries.length;
|
||||
}
|
||||
_textEditingController.text = filteredEntries[currentHighlight!].label;
|
||||
_textEditingController.selection =
|
||||
TextSelection.collapsed(offset: _textEditingController.text.length);
|
||||
});
|
||||
|
||||
void handleDownKeyInvoke(_) => setState(() {
|
||||
if (!_menuHasEnabledItem || !_controller.isOpen) {
|
||||
return;
|
||||
}
|
||||
_enableFilter = false;
|
||||
currentHighlight ??= -1;
|
||||
currentHighlight = (currentHighlight! + 1) % filteredEntries.length;
|
||||
while (!filteredEntries[currentHighlight!].enabled) {
|
||||
currentHighlight = (currentHighlight! + 1) % filteredEntries.length;
|
||||
}
|
||||
_textEditingController.text = filteredEntries[currentHighlight!].label;
|
||||
_textEditingController.selection =
|
||||
TextSelection.collapsed(offset: _textEditingController.text.length);
|
||||
});
|
||||
|
||||
void handlePressed(MenuController controller) {
|
||||
if (controller.isOpen) {
|
||||
currentHighlight = -1;
|
||||
controller.close();
|
||||
} else { // close to open
|
||||
if (_textEditingController.text.isNotEmpty) {
|
||||
_enableFilter = false;
|
||||
}
|
||||
controller.open();
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_textEditingController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final TextDirection textDirection = Directionality.of(context);
|
||||
_initialMenu ??= _buildButtons(widget.dropdownMenuEntries, _textEditingController, textDirection);
|
||||
final DropdownMenuThemeData theme = DropdownMenuTheme.of(context);
|
||||
final DropdownMenuThemeData defaults = _DropdownMenuDefaultsM3(context);
|
||||
|
||||
if (_enableFilter) {
|
||||
filteredEntries = filter(widget.dropdownMenuEntries, _textEditingController);
|
||||
}
|
||||
|
||||
if (widget.enableSearch) {
|
||||
currentHighlight = search(filteredEntries, _textEditingController);
|
||||
}
|
||||
|
||||
final List<Widget> menu = _buildButtons(filteredEntries, _textEditingController, textDirection, focusedIndex: currentHighlight);
|
||||
|
||||
final TextStyle? effectiveTextStyle = widget.textStyle ?? theme.textStyle ?? defaults.textStyle;
|
||||
|
||||
MenuStyle? effectiveMenuStyle = widget.menuStyle
|
||||
?? theme.menuStyle
|
||||
?? defaults.menuStyle!;
|
||||
|
||||
final double? anchorWidth = getWidth(_anchorKey);
|
||||
if (widget.width != null) {
|
||||
effectiveMenuStyle = effectiveMenuStyle.copyWith(minimumSize: MaterialStatePropertyAll<Size?>(Size(widget.width!, 0.0)));
|
||||
} else if (anchorWidth != null){
|
||||
effectiveMenuStyle = effectiveMenuStyle.copyWith(minimumSize: MaterialStatePropertyAll<Size?>(Size(anchorWidth, 0.0)));
|
||||
}
|
||||
|
||||
if (widget.menuHeight != null) {
|
||||
effectiveMenuStyle = effectiveMenuStyle.copyWith(maximumSize: MaterialStatePropertyAll<Size>(Size(double.infinity, widget.menuHeight!)));
|
||||
}
|
||||
final InputDecorationTheme effectiveInputDecorationTheme = widget.inputDecorationTheme
|
||||
?? theme.inputDecorationTheme
|
||||
?? defaults.inputDecorationTheme!;
|
||||
|
||||
return Shortcuts(
|
||||
shortcuts: _kMenuTraversalShortcuts,
|
||||
child: Actions(
|
||||
actions: <Type, Action<Intent>>{
|
||||
_ArrowUpIntent: CallbackAction<_ArrowUpIntent>(
|
||||
onInvoke: handleUpKeyInvoke,
|
||||
),
|
||||
_ArrowDownIntent: CallbackAction<_ArrowDownIntent>(
|
||||
onInvoke: handleDownKeyInvoke,
|
||||
),
|
||||
},
|
||||
child: MenuAnchor(
|
||||
style: effectiveMenuStyle,
|
||||
controller: _controller,
|
||||
menuChildren: menu,
|
||||
crossAxisUnconstrained: false,
|
||||
builder: (BuildContext context, MenuController controller, Widget? child) {
|
||||
assert(_initialMenu != null);
|
||||
final Widget trailingButton = Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: IconButton(
|
||||
isSelected: controller.isOpen,
|
||||
icon: widget.trailingIcon ?? const Icon(Icons.arrow_drop_down),
|
||||
selectedIcon: widget.selectedTrailingIcon ?? const Icon(Icons.arrow_drop_up),
|
||||
onPressed: () {
|
||||
_textFocusNode.requestFocus();
|
||||
handlePressed(controller);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
final Widget leadingButton = Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: widget.leadingIcon ?? const SizedBox()
|
||||
);
|
||||
|
||||
return _DropdownMenuBody(
|
||||
key: _anchorKey,
|
||||
width: widget.width,
|
||||
children: <Widget>[
|
||||
TextField(
|
||||
focusNode: _textFocusNode,
|
||||
style: effectiveTextStyle,
|
||||
controller: _textEditingController,
|
||||
onEditingComplete: () {
|
||||
if (currentHighlight != null) {
|
||||
_textEditingController.text = filteredEntries[currentHighlight!].label;
|
||||
_textEditingController.selection =
|
||||
TextSelection.collapsed(offset: _textEditingController.text.length);
|
||||
}
|
||||
controller.close();
|
||||
},
|
||||
onTap: () {
|
||||
handlePressed(controller);
|
||||
},
|
||||
onChanged: (_) {
|
||||
controller.open();
|
||||
setState(() {
|
||||
filteredEntries = widget.dropdownMenuEntries;
|
||||
_enableFilter = widget.enableFilter;
|
||||
});
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
enabled: widget.enabled,
|
||||
label: widget.label,
|
||||
hintText: widget.hintText,
|
||||
prefixIcon: widget.leadingIcon != null ? Container(
|
||||
key: _leadingKey,
|
||||
child: widget.leadingIcon
|
||||
) : null,
|
||||
suffixIcon: trailingButton,
|
||||
).applyDefaults(effectiveInputDecorationTheme)
|
||||
),
|
||||
for (Widget c in _initialMenu!) c,
|
||||
trailingButton,
|
||||
leadingButton,
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ArrowUpIntent extends Intent {
|
||||
const _ArrowUpIntent();
|
||||
}
|
||||
|
||||
class _ArrowDownIntent extends Intent {
|
||||
const _ArrowDownIntent();
|
||||
}
|
||||
|
||||
class _DropdownMenuBody extends MultiChildRenderObjectWidget {
|
||||
_DropdownMenuBody({
|
||||
super.key,
|
||||
super.children,
|
||||
this.width,
|
||||
});
|
||||
|
||||
final double? width;
|
||||
|
||||
@override
|
||||
_RenderDropdownMenuBody createRenderObject(BuildContext context) {
|
||||
return _RenderDropdownMenuBody(
|
||||
width: width,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DropdownMenuBodyParentData extends ContainerBoxParentData<RenderBox> { }
|
||||
|
||||
class _RenderDropdownMenuBody extends RenderBox
|
||||
with ContainerRenderObjectMixin<RenderBox, _DropdownMenuBodyParentData>,
|
||||
RenderBoxContainerDefaultsMixin<RenderBox, _DropdownMenuBodyParentData> {
|
||||
|
||||
_RenderDropdownMenuBody({
|
||||
this.width,
|
||||
});
|
||||
|
||||
final double? width;
|
||||
|
||||
@override
|
||||
void setupParentData(RenderBox child) {
|
||||
if (child.parentData is! _DropdownMenuBodyParentData) {
|
||||
child.parentData = _DropdownMenuBodyParentData();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
final BoxConstraints constraints = this.constraints;
|
||||
double maxWidth = 0.0;
|
||||
double? maxHeight;
|
||||
RenderBox? child = firstChild;
|
||||
|
||||
final BoxConstraints innerConstraints = BoxConstraints(
|
||||
maxWidth: width ?? computeMaxIntrinsicWidth(constraints.maxWidth),
|
||||
maxHeight: computeMaxIntrinsicHeight(constraints.maxHeight),
|
||||
);
|
||||
while (child != null) {
|
||||
if (child == firstChild) {
|
||||
child.layout(innerConstraints, parentUsesSize: true);
|
||||
maxHeight ??= child.size.height;
|
||||
final _DropdownMenuBodyParentData childParentData = child.parentData! as _DropdownMenuBodyParentData;
|
||||
assert(child.parentData == childParentData);
|
||||
child = childParentData.nextSibling;
|
||||
continue;
|
||||
}
|
||||
child.layout(innerConstraints, parentUsesSize: true);
|
||||
final _DropdownMenuBodyParentData childParentData = child.parentData! as _DropdownMenuBodyParentData;
|
||||
childParentData.offset = Offset.zero;
|
||||
maxWidth = math.max(maxWidth, child.size.width);
|
||||
maxHeight ??= child.size.height;
|
||||
assert(child.parentData == childParentData);
|
||||
child = childParentData.nextSibling;
|
||||
}
|
||||
|
||||
assert(maxHeight != null);
|
||||
maxWidth = math.max(_kMinimumWidth, maxWidth);
|
||||
size = constraints.constrain(Size(width ?? maxWidth, maxHeight!));
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
final RenderBox? child = firstChild;
|
||||
if (child != null) {
|
||||
final _DropdownMenuBodyParentData childParentData = child.parentData! as _DropdownMenuBodyParentData;
|
||||
context.paintChild(child, offset + childParentData.offset);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Size computeDryLayout(BoxConstraints constraints) {
|
||||
final BoxConstraints constraints = this.constraints;
|
||||
double maxWidth = 0.0;
|
||||
double? maxHeight;
|
||||
RenderBox? child = firstChild;
|
||||
final BoxConstraints innerConstraints = BoxConstraints(
|
||||
maxWidth: width ?? computeMaxIntrinsicWidth(constraints.maxWidth),
|
||||
maxHeight: computeMaxIntrinsicHeight(constraints.maxHeight),
|
||||
);
|
||||
|
||||
while (child != null) {
|
||||
if (child == firstChild) {
|
||||
final Size childSize = child.getDryLayout(innerConstraints);
|
||||
maxHeight ??= childSize.height;
|
||||
final _DropdownMenuBodyParentData childParentData = child.parentData! as _DropdownMenuBodyParentData;
|
||||
assert(child.parentData == childParentData);
|
||||
child = childParentData.nextSibling;
|
||||
continue;
|
||||
}
|
||||
final Size childSize = child.getDryLayout(innerConstraints);
|
||||
final _DropdownMenuBodyParentData childParentData = child.parentData! as _DropdownMenuBodyParentData;
|
||||
childParentData.offset = Offset.zero;
|
||||
maxWidth = math.max(maxWidth, childSize.width);
|
||||
maxHeight ??= childSize.height;
|
||||
assert(child.parentData == childParentData);
|
||||
child = childParentData.nextSibling;
|
||||
}
|
||||
|
||||
assert(maxHeight != null);
|
||||
maxWidth = math.max(_kMinimumWidth, maxWidth);
|
||||
return constraints.constrain(Size(width ?? maxWidth, maxHeight!));
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMinIntrinsicWidth(double height) {
|
||||
RenderBox? child = firstChild;
|
||||
double width = 0;
|
||||
while (child != null) {
|
||||
if (child == firstChild) {
|
||||
final _DropdownMenuBodyParentData childParentData = child.parentData! as _DropdownMenuBodyParentData;
|
||||
child = childParentData.nextSibling;
|
||||
continue;
|
||||
}
|
||||
final double maxIntrinsicWidth = child.getMinIntrinsicWidth(height);
|
||||
if (child == lastChild) {
|
||||
width += maxIntrinsicWidth;
|
||||
}
|
||||
if (child == childBefore(lastChild!)) {
|
||||
width += maxIntrinsicWidth;
|
||||
}
|
||||
width = math.max(width, maxIntrinsicWidth);
|
||||
final _DropdownMenuBodyParentData childParentData = child.parentData! as _DropdownMenuBodyParentData;
|
||||
child = childParentData.nextSibling;
|
||||
}
|
||||
|
||||
return math.max(width, _kMinimumWidth);
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMaxIntrinsicWidth(double height) {
|
||||
RenderBox? child = firstChild;
|
||||
double width = 0;
|
||||
while (child != null) {
|
||||
if (child == firstChild) {
|
||||
final _DropdownMenuBodyParentData childParentData = child.parentData! as _DropdownMenuBodyParentData;
|
||||
child = childParentData.nextSibling;
|
||||
continue;
|
||||
}
|
||||
final double maxIntrinsicWidth = child.getMaxIntrinsicWidth(height);
|
||||
// Add the width of leading Icon.
|
||||
if (child == lastChild) {
|
||||
width += maxIntrinsicWidth;
|
||||
}
|
||||
// Add the width of trailing Icon.
|
||||
if (child == childBefore(lastChild!)) {
|
||||
width += maxIntrinsicWidth;
|
||||
}
|
||||
width = math.max(width, maxIntrinsicWidth);
|
||||
final _DropdownMenuBodyParentData childParentData = child.parentData! as _DropdownMenuBodyParentData;
|
||||
child = childParentData.nextSibling;
|
||||
}
|
||||
|
||||
return math.max(width, _kMinimumWidth);
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMinIntrinsicHeight(double height) {
|
||||
final RenderBox? child = firstChild;
|
||||
double width = 0;
|
||||
if (child != null) {
|
||||
width = math.max(width, child.getMinIntrinsicHeight(height));
|
||||
}
|
||||
return width;
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMaxIntrinsicHeight(double height) {
|
||||
final RenderBox? child = firstChild;
|
||||
double width = 0;
|
||||
if (child != null) {
|
||||
width = math.max(width, child.getMaxIntrinsicHeight(height));
|
||||
}
|
||||
return width;
|
||||
}
|
||||
|
||||
@override
|
||||
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
|
||||
final RenderBox? child = firstChild;
|
||||
if (child != null) {
|
||||
final _DropdownMenuBodyParentData childParentData = child.parentData! as _DropdownMenuBodyParentData;
|
||||
final bool isHit = result.addWithPaintOffset(
|
||||
offset: childParentData.offset,
|
||||
position: position,
|
||||
hitTest: (BoxHitTestResult result, Offset transformed) {
|
||||
assert(transformed == position - childParentData.offset);
|
||||
return child.hitTest(result, position: transformed);
|
||||
},
|
||||
);
|
||||
if (isHit) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Hand coded defaults. These will be updated once we have tokens/spec.
|
||||
class _DropdownMenuDefaultsM3 extends DropdownMenuThemeData {
|
||||
_DropdownMenuDefaultsM3(this.context);
|
||||
|
||||
final BuildContext context;
|
||||
late final ThemeData _theme = Theme.of(context);
|
||||
|
||||
@override
|
||||
TextStyle? get textStyle => _theme.textTheme.labelLarge;
|
||||
|
||||
@override
|
||||
MenuStyle get menuStyle {
|
||||
return const MenuStyle(
|
||||
minimumSize: MaterialStatePropertyAll<Size>(Size(_kMinimumWidth, 0.0)),
|
||||
maximumSize: MaterialStatePropertyAll<Size>(Size.infinite),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
InputDecorationTheme get inputDecorationTheme {
|
||||
return const InputDecorationTheme(border: OutlineInputBorder());
|
||||
}
|
||||
}
|
176
packages/flutter/lib/src/material/dropdown_menu_theme.dart
Normal file
176
packages/flutter/lib/src/material/dropdown_menu_theme.dart
Normal file
@ -0,0 +1,176 @@
|
||||
// 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/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'input_decorator.dart';
|
||||
import 'menu_style.dart';
|
||||
import 'theme.dart';
|
||||
|
||||
// Examples can assume:
|
||||
// late BuildContext context;
|
||||
|
||||
/// Overrides the default values of visual properties for descendant [DropdownMenu] widgets.
|
||||
///
|
||||
/// Descendant widgets obtain the current [DropdownMenuThemeData] object with
|
||||
/// [DropdownMenuTheme.of]. Instances of [DropdownMenuTheme] can
|
||||
/// be customized with [DropdownMenuThemeData.copyWith].
|
||||
///
|
||||
/// Typically a [DropdownMenuTheme] is specified as part of the overall [Theme] with
|
||||
/// [ThemeData.dropdownMenuTheme].
|
||||
///
|
||||
/// All [DropdownMenuThemeData] properties are null by default. When null, the [DropdownMenu]
|
||||
/// computes its own default values, typically based on the overall
|
||||
/// theme's [ThemeData.colorScheme], [ThemeData.textTheme], and [ThemeData.iconTheme].
|
||||
@immutable
|
||||
class DropdownMenuThemeData with Diagnosticable {
|
||||
/// Creates a [DropdownMenuThemeData] that can be used to override default properties
|
||||
/// in a [DropdownMenuTheme] widget.
|
||||
const DropdownMenuThemeData({
|
||||
this.textStyle,
|
||||
this.inputDecorationTheme,
|
||||
this.menuStyle,
|
||||
});
|
||||
|
||||
/// Overrides the default value for [DropdownMenu.textStyle].
|
||||
final TextStyle? textStyle;
|
||||
|
||||
/// The input decoration theme for the [TextField]s in a [DropdownMenu].
|
||||
///
|
||||
/// If this is null, the [DropdownMenu] provides its own defaults.
|
||||
final InputDecorationTheme? inputDecorationTheme;
|
||||
|
||||
/// Overrides the menu's default style in a [DropdownMenu].
|
||||
///
|
||||
/// Any values not set in the [MenuStyle] will use the menu default for that
|
||||
/// property.
|
||||
final MenuStyle? menuStyle;
|
||||
|
||||
/// Creates a copy of this object with the given fields replaced with the
|
||||
/// new values.
|
||||
DropdownMenuThemeData copyWith({
|
||||
TextStyle? textStyle,
|
||||
InputDecorationTheme? inputDecorationTheme,
|
||||
MenuStyle? menuStyle,
|
||||
}) {
|
||||
return DropdownMenuThemeData(
|
||||
textStyle: textStyle ?? this.textStyle,
|
||||
inputDecorationTheme: inputDecorationTheme ?? this.inputDecorationTheme,
|
||||
menuStyle: menuStyle ?? this.menuStyle,
|
||||
);
|
||||
}
|
||||
|
||||
/// Linearly interpolates between two dropdown menu themes.
|
||||
static DropdownMenuThemeData lerp(DropdownMenuThemeData? a, DropdownMenuThemeData? b, double t) {
|
||||
return DropdownMenuThemeData(
|
||||
textStyle: TextStyle.lerp(a?.textStyle, b?.textStyle, t),
|
||||
inputDecorationTheme: t < 0.5 ? a?.inputDecorationTheme : b?.inputDecorationTheme,
|
||||
menuStyle: MenuStyle.lerp(a?.menuStyle, b?.menuStyle, t),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
textStyle,
|
||||
inputDecorationTheme,
|
||||
menuStyle,
|
||||
);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
if (other.runtimeType != runtimeType) {
|
||||
return false;
|
||||
}
|
||||
return other is DropdownMenuThemeData
|
||||
&& other.textStyle == textStyle
|
||||
&& other.inputDecorationTheme == inputDecorationTheme
|
||||
&& other.menuStyle == menuStyle;
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DiagnosticsProperty<TextStyle>('textStyle', textStyle, defaultValue: null));
|
||||
properties.add(DiagnosticsProperty<InputDecorationTheme>('inputDecorationTheme', inputDecorationTheme, defaultValue: null));
|
||||
properties.add(DiagnosticsProperty<MenuStyle>('menuStyle', menuStyle, defaultValue: null));
|
||||
}
|
||||
}
|
||||
|
||||
/// An inherited widget that defines the visual properties for [DropdownMenu]s in this widget's subtree.
|
||||
///
|
||||
/// Values specified here are used for [DropdownMenu] properties that are not
|
||||
/// given an explicit non-null value.
|
||||
class DropdownMenuTheme extends InheritedTheme {
|
||||
/// Creates a [DropdownMenuTheme] that controls visual parameters for
|
||||
/// descendant [DropdownMenu]s.
|
||||
const DropdownMenuTheme({
|
||||
super.key,
|
||||
required this.data,
|
||||
required super.child,
|
||||
}) : assert(data != null);
|
||||
|
||||
/// Specifies the visual properties used by descendant [DropdownMenu]
|
||||
/// widgets.
|
||||
final DropdownMenuThemeData data;
|
||||
|
||||
/// The closest instance of this class that encloses the given context.
|
||||
///
|
||||
/// If there is no enclosing [DropdownMenuTheme] widget, then
|
||||
/// [ThemeData.dropdownMenuTheme] is used.
|
||||
///
|
||||
/// Typical usage is as follows:
|
||||
///
|
||||
/// ```dart
|
||||
/// DropdownMenuThemeData theme = DropdownMenuTheme.of(context);
|
||||
/// ```
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [maybeOf], which returns null if it doesn't find a
|
||||
/// [DropdownMenuTheme] ancestor.
|
||||
static DropdownMenuThemeData of(BuildContext context) {
|
||||
return maybeOf(context) ?? Theme.of(context).dropdownMenuTheme;
|
||||
}
|
||||
|
||||
/// The data from the closest instance of this class that encloses the given
|
||||
/// context, if any.
|
||||
///
|
||||
/// Use this function if you want to allow situations where no
|
||||
/// [DropdownMenuTheme] is in scope. Prefer using [DropdownMenuTheme.of]
|
||||
/// in situations where a [DropdownMenuThemeData] is expected to be
|
||||
/// non-null.
|
||||
///
|
||||
/// If there is no [DropdownMenuTheme] in scope, then this function will
|
||||
/// return null.
|
||||
///
|
||||
/// Typical usage is as follows:
|
||||
///
|
||||
/// ```dart
|
||||
/// DropdownMenuThemeData? theme = DropdownMenuTheme.maybeOf(context);
|
||||
/// if (theme == null) {
|
||||
/// // Do something else instead.
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [of], which will return [ThemeData.dropdownMenuTheme] if it doesn't
|
||||
/// find a [DropdownMenuTheme] ancestor, instead of returning null.
|
||||
static DropdownMenuThemeData? maybeOf(BuildContext context) {
|
||||
assert(context != null);
|
||||
return context.dependOnInheritedWidgetOfExactType<DropdownMenuTheme>()?.data;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget wrap(BuildContext context, Widget child) {
|
||||
return DropdownMenuTheme(data: data, child: child);
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(DropdownMenuTheme oldWidget) => data != oldWidget.data;
|
||||
}
|
@ -131,6 +131,7 @@ class MenuAnchor extends StatefulWidget {
|
||||
this.anchorTapClosesMenu = false,
|
||||
this.onOpen,
|
||||
this.onClose,
|
||||
this.crossAxisUnconstrained = true,
|
||||
required this.menuChildren,
|
||||
this.builder,
|
||||
this.child,
|
||||
@ -213,6 +214,14 @@ class MenuAnchor extends StatefulWidget {
|
||||
/// A callback that is invoked when the menu is closed.
|
||||
final VoidCallback? onClose;
|
||||
|
||||
/// Determine if the menu panel can be wrapped by a [UnconstrainedBox] which allows
|
||||
/// the panel to render at its "natural" size.
|
||||
///
|
||||
/// Defaults to true as it allows developers to render the menu panel at the
|
||||
/// size it should be. When it is set to false, it can be useful when the menu should
|
||||
/// be constrained in both main axis and cross axis, such as a [DropdownMenu].
|
||||
final bool crossAxisUnconstrained;
|
||||
|
||||
/// A list of children containing the menu items that are the contents of the
|
||||
/// menu surrounded by this [MenuAnchor].
|
||||
///
|
||||
@ -516,6 +525,7 @@ class _MenuAnchorState extends State<MenuAnchor> {
|
||||
menuPosition: position,
|
||||
clipBehavior: widget.clipBehavior,
|
||||
menuChildren: widget.menuChildren,
|
||||
crossAxisUnconstrained: widget.crossAxisUnconstrained,
|
||||
),
|
||||
),
|
||||
to: overlay.context,
|
||||
@ -791,6 +801,7 @@ class MenuItemButton extends StatefulWidget {
|
||||
super.key,
|
||||
this.onPressed,
|
||||
this.onHover,
|
||||
this.requestFocusOnHover = true,
|
||||
this.onFocusChange,
|
||||
this.focusNode,
|
||||
this.shortcut,
|
||||
@ -817,6 +828,11 @@ class MenuItemButton extends StatefulWidget {
|
||||
/// area and false if a pointer has exited.
|
||||
final ValueChanged<bool>? onHover;
|
||||
|
||||
/// Determine if hovering can request focus.
|
||||
///
|
||||
/// Defaults to true.
|
||||
final bool requestFocusOnHover;
|
||||
|
||||
/// Handler called when the focus changes.
|
||||
///
|
||||
/// Called with true if this widget's node gains focus, and false if it loses
|
||||
@ -1064,7 +1080,7 @@ class _MenuItemButtonState extends State<MenuItemButton> {
|
||||
|
||||
void _handleHover(bool hovering) {
|
||||
widget.onHover?.call(hovering);
|
||||
if (hovering) {
|
||||
if (hovering && widget.requestFocusOnHover) {
|
||||
assert(_debugMenuInfo('Requesting focus for $_focusNode from hover'));
|
||||
_focusNode.requestFocus();
|
||||
}
|
||||
@ -2424,7 +2440,10 @@ class _MenuDirectionalFocusAction extends DirectionalFocusAction {
|
||||
// correct node.
|
||||
if (currentMenu.widget.childFocusNode != null) {
|
||||
final FocusTraversalPolicy? policy = FocusTraversalGroup.maybeOf(primaryFocus!.context!);
|
||||
policy?.invalidateScopeData(currentMenu.widget.childFocusNode!.nearestScope!);
|
||||
if (currentMenu.widget.childFocusNode!.nearestScope != null) {
|
||||
policy?.invalidateScopeData(currentMenu.widget.childFocusNode!.nearestScope!);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@ -2455,7 +2474,10 @@ class _MenuDirectionalFocusAction extends DirectionalFocusAction {
|
||||
// correct node.
|
||||
if (currentMenu.widget.childFocusNode != null) {
|
||||
final FocusTraversalPolicy? policy = FocusTraversalGroup.maybeOf(primaryFocus!.context!);
|
||||
policy?.invalidateScopeData(currentMenu.widget.childFocusNode!.nearestScope!);
|
||||
if (currentMenu.widget.childFocusNode!.nearestScope != null) {
|
||||
policy?.invalidateScopeData(currentMenu.widget.childFocusNode!.nearestScope!);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@ -3198,6 +3220,7 @@ class _MenuPanel extends StatefulWidget {
|
||||
required this.menuStyle,
|
||||
this.clipBehavior = Clip.none,
|
||||
required this.orientation,
|
||||
this.crossAxisUnconstrained = true,
|
||||
required this.children,
|
||||
});
|
||||
|
||||
@ -3209,6 +3232,13 @@ class _MenuPanel extends StatefulWidget {
|
||||
/// Defaults to [Clip.none].
|
||||
final Clip clipBehavior;
|
||||
|
||||
/// Determine if a [UnconstrainedBox] can be applied to the menu panel to allow it to render
|
||||
/// at its "natural" size.
|
||||
///
|
||||
/// Defaults to true. When it is set to false, it can be useful when the menu should
|
||||
/// be constrained in both main-axis and cross-axis, such as a [DropdownMenu].
|
||||
final bool crossAxisUnconstrained;
|
||||
|
||||
/// The layout orientation of this panel.
|
||||
final Axis orientation;
|
||||
|
||||
@ -3297,38 +3327,45 @@ class _MenuPanelState extends State<_MenuPanel> {
|
||||
);
|
||||
}
|
||||
}
|
||||
return ConstrainedBox(
|
||||
constraints: effectiveConstraints,
|
||||
child: UnconstrainedBox(
|
||||
constrainedAxis: widget.orientation,
|
||||
|
||||
Widget menuPanel = _intrinsicCrossSize(
|
||||
child: Material(
|
||||
elevation: elevation,
|
||||
shape: shape,
|
||||
color: backgroundColor,
|
||||
shadowColor: shadowColor,
|
||||
surfaceTintColor: surfaceTintColor,
|
||||
type: backgroundColor == null ? MaterialType.transparency : MaterialType.canvas,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: _intrinsicCrossSize(
|
||||
child: Material(
|
||||
elevation: elevation,
|
||||
shape: shape,
|
||||
color: backgroundColor,
|
||||
shadowColor: shadowColor,
|
||||
surfaceTintColor: surfaceTintColor,
|
||||
type: backgroundColor == null ? MaterialType.transparency : MaterialType.canvas,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: Padding(
|
||||
padding: resolvedPadding,
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: widget.orientation,
|
||||
child: Flex(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
textDirection: Directionality.of(context),
|
||||
direction: widget.orientation,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: widget.children,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: resolvedPadding,
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: widget.orientation,
|
||||
child: Flex(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
textDirection: Directionality.of(context),
|
||||
direction: widget.orientation,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: widget.children,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (widget.crossAxisUnconstrained) {
|
||||
menuPanel = UnconstrainedBox(
|
||||
constrainedAxis: widget.orientation,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: menuPanel,
|
||||
);
|
||||
}
|
||||
|
||||
return ConstrainedBox(
|
||||
constraints: effectiveConstraints,
|
||||
child: menuPanel,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _intrinsicCrossSize({required Widget child}) {
|
||||
@ -3349,6 +3386,7 @@ class _Submenu extends StatelessWidget {
|
||||
required this.menuPosition,
|
||||
required this.alignmentOffset,
|
||||
required this.clipBehavior,
|
||||
this.crossAxisUnconstrained = true,
|
||||
required this.menuChildren,
|
||||
});
|
||||
|
||||
@ -3357,6 +3395,7 @@ class _Submenu extends StatelessWidget {
|
||||
final Offset? menuPosition;
|
||||
final Offset alignmentOffset;
|
||||
final Clip clipBehavior;
|
||||
final bool crossAxisUnconstrained;
|
||||
final List<Widget> menuChildren;
|
||||
|
||||
@override
|
||||
@ -3454,6 +3493,7 @@ class _Submenu extends StatelessWidget {
|
||||
menuStyle: menuStyle,
|
||||
clipBehavior: clipBehavior,
|
||||
orientation: anchor._orientation,
|
||||
crossAxisUnconstrained: crossAxisUnconstrained,
|
||||
children: menuChildren,
|
||||
),
|
||||
),
|
||||
|
@ -25,6 +25,7 @@ import 'data_table_theme.dart';
|
||||
import 'dialog_theme.dart';
|
||||
import 'divider_theme.dart';
|
||||
import 'drawer_theme.dart';
|
||||
import 'dropdown_menu_theme.dart';
|
||||
import 'elevated_button_theme.dart';
|
||||
import 'expansion_tile_theme.dart';
|
||||
import 'filled_button_theme.dart';
|
||||
@ -348,6 +349,7 @@ class ThemeData with Diagnosticable {
|
||||
DialogTheme? dialogTheme,
|
||||
DividerThemeData? dividerTheme,
|
||||
DrawerThemeData? drawerTheme,
|
||||
DropdownMenuThemeData? dropdownMenuTheme,
|
||||
ElevatedButtonThemeData? elevatedButtonTheme,
|
||||
ExpansionTileThemeData? expansionTileTheme,
|
||||
FilledButtonThemeData? filledButtonTheme,
|
||||
@ -602,6 +604,7 @@ class ThemeData with Diagnosticable {
|
||||
dialogTheme ??= const DialogTheme();
|
||||
dividerTheme ??= const DividerThemeData();
|
||||
drawerTheme ??= const DrawerThemeData();
|
||||
dropdownMenuTheme ??= const DropdownMenuThemeData();
|
||||
elevatedButtonTheme ??= const ElevatedButtonThemeData();
|
||||
expansionTileTheme ??= const ExpansionTileThemeData();
|
||||
filledButtonTheme ??= const FilledButtonThemeData();
|
||||
@ -699,6 +702,7 @@ class ThemeData with Diagnosticable {
|
||||
dialogTheme: dialogTheme,
|
||||
dividerTheme: dividerTheme,
|
||||
drawerTheme: drawerTheme,
|
||||
dropdownMenuTheme: dropdownMenuTheme,
|
||||
elevatedButtonTheme: elevatedButtonTheme,
|
||||
expansionTileTheme: expansionTileTheme,
|
||||
filledButtonTheme: filledButtonTheme,
|
||||
@ -812,6 +816,7 @@ class ThemeData with Diagnosticable {
|
||||
required this.dialogTheme,
|
||||
required this.dividerTheme,
|
||||
required this.drawerTheme,
|
||||
required this.dropdownMenuTheme,
|
||||
required this.elevatedButtonTheme,
|
||||
required this.expansionTileTheme,
|
||||
required this.filledButtonTheme,
|
||||
@ -983,6 +988,7 @@ class ThemeData with Diagnosticable {
|
||||
assert(dialogTheme != null),
|
||||
assert(dividerTheme != null),
|
||||
assert(drawerTheme != null),
|
||||
assert(dropdownMenuTheme != null),
|
||||
assert(elevatedButtonTheme != null),
|
||||
assert(expansionTileTheme != null),
|
||||
assert(filledButtonTheme != null),
|
||||
@ -1554,6 +1560,9 @@ class ThemeData with Diagnosticable {
|
||||
/// A theme for customizing the appearance and layout of [Drawer] widgets.
|
||||
final DrawerThemeData drawerTheme;
|
||||
|
||||
/// A theme for customizing the appearance and layout of [DropdownMenu] widgets.
|
||||
final DropdownMenuThemeData dropdownMenuTheme;
|
||||
|
||||
/// A theme for customizing the appearance and internal layout of
|
||||
/// [ElevatedButton]s.
|
||||
final ElevatedButtonThemeData elevatedButtonTheme;
|
||||
@ -1816,6 +1825,13 @@ class ThemeData with Diagnosticable {
|
||||
Color get toggleableActiveColor => _toggleableActiveColor!;
|
||||
final Color? _toggleableActiveColor;
|
||||
|
||||
// The number 5 was chosen without any real science or research behind it. It
|
||||
|
||||
// copies of ThemeData in memory comfortably) and not too small (most apps
|
||||
// shouldn't have more than 5 theme/localization pairs).
|
||||
static const int _localizedThemeDataCacheSize = 5;
|
||||
/// Caches localized themes to speed up the [localize] method.
|
||||
|
||||
/// Creates a copy of this theme but with the given fields replaced with the new values.
|
||||
///
|
||||
/// The [brightness] value is applied to the [colorScheme].
|
||||
@ -1883,6 +1899,7 @@ class ThemeData with Diagnosticable {
|
||||
DialogTheme? dialogTheme,
|
||||
DividerThemeData? dividerTheme,
|
||||
DrawerThemeData? drawerTheme,
|
||||
DropdownMenuThemeData? dropdownMenuTheme,
|
||||
ElevatedButtonThemeData? elevatedButtonTheme,
|
||||
ExpansionTileThemeData? expansionTileTheme,
|
||||
FilledButtonThemeData? filledButtonTheme,
|
||||
@ -2047,6 +2064,7 @@ class ThemeData with Diagnosticable {
|
||||
dialogTheme: dialogTheme ?? this.dialogTheme,
|
||||
dividerTheme: dividerTheme ?? this.dividerTheme,
|
||||
drawerTheme: drawerTheme ?? this.drawerTheme,
|
||||
dropdownMenuTheme: dropdownMenuTheme ?? this.dropdownMenuTheme,
|
||||
elevatedButtonTheme: elevatedButtonTheme ?? this.elevatedButtonTheme,
|
||||
expansionTileTheme: expansionTileTheme ?? this.expansionTileTheme,
|
||||
filledButtonTheme: filledButtonTheme ?? this.filledButtonTheme,
|
||||
@ -2089,14 +2107,7 @@ class ThemeData with Diagnosticable {
|
||||
bottomAppBarColor: bottomAppBarColor ?? _bottomAppBarColor,
|
||||
);
|
||||
}
|
||||
|
||||
// The number 5 was chosen without any real science or research behind it. It
|
||||
// just seemed like a number that's not too big (we should be able to fit 5
|
||||
// copies of ThemeData in memory comfortably) and not too small (most apps
|
||||
// shouldn't have more than 5 theme/localization pairs).
|
||||
static const int _localizedThemeDataCacheSize = 5;
|
||||
|
||||
/// Caches localized themes to speed up the [localize] method.
|
||||
static final _FifoCache<_IdentityThemeDataCacheKey, ThemeData> _localizedThemeDataCache =
|
||||
_FifoCache<_IdentityThemeDataCacheKey, ThemeData>(_localizedThemeDataCacheSize);
|
||||
|
||||
@ -2253,6 +2264,7 @@ class ThemeData with Diagnosticable {
|
||||
dialogTheme: DialogTheme.lerp(a.dialogTheme, b.dialogTheme, t),
|
||||
dividerTheme: DividerThemeData.lerp(a.dividerTheme, b.dividerTheme, t),
|
||||
drawerTheme: DrawerThemeData.lerp(a.drawerTheme, b.drawerTheme, t)!,
|
||||
dropdownMenuTheme: DropdownMenuThemeData.lerp(a.dropdownMenuTheme, b.dropdownMenuTheme, t),
|
||||
elevatedButtonTheme: ElevatedButtonThemeData.lerp(a.elevatedButtonTheme, b.elevatedButtonTheme, t)!,
|
||||
expansionTileTheme: ExpansionTileThemeData.lerp(a.expansionTileTheme, b.expansionTileTheme, t)!,
|
||||
filledButtonTheme: FilledButtonThemeData.lerp(a.filledButtonTheme, b.filledButtonTheme, t)!,
|
||||
@ -2361,6 +2373,7 @@ class ThemeData with Diagnosticable {
|
||||
other.dialogTheme == dialogTheme &&
|
||||
other.dividerTheme == dividerTheme &&
|
||||
other.drawerTheme == drawerTheme &&
|
||||
other.dropdownMenuTheme == dropdownMenuTheme &&
|
||||
other.elevatedButtonTheme == elevatedButtonTheme &&
|
||||
other.expansionTileTheme == expansionTileTheme &&
|
||||
other.filledButtonTheme == filledButtonTheme &&
|
||||
@ -2466,6 +2479,7 @@ class ThemeData with Diagnosticable {
|
||||
dialogTheme,
|
||||
dividerTheme,
|
||||
drawerTheme,
|
||||
dropdownMenuTheme,
|
||||
elevatedButtonTheme,
|
||||
expansionTileTheme,
|
||||
filledButtonTheme,
|
||||
@ -2573,6 +2587,7 @@ class ThemeData with Diagnosticable {
|
||||
properties.add(DiagnosticsProperty<DialogTheme>('dialogTheme', dialogTheme, defaultValue: defaultData.dialogTheme, level: DiagnosticLevel.debug));
|
||||
properties.add(DiagnosticsProperty<DividerThemeData>('dividerTheme', dividerTheme, defaultValue: defaultData.dividerTheme, level: DiagnosticLevel.debug));
|
||||
properties.add(DiagnosticsProperty<DrawerThemeData>('drawerTheme', drawerTheme, defaultValue: defaultData.drawerTheme, level: DiagnosticLevel.debug));
|
||||
properties.add(DiagnosticsProperty<DropdownMenuThemeData>('dropdownMenuTheme', dropdownMenuTheme, defaultValue: defaultData.dropdownMenuTheme, level: DiagnosticLevel.debug));
|
||||
properties.add(DiagnosticsProperty<ElevatedButtonThemeData>('elevatedButtonTheme', elevatedButtonTheme, defaultValue: defaultData.elevatedButtonTheme, level: DiagnosticLevel.debug));
|
||||
properties.add(DiagnosticsProperty<ExpansionTileThemeData>('expansionTileTheme', expansionTileTheme, level: DiagnosticLevel.debug));
|
||||
properties.add(DiagnosticsProperty<FilledButtonThemeData>('filledButtonTheme', filledButtonTheme, defaultValue: defaultData.filledButtonTheme, level: DiagnosticLevel.debug));
|
||||
|
762
packages/flutter/test/material/dropdown_menu_test.dart
Normal file
762
packages/flutter/test/material/dropdown_menu_test.dart
Normal file
@ -0,0 +1,762 @@
|
||||
// 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_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
final List<DropdownMenuEntry> menuChildren = <DropdownMenuEntry>[];
|
||||
|
||||
for (final TestMenu value in TestMenu.values) {
|
||||
final DropdownMenuEntry entry = DropdownMenuEntry(label: value.label);
|
||||
menuChildren.add(entry);
|
||||
}
|
||||
|
||||
Widget buildTest(ThemeData themeData, List<DropdownMenuEntry> entries,
|
||||
{double? width, double? menuHeight, Widget? leadingIcon, Widget? label}) {
|
||||
return MaterialApp(
|
||||
theme: themeData,
|
||||
home: Scaffold(
|
||||
body: DropdownMenu(
|
||||
label: label,
|
||||
leadingIcon: leadingIcon,
|
||||
width: width,
|
||||
menuHeight: menuHeight,
|
||||
dropdownMenuEntries: entries,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets('DropdownMenu defaults', (WidgetTester tester) async {
|
||||
final ThemeData themeData = ThemeData();
|
||||
await tester.pumpWidget(buildTest(themeData, menuChildren));
|
||||
|
||||
final EditableText editableText = tester.widget(find.byType(EditableText));
|
||||
expect(editableText.style.color, themeData.textTheme.labelLarge!.color);
|
||||
expect(editableText.style.background, themeData.textTheme.labelLarge!.background);
|
||||
expect(editableText.style.shadows, themeData.textTheme.labelLarge!.shadows);
|
||||
expect(editableText.style.decoration, themeData.textTheme.labelLarge!.decoration);
|
||||
expect(editableText.style.locale, themeData.textTheme.labelLarge!.locale);
|
||||
expect(editableText.style.wordSpacing, themeData.textTheme.labelLarge!.wordSpacing);
|
||||
|
||||
final TextField textField = tester.widget(find.byType(TextField));
|
||||
expect(textField.decoration?.border, const OutlineInputBorder());
|
||||
|
||||
await tester.tap(find.widgetWithIcon(IconButton, Icons.arrow_drop_down).first);
|
||||
await tester.pump();
|
||||
expect(find.byType(MenuAnchor), findsOneWidget);
|
||||
|
||||
final Finder menuMaterial = find.ancestor(
|
||||
of: find.widgetWithText(TextButton, TestMenu.mainMenu0.label),
|
||||
matching: find.byType(Material),
|
||||
).last;
|
||||
Material material = tester.widget<Material>(menuMaterial);
|
||||
expect(material.color, themeData.colorScheme.surface);
|
||||
expect(material.shadowColor, themeData.colorScheme.shadow);
|
||||
expect(material.surfaceTintColor, themeData.colorScheme.surfaceTint);
|
||||
expect(material.elevation, 3.0);
|
||||
expect(material.shape, const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0))));
|
||||
|
||||
final Finder buttonMaterial = find.descendant(
|
||||
of: find.byType(TextButton),
|
||||
matching: find.byType(Material),
|
||||
).last;
|
||||
|
||||
material = tester.widget<Material>(buttonMaterial);
|
||||
expect(material.color, Colors.transparent);
|
||||
expect(material.elevation, 0.0);
|
||||
expect(material.shape, const RoundedRectangleBorder());
|
||||
expect(material.textStyle?.color, themeData.colorScheme.onSurface);
|
||||
});
|
||||
|
||||
testWidgets('DropdownMenu can be disabled', (WidgetTester tester) async {
|
||||
final ThemeData themeData = ThemeData();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: themeData,
|
||||
home: Scaffold(
|
||||
body: SafeArea(
|
||||
child: DropdownMenu(
|
||||
enabled: false,
|
||||
dropdownMenuEntries: menuChildren,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final TextField textField = tester.widget(find.byType(TextField));
|
||||
expect(textField.decoration?.enabled, false);
|
||||
final Finder menuMaterial = find.ancestor(
|
||||
of: find.byType(SingleChildScrollView),
|
||||
matching: find.byType(Material),
|
||||
);
|
||||
expect(menuMaterial, findsNothing);
|
||||
|
||||
await tester.tap(find.byType(TextField));
|
||||
await tester.pump();
|
||||
final Finder updatedMenuMaterial = find.ancestor(
|
||||
of: find.byType(SingleChildScrollView),
|
||||
matching: find.byType(Material),
|
||||
);
|
||||
expect(updatedMenuMaterial, findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('The width of the text field should always be the same as the menu view',
|
||||
(WidgetTester tester) async {
|
||||
|
||||
final ThemeData themeData = ThemeData();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: themeData,
|
||||
home: Scaffold(
|
||||
body: SafeArea(
|
||||
child: DropdownMenu(
|
||||
dropdownMenuEntries: menuChildren,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
final Finder textField = find.byType(TextField);
|
||||
final Size anchorSize = tester.getSize(textField);
|
||||
expect(anchorSize, const Size(180.0, 54.0));
|
||||
|
||||
await tester.tap(find.byType(DropdownMenu));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final Finder menuMaterial = find.ancestor(
|
||||
of: find.byType(SingleChildScrollView),
|
||||
matching: find.byType(Material),
|
||||
);
|
||||
final Size menuSize = tester.getSize(menuMaterial);
|
||||
expect(menuSize, const Size(180.0, 304.0));
|
||||
|
||||
// The text field should have same width as the menu
|
||||
// when the width property is not null.
|
||||
await tester.pumpWidget(buildTest(themeData, menuChildren, width: 200.0));
|
||||
|
||||
final Finder anchor = find.byType(TextField);
|
||||
final Size size = tester.getSize(anchor);
|
||||
expect(size, const Size(200.0, 54.0));
|
||||
|
||||
await tester.tap(find.byType(DropdownMenu));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final Finder updatedMenu = find.ancestor(
|
||||
of: find.byType(SingleChildScrollView),
|
||||
matching: find.byType(Material),
|
||||
);
|
||||
final Size updatedMenuSize = tester.getSize(updatedMenu);
|
||||
expect(updatedMenuSize, const Size(200.0, 304.0));
|
||||
});
|
||||
|
||||
testWidgets('The width property can customize the width of the dropdown menu', (WidgetTester tester) async {
|
||||
final ThemeData themeData = ThemeData();
|
||||
final List<DropdownMenuEntry> shortMenuItems = <DropdownMenuEntry>[];
|
||||
|
||||
for (final ShortMenu value in ShortMenu.values) {
|
||||
final DropdownMenuEntry entry = DropdownMenuEntry(label: value.label);
|
||||
shortMenuItems.add(entry);
|
||||
}
|
||||
|
||||
const double customBigWidth = 250.0;
|
||||
await tester.pumpWidget(buildTest(themeData, shortMenuItems, width: customBigWidth));
|
||||
RenderBox box = tester.firstRenderObject(find.byType(DropdownMenu));
|
||||
expect(box.size.width, customBigWidth);
|
||||
|
||||
await tester.tap(find.byType(DropdownMenu));
|
||||
await tester.pump();
|
||||
expect(find.byType(MenuItemButton), findsNWidgets(6));
|
||||
Size buttonSize = tester.getSize(find.widgetWithText(MenuItemButton, 'I0').last);
|
||||
expect(buttonSize.width, customBigWidth);
|
||||
|
||||
// reset test
|
||||
await tester.pumpWidget(Container());
|
||||
const double customSmallWidth = 100.0;
|
||||
await tester.pumpWidget(buildTest(themeData, shortMenuItems, width: customSmallWidth));
|
||||
box = tester.firstRenderObject(find.byType(DropdownMenu));
|
||||
expect(box.size.width, customSmallWidth);
|
||||
|
||||
await tester.tap(find.byType(DropdownMenu));
|
||||
await tester.pump();
|
||||
expect(find.byType(MenuItemButton), findsNWidgets(6));
|
||||
buttonSize = tester.getSize(find.widgetWithText(MenuItemButton, 'I0').last);
|
||||
expect(buttonSize.width, customSmallWidth);
|
||||
});
|
||||
|
||||
testWidgets('The menuHeight property can be used to show a shorter scrollable menu list instead of the complete list',
|
||||
(WidgetTester tester) async {
|
||||
final ThemeData themeData = ThemeData();
|
||||
await tester.pumpWidget(buildTest(themeData, menuChildren));
|
||||
|
||||
await tester.tap(find.byType(DropdownMenu));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final Element firstItem = tester.element(find.widgetWithText(MenuItemButton, 'Item 0').last);
|
||||
final RenderBox firstBox = firstItem.renderObject! as RenderBox;
|
||||
final Offset topLeft = firstBox.localToGlobal(firstBox.size.topLeft(Offset.zero));
|
||||
final Element lastItem = tester.element(find.widgetWithText(MenuItemButton, 'Item 5').last);
|
||||
final RenderBox lastBox = lastItem.renderObject! as RenderBox;
|
||||
final Offset bottomRight = lastBox.localToGlobal(lastBox.size.bottomRight(Offset.zero));
|
||||
// height = height of MenuItemButton * 6 = 48 * 6
|
||||
expect(bottomRight.dy - topLeft.dy, 288.0);
|
||||
|
||||
final Finder menuView = find.ancestor(
|
||||
of: find.byType(SingleChildScrollView),
|
||||
matching: find.byType(Padding),
|
||||
).first;
|
||||
final Size menuViewSize = tester.getSize(menuView);
|
||||
expect(menuViewSize, const Size(180.0, 304.0)); // 304 = 288 + vertical padding(2 * 8)
|
||||
|
||||
// Constrains the menu height.
|
||||
await tester.pumpWidget(Container());
|
||||
await tester.pumpWidget(buildTest(themeData, menuChildren, menuHeight: 100));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byType(DropdownMenu));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final Finder updatedMenu = find.ancestor(
|
||||
of: find.byType(SingleChildScrollView),
|
||||
matching: find.byType(Padding),
|
||||
).first;
|
||||
|
||||
final Size updatedMenuSize = tester.getSize(updatedMenu);
|
||||
expect(updatedMenuSize, const Size(180.0, 100.0));
|
||||
});
|
||||
|
||||
testWidgets('The text in the menu button should be aligned with the text of '
|
||||
'the text field - LTR', (WidgetTester tester) async {
|
||||
final ThemeData themeData = ThemeData();
|
||||
// Default text field (without leading icon).
|
||||
await tester.pumpWidget(buildTest(themeData, menuChildren, label: const Text('label')));
|
||||
|
||||
final Finder label = find.text('label');
|
||||
final Offset labelTopLeft = tester.getTopLeft(label);
|
||||
|
||||
await tester.tap(find.byType(DropdownMenu));
|
||||
await tester.pumpAndSettle();
|
||||
final Finder itemText = find.text('Item 0').last;
|
||||
final Offset itemTextTopLeft = tester.getTopLeft(itemText);
|
||||
|
||||
expect(labelTopLeft.dx, equals(itemTextTopLeft.dx));
|
||||
|
||||
// Test when the text field has a leading icon.
|
||||
await tester.pumpWidget(Container());
|
||||
await tester.pumpWidget(buildTest(themeData, menuChildren,
|
||||
leadingIcon: const Icon(Icons.search),
|
||||
label: const Text('label'),
|
||||
));
|
||||
|
||||
final Finder leadingIcon = find.widgetWithIcon(Container, Icons.search);
|
||||
final double iconWidth = tester.getSize(leadingIcon).width;
|
||||
final Finder updatedLabel = find.text('label');
|
||||
final Offset updatedLabelTopLeft = tester.getTopLeft(updatedLabel);
|
||||
|
||||
await tester.tap(find.byType(DropdownMenu));
|
||||
await tester.pumpAndSettle();
|
||||
final Finder updatedItemText = find.text('Item 0').last;
|
||||
final Offset updatedItemTextTopLeft = tester.getTopLeft(updatedItemText);
|
||||
|
||||
|
||||
expect(updatedLabelTopLeft.dx, equals(updatedItemTextTopLeft.dx));
|
||||
expect(updatedLabelTopLeft.dx, equals(iconWidth));
|
||||
|
||||
// Test when then leading icon is a widget with a bigger size.
|
||||
await tester.pumpWidget(Container());
|
||||
await tester.pumpWidget(buildTest(themeData, menuChildren,
|
||||
leadingIcon: const SizedBox(
|
||||
width: 75.0,
|
||||
child: Icon(Icons.search)),
|
||||
label: const Text('label'),
|
||||
));
|
||||
|
||||
final Finder largeLeadingIcon = find.widgetWithIcon(Container, Icons.search);
|
||||
final double largeIconWidth = tester.getSize(largeLeadingIcon).width;
|
||||
final Finder updatedLabel1 = find.text('label');
|
||||
final Offset updatedLabelTopLeft1 = tester.getTopLeft(updatedLabel1);
|
||||
|
||||
await tester.tap(find.byType(DropdownMenu));
|
||||
await tester.pumpAndSettle();
|
||||
final Finder updatedItemText1 = find.text('Item 0').last;
|
||||
final Offset updatedItemTextTopLeft1 = tester.getTopLeft(updatedItemText1);
|
||||
|
||||
|
||||
expect(updatedLabelTopLeft1.dx, equals(updatedItemTextTopLeft1.dx));
|
||||
expect(updatedLabelTopLeft1.dx, equals(largeIconWidth));
|
||||
});
|
||||
|
||||
testWidgets('The text in the menu button should be aligned with the text of '
|
||||
'the text field - RTL', (WidgetTester tester) async {
|
||||
final ThemeData themeData = ThemeData();
|
||||
// Default text field (without leading icon).
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
theme: themeData,
|
||||
home: Scaffold(
|
||||
body: Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: DropdownMenu(
|
||||
label: const Text('label'),
|
||||
dropdownMenuEntries: menuChildren,
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
final Finder label = find.text('label');
|
||||
final Offset labelTopRight = tester.getTopRight(label);
|
||||
|
||||
await tester.tap(find.byType(DropdownMenu));
|
||||
await tester.pumpAndSettle();
|
||||
final Finder itemText = find.text('Item 0').last;
|
||||
final Offset itemTextTopRight = tester.getTopRight(itemText);
|
||||
|
||||
expect(labelTopRight.dx, equals(itemTextTopRight.dx));
|
||||
|
||||
// Test when the text field has a leading icon.
|
||||
await tester.pumpWidget(Container());
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
theme: themeData,
|
||||
home: Scaffold(
|
||||
body: Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: DropdownMenu(
|
||||
leadingIcon: const Icon(Icons.search),
|
||||
label: const Text('label'),
|
||||
dropdownMenuEntries: menuChildren,
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
await tester.pump();
|
||||
|
||||
final Finder leadingIcon = find.widgetWithIcon(Container, Icons.search);
|
||||
final double iconWidth = tester.getSize(leadingIcon).width;
|
||||
final Offset dropdownMenuTopRight = tester.getTopRight(find.byType(DropdownMenu));
|
||||
final Finder updatedLabel = find.text('label');
|
||||
final Offset updatedLabelTopRight = tester.getTopRight(updatedLabel);
|
||||
|
||||
await tester.tap(find.byType(DropdownMenu));
|
||||
await tester.pumpAndSettle();
|
||||
final Finder updatedItemText = find.text('Item 0').last;
|
||||
final Offset updatedItemTextTopRight = tester.getTopRight(updatedItemText);
|
||||
|
||||
|
||||
expect(updatedLabelTopRight.dx, equals(updatedItemTextTopRight.dx));
|
||||
expect(updatedLabelTopRight.dx, equals(dropdownMenuTopRight.dx - iconWidth));
|
||||
|
||||
// Test when then leading icon is a widget with a bigger size.
|
||||
await tester.pumpWidget(Container());
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
theme: themeData,
|
||||
home: Scaffold(
|
||||
body: Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: DropdownMenu(
|
||||
leadingIcon: const SizedBox(width: 75.0, child: Icon(Icons.search)),
|
||||
label: const Text('label'),
|
||||
dropdownMenuEntries: menuChildren,
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
await tester.pump();
|
||||
|
||||
final Finder largeLeadingIcon = find.widgetWithIcon(Container, Icons.search);
|
||||
final double largeIconWidth = tester.getSize(largeLeadingIcon).width;
|
||||
final Offset updatedDropdownMenuTopRight = tester.getTopRight(find.byType(DropdownMenu));
|
||||
final Finder updatedLabel1 = find.text('label');
|
||||
final Offset updatedLabelTopRight1 = tester.getTopRight(updatedLabel1);
|
||||
|
||||
await tester.tap(find.byType(DropdownMenu));
|
||||
await tester.pumpAndSettle();
|
||||
final Finder updatedItemText1 = find.text('Item 0').last;
|
||||
final Offset updatedItemTextTopRight1 = tester.getTopRight(updatedItemText1);
|
||||
|
||||
|
||||
expect(updatedLabelTopRight1.dx, equals(updatedItemTextTopRight1.dx));
|
||||
expect(updatedLabelTopRight1.dx, equals(updatedDropdownMenuTopRight.dx - largeIconWidth));
|
||||
});
|
||||
|
||||
testWidgets('DropdownMenu has default trailing icon button', (WidgetTester tester) async {
|
||||
final ThemeData themeData = ThemeData();
|
||||
await tester.pumpWidget(buildTest(themeData, menuChildren));
|
||||
await tester.pump();
|
||||
|
||||
final Finder iconButton = find.widgetWithIcon(IconButton, Icons.arrow_drop_down).first;
|
||||
expect(iconButton, findsOneWidget);
|
||||
|
||||
await tester.tap(iconButton);
|
||||
await tester.pump();
|
||||
|
||||
final Finder menuMaterial = find.ancestor(
|
||||
of: find.widgetWithText(MenuItemButton, TestMenu.mainMenu0.label),
|
||||
matching: find.byType(Material),
|
||||
).last;
|
||||
expect(menuMaterial, findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('DropdownMenu can customize trailing icon button', (WidgetTester tester) async {
|
||||
final ThemeData themeData = ThemeData();
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
theme: themeData,
|
||||
home: Scaffold(
|
||||
body: DropdownMenu(
|
||||
trailingIcon: const Icon(Icons.ac_unit),
|
||||
dropdownMenuEntries: menuChildren,
|
||||
),
|
||||
),
|
||||
));
|
||||
await tester.pump();
|
||||
|
||||
final Finder iconButton = find.widgetWithIcon(IconButton, Icons.ac_unit).first;
|
||||
expect(iconButton, findsOneWidget);
|
||||
|
||||
await tester.tap(iconButton);
|
||||
await tester.pump();
|
||||
|
||||
final Finder menuMaterial = find.ancestor(
|
||||
of: find.widgetWithText(MenuItemButton, TestMenu.mainMenu0.label),
|
||||
matching: find.byType(Material),
|
||||
).last;
|
||||
expect(menuMaterial, findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Down key can highlight the menu item', (WidgetTester tester) async {
|
||||
final ThemeData themeData = ThemeData();
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
theme: themeData,
|
||||
home: Scaffold(
|
||||
body: DropdownMenu(
|
||||
trailingIcon: const Icon(Icons.ac_unit),
|
||||
dropdownMenuEntries: menuChildren,
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
await tester.tap(find.byType(DropdownMenu));
|
||||
await tester.pump();
|
||||
|
||||
await simulateKeyDownEvent(LogicalKeyboardKey.arrowDown);
|
||||
await tester.pumpAndSettle();
|
||||
Finder button0Material = find.descendant(
|
||||
of: find.widgetWithText(MenuItemButton, 'Item 0').last,
|
||||
matching: find.byType(Material),
|
||||
);
|
||||
|
||||
Material item0material = tester.widget<Material>(button0Material);
|
||||
expect(item0material.color, themeData.colorScheme.onSurface.withOpacity(0.12));
|
||||
|
||||
// Press down key one more time, the highlight should move to the next item.
|
||||
await simulateKeyDownEvent(LogicalKeyboardKey.arrowDown);
|
||||
await tester.pumpAndSettle();
|
||||
final Finder button1Material = find.descendant(
|
||||
of: find.widgetWithText(MenuItemButton, 'Menu 1').last,
|
||||
matching: find.byType(Material),
|
||||
);
|
||||
final Material item1material = tester.widget<Material>(button1Material);
|
||||
expect(item1material.color, themeData.colorScheme.onSurface.withOpacity(0.12));
|
||||
button0Material = find.descendant(
|
||||
of: find.widgetWithText(MenuItemButton, 'Item 0').last,
|
||||
matching: find.byType(Material),
|
||||
);
|
||||
item0material = tester.widget<Material>(button0Material);
|
||||
expect(item0material.color, Colors.transparent); // the previous item should not be highlighted.
|
||||
});
|
||||
|
||||
testWidgets('Up key can highlight the menu item', (WidgetTester tester) async {
|
||||
final ThemeData themeData = ThemeData();
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
theme: themeData,
|
||||
home: Scaffold(
|
||||
body: DropdownMenu(
|
||||
dropdownMenuEntries: menuChildren,
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
await tester.tap(find.byType(DropdownMenu));
|
||||
await tester.pump();
|
||||
|
||||
await simulateKeyDownEvent(LogicalKeyboardKey.arrowUp);
|
||||
await tester.pumpAndSettle();
|
||||
Finder button5Material = find.descendant(
|
||||
of: find.widgetWithText(MenuItemButton, 'Item 5').last,
|
||||
matching: find.byType(Material),
|
||||
);
|
||||
|
||||
Material item5material = tester.widget<Material>(button5Material);
|
||||
expect(item5material.color, themeData.colorScheme.onSurface.withOpacity(0.12));
|
||||
|
||||
// Press up key one more time, the highlight should move up to the item 4.
|
||||
await simulateKeyDownEvent(LogicalKeyboardKey.arrowUp);
|
||||
await tester.pumpAndSettle();
|
||||
final Finder button4Material = find.descendant(
|
||||
of: find.widgetWithText(MenuItemButton, 'Item 4').last,
|
||||
matching: find.byType(Material),
|
||||
);
|
||||
final Material item4material = tester.widget<Material>(button4Material);
|
||||
expect(item4material.color, themeData.colorScheme.onSurface.withOpacity(0.12));
|
||||
button5Material = find.descendant(
|
||||
of: find.widgetWithText(MenuItemButton, 'Item 5').last,
|
||||
matching: find.byType(Material),
|
||||
);
|
||||
|
||||
item5material = tester.widget<Material>(button5Material);
|
||||
expect(item5material.color, Colors.transparent); // the previous item should not be highlighted.
|
||||
});
|
||||
|
||||
testWidgets('The text input should match the label of the menu item while pressing down key', (WidgetTester tester) async {
|
||||
final ThemeData themeData = ThemeData();
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
theme: themeData,
|
||||
home: Scaffold(
|
||||
body: DropdownMenu(
|
||||
dropdownMenuEntries: menuChildren,
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
// Open the menu
|
||||
await tester.tap(find.byType(DropdownMenu));
|
||||
await tester.pump();
|
||||
|
||||
await simulateKeyDownEvent(LogicalKeyboardKey.arrowDown);
|
||||
await tester.pump();
|
||||
expect(find.widgetWithText(TextField, 'Item 0'), findsOneWidget);
|
||||
|
||||
// Press down key one more time to the next item.
|
||||
await simulateKeyDownEvent(LogicalKeyboardKey.arrowDown);
|
||||
await tester.pump();
|
||||
expect(find.widgetWithText(TextField, 'Menu 1'), findsOneWidget);
|
||||
|
||||
// Press down to the next item.
|
||||
await simulateKeyDownEvent(LogicalKeyboardKey.arrowDown);
|
||||
await tester.pump();
|
||||
expect(find.widgetWithText(TextField, 'Item 2'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('The text input should match the label of the menu item while pressing up key', (WidgetTester tester) async {
|
||||
final ThemeData themeData = ThemeData();
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
theme: themeData,
|
||||
home: Scaffold(
|
||||
body: DropdownMenu(
|
||||
dropdownMenuEntries: menuChildren,
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
// Open the menu
|
||||
await tester.tap(find.byType(DropdownMenu));
|
||||
await tester.pump();
|
||||
|
||||
await simulateKeyDownEvent(LogicalKeyboardKey.arrowUp);
|
||||
await tester.pump();
|
||||
expect(find.widgetWithText(TextField, 'Item 5'), findsOneWidget);
|
||||
|
||||
// Press up key one more time to the upper item.
|
||||
await simulateKeyDownEvent(LogicalKeyboardKey.arrowUp);
|
||||
await tester.pump();
|
||||
expect(find.widgetWithText(TextField, 'Item 4'), findsOneWidget);
|
||||
|
||||
// Press up to the upper item.
|
||||
await simulateKeyDownEvent(LogicalKeyboardKey.arrowUp);
|
||||
await tester.pump();
|
||||
expect(find.widgetWithText(TextField, 'Item 3'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Disabled button will be skipped while pressing up/down key', (WidgetTester tester) async {
|
||||
final ThemeData themeData = ThemeData();
|
||||
final List<DropdownMenuEntry> menuWithDisabledItems = <DropdownMenuEntry>[
|
||||
const DropdownMenuEntry(label: 'Item 0'),
|
||||
const DropdownMenuEntry(label: 'Item 1', enabled: false),
|
||||
const DropdownMenuEntry(label: 'Item 2', enabled: false),
|
||||
const DropdownMenuEntry(label: 'Item 3'),
|
||||
const DropdownMenuEntry(label: 'Item 4'),
|
||||
const DropdownMenuEntry(label: 'Item 5', enabled: false),
|
||||
];
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
theme: themeData,
|
||||
home: Scaffold(
|
||||
body: DropdownMenu(
|
||||
dropdownMenuEntries: menuWithDisabledItems,
|
||||
),
|
||||
),
|
||||
));
|
||||
await tester.pump();
|
||||
|
||||
// Open the menu
|
||||
await tester.tap(find.byType(DropdownMenu));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await simulateKeyDownEvent(LogicalKeyboardKey.arrowDown);
|
||||
await tester.pumpAndSettle();
|
||||
final Finder button0Material = find.descendant(
|
||||
of: find.widgetWithText(MenuItemButton, 'Item 0').last,
|
||||
matching: find.byType(Material),
|
||||
);
|
||||
final Material item0Material = tester.widget<Material>(button0Material);
|
||||
expect(item0Material.color, themeData.colorScheme.onSurface.withOpacity(0.12)); // first item can be highlighted as it's enabled.
|
||||
|
||||
// Continue to press down key. Item 3 should be highlighted as Menu 1 and Item 2 are both disabled.
|
||||
await simulateKeyDownEvent(LogicalKeyboardKey.arrowDown);
|
||||
await tester.pumpAndSettle();
|
||||
final Finder button3Material = find.descendant(
|
||||
of: find.widgetWithText(MenuItemButton, 'Item 3').last,
|
||||
matching: find.byType(Material),
|
||||
);
|
||||
final Material item3Material = tester.widget<Material>(button3Material);
|
||||
expect(item3Material.color, themeData.colorScheme.onSurface.withOpacity(0.12));
|
||||
});
|
||||
|
||||
testWidgets('Searching is enabled by default', (WidgetTester tester) async {
|
||||
final ThemeData themeData = ThemeData();
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
theme: themeData,
|
||||
home: Scaffold(
|
||||
body: DropdownMenu(
|
||||
dropdownMenuEntries: menuChildren,
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
// Open the menu
|
||||
await tester.tap(find.byType(DropdownMenu));
|
||||
await tester.pump();
|
||||
await tester.enterText(find.byType(TextField).first, 'Menu 1');
|
||||
await tester.pumpAndSettle();
|
||||
final Finder buttonMaterial = find.descendant(
|
||||
of: find.widgetWithText(MenuItemButton, 'Menu 1').last,
|
||||
matching: find.byType(Material),
|
||||
);
|
||||
final Material itemMaterial = tester.widget<Material>(buttonMaterial);
|
||||
expect(itemMaterial.color, themeData.colorScheme.onSurface.withOpacity(0.12)); // Menu 1 button is highlighted.
|
||||
});
|
||||
|
||||
testWidgets('Highlight can move up/down from the searching result', (WidgetTester tester) async {
|
||||
final ThemeData themeData = ThemeData();
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
theme: themeData,
|
||||
home: Scaffold(
|
||||
body: DropdownMenu(
|
||||
dropdownMenuEntries: menuChildren,
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
// Open the menu
|
||||
await tester.tap(find.byType(DropdownMenu));
|
||||
await tester.pump();
|
||||
await tester.enterText(find.byType(TextField).first, 'Menu 1');
|
||||
await tester.pumpAndSettle();
|
||||
final Finder buttonMaterial = find.descendant(
|
||||
of: find.widgetWithText(MenuItemButton, 'Menu 1').last,
|
||||
matching: find.byType(Material),
|
||||
);
|
||||
final Material itemMaterial = tester.widget<Material>(buttonMaterial);
|
||||
expect(itemMaterial.color, themeData.colorScheme.onSurface.withOpacity(0.12));
|
||||
|
||||
// Press up to the upper item (Item 0).
|
||||
await simulateKeyDownEvent(LogicalKeyboardKey.arrowUp);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.widgetWithText(TextField, 'Item 0'), findsOneWidget);
|
||||
final Finder button0Material = find.descendant(
|
||||
of: find.widgetWithText(MenuItemButton, 'Item 0').last,
|
||||
matching: find.byType(Material),
|
||||
);
|
||||
final Material item0Material = tester.widget<Material>(button0Material);
|
||||
expect(item0Material.color, themeData.colorScheme.onSurface.withOpacity(0.12)); // Move up, the 'Item 0' is highlighted.
|
||||
|
||||
// Continue to move up to the last item (Item 5).
|
||||
await simulateKeyDownEvent(LogicalKeyboardKey.arrowUp);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.widgetWithText(TextField, 'Item 5'), findsOneWidget);
|
||||
final Finder button5Material = find.descendant(
|
||||
of: find.widgetWithText(MenuItemButton, 'Item 5').last,
|
||||
matching: find.byType(Material),
|
||||
);
|
||||
final Material item5Material = tester.widget<Material>(button5Material);
|
||||
expect(item5Material.color, themeData.colorScheme.onSurface.withOpacity(0.12));
|
||||
});
|
||||
|
||||
testWidgets('Filtering is disabled by default', (WidgetTester tester) async {
|
||||
final ThemeData themeData = ThemeData();
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
theme: themeData,
|
||||
home: Scaffold(
|
||||
body: DropdownMenu(
|
||||
dropdownMenuEntries: menuChildren,
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
// Open the menu
|
||||
await tester.tap(find.byType(DropdownMenu));
|
||||
await tester.pump();
|
||||
|
||||
await tester.enterText(find.byType(TextField).first, 'Menu 1');
|
||||
await tester.pumpAndSettle();
|
||||
for (final TestMenu menu in TestMenu.values) {
|
||||
// One is layout for the _DropdownMenuBody, the other one is the real button item in the menu.
|
||||
expect(find.widgetWithText(MenuItemButton, menu.label), findsNWidgets(2));
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('Enable filtering', (WidgetTester tester) async {
|
||||
final ThemeData themeData = ThemeData();
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
theme: themeData,
|
||||
home: Scaffold(
|
||||
body: DropdownMenu(
|
||||
enableFilter: true,
|
||||
dropdownMenuEntries: menuChildren,
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
// Open the menu
|
||||
await tester.tap(find.byType(DropdownMenu));
|
||||
await tester.pump();
|
||||
|
||||
await tester.enterText(find
|
||||
.byType(TextField)
|
||||
.first, 'Menu 1');
|
||||
await tester.pumpAndSettle();
|
||||
for (final TestMenu menu in TestMenu.values) {
|
||||
// 'Menu 1' should be 2, other items should only find one.
|
||||
if (menu.label == TestMenu.mainMenu1.label) {
|
||||
expect(find.widgetWithText(MenuItemButton, menu.label), findsNWidgets(2));
|
||||
} else {
|
||||
expect(find.widgetWithText(MenuItemButton, menu.label), findsOneWidget);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
enum TestMenu {
|
||||
mainMenu0('Item 0'),
|
||||
mainMenu1('Menu 1'),
|
||||
mainMenu2('Item 2'),
|
||||
mainMenu3('Item 3'),
|
||||
mainMenu4('Item 4'),
|
||||
mainMenu5('Item 5');
|
||||
|
||||
const TestMenu(this.label);
|
||||
final String label;
|
||||
}
|
||||
|
||||
enum ShortMenu {
|
||||
item0('I0'),
|
||||
item1('I1'),
|
||||
item2('I2');
|
||||
|
||||
const ShortMenu(this.label);
|
||||
final String label;
|
||||
}
|
398
packages/flutter/test/material/dropdown_menu_theme_test.dart
Normal file
398
packages/flutter/test/material/dropdown_menu_theme_test.dart
Normal file
@ -0,0 +1,398 @@
|
||||
// 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/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
test('DropdownMenuThemeData copyWith, ==, hashCode basics', () {
|
||||
expect(const DropdownMenuThemeData(), const DropdownMenuThemeData().copyWith());
|
||||
expect(const DropdownMenuThemeData().hashCode, const DropdownMenuThemeData().copyWith().hashCode);
|
||||
|
||||
const DropdownMenuThemeData custom = DropdownMenuThemeData(
|
||||
menuStyle: MenuStyle(backgroundColor: MaterialStatePropertyAll<Color>(Colors.green)),
|
||||
inputDecorationTheme: InputDecorationTheme(filled: true),
|
||||
textStyle: TextStyle(fontSize: 25.0),
|
||||
);
|
||||
final DropdownMenuThemeData copy = const DropdownMenuThemeData().copyWith(
|
||||
menuStyle: custom.menuStyle,
|
||||
inputDecorationTheme: custom.inputDecorationTheme,
|
||||
textStyle: custom.textStyle,
|
||||
);
|
||||
expect(copy, custom);
|
||||
});
|
||||
|
||||
testWidgets('Default DropdownMenuThemeData debugFillProperties', (WidgetTester tester) async {
|
||||
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
|
||||
const DropdownMenuThemeData().debugFillProperties(builder);
|
||||
|
||||
final List<String> description = builder.properties
|
||||
.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
|
||||
.map((DiagnosticsNode node) => node.toString())
|
||||
.toList();
|
||||
|
||||
expect(description, <String>[]);
|
||||
});
|
||||
|
||||
testWidgets('With no other configuration, defaults are used', (WidgetTester tester) async {
|
||||
final ThemeData themeData = ThemeData();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: themeData,
|
||||
home: const Scaffold(
|
||||
body: Center(
|
||||
child: DropdownMenu(
|
||||
dropdownMenuEntries: <DropdownMenuEntry>[
|
||||
DropdownMenuEntry(label: 'Item 0'),
|
||||
DropdownMenuEntry(label: 'Item 1'),
|
||||
DropdownMenuEntry(label: 'Item 2'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
final EditableText editableText = tester.widget(find.byType(EditableText));
|
||||
expect(editableText.style.color, themeData.textTheme.labelLarge!.color);
|
||||
expect(editableText.style.background, themeData.textTheme.labelLarge!.background);
|
||||
expect(editableText.style.shadows, themeData.textTheme.labelLarge!.shadows);
|
||||
expect(editableText.style.decoration, themeData.textTheme.labelLarge!.decoration);
|
||||
expect(editableText.style.locale, themeData.textTheme.labelLarge!.locale);
|
||||
expect(editableText.style.wordSpacing, themeData.textTheme.labelLarge!.wordSpacing);
|
||||
|
||||
final TextField textField = tester.widget(find.byType(TextField));
|
||||
expect(textField.decoration?.border, const OutlineInputBorder());
|
||||
|
||||
await tester.tap(find.widgetWithIcon(IconButton, Icons.arrow_drop_down).first);
|
||||
await tester.pump();
|
||||
expect(find.byType(MenuAnchor), findsOneWidget);
|
||||
|
||||
final Finder menuMaterial = find.ancestor(
|
||||
of: find.widgetWithText(TextButton, 'Item 0'),
|
||||
matching: find.byType(Material),
|
||||
).last;
|
||||
Material material = tester.widget<Material>(menuMaterial);
|
||||
expect(material.color, themeData.colorScheme.surface);
|
||||
expect(material.shadowColor, themeData.colorScheme.shadow);
|
||||
expect(material.surfaceTintColor, themeData.colorScheme.surfaceTint);
|
||||
expect(material.elevation, 3.0);
|
||||
expect(material.shape, const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0))));
|
||||
|
||||
final Finder buttonMaterial = find.descendant(
|
||||
of: find.widgetWithText(TextButton, 'Item 0'),
|
||||
matching: find.byType(Material),
|
||||
).last;
|
||||
|
||||
material = tester.widget<Material>(buttonMaterial);
|
||||
expect(material.color, Colors.transparent);
|
||||
expect(material.elevation, 0.0);
|
||||
expect(material.shape, const RoundedRectangleBorder());
|
||||
expect(material.textStyle?.color, themeData.colorScheme.onSurface);
|
||||
});
|
||||
|
||||
testWidgets('ThemeData.dropdownMenuTheme overrides defaults', (WidgetTester tester) async {
|
||||
final ThemeData theme = ThemeData(
|
||||
dropdownMenuTheme: DropdownMenuThemeData(
|
||||
textStyle: TextStyle(
|
||||
color: Colors.orange,
|
||||
backgroundColor: Colors.indigo,
|
||||
fontSize: 30.0,
|
||||
shadows: kElevationToShadow[1],
|
||||
decoration: TextDecoration.underline,
|
||||
wordSpacing: 2.0,
|
||||
),
|
||||
menuStyle: const MenuStyle(
|
||||
backgroundColor: MaterialStatePropertyAll<Color>(Colors.grey),
|
||||
shadowColor: MaterialStatePropertyAll<Color>(Colors.brown),
|
||||
surfaceTintColor: MaterialStatePropertyAll<Color>(Colors.amberAccent),
|
||||
elevation: MaterialStatePropertyAll<double>(10.0),
|
||||
shape: MaterialStatePropertyAll<OutlinedBorder>(
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10.0))),
|
||||
),
|
||||
),
|
||||
inputDecorationTheme: const InputDecorationTheme(filled: true, fillColor: Colors.lightGreen),
|
||||
)
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: theme,
|
||||
home: const Scaffold(
|
||||
body: Center(
|
||||
child: DropdownMenu(
|
||||
dropdownMenuEntries: <DropdownMenuEntry>[
|
||||
DropdownMenuEntry(label: 'Item 0'),
|
||||
DropdownMenuEntry(label: 'Item 1'),
|
||||
DropdownMenuEntry(label: 'Item 2'),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
final EditableText editableText = tester.widget(find.byType(EditableText));
|
||||
expect(editableText.style.color, Colors.orange);
|
||||
expect(editableText.style.backgroundColor, Colors.indigo);
|
||||
expect(editableText.style.shadows, kElevationToShadow[1]);
|
||||
expect(editableText.style.decoration, TextDecoration.underline);
|
||||
expect(editableText.style.wordSpacing, 2.0);
|
||||
|
||||
final TextField textField = tester.widget(find.byType(TextField));
|
||||
expect(textField.decoration?.filled, isTrue);
|
||||
expect(textField.decoration?.fillColor, Colors.lightGreen);
|
||||
|
||||
await tester.tap(find.widgetWithIcon(IconButton, Icons.arrow_drop_down).first);
|
||||
await tester.pump();
|
||||
expect(find.byType(MenuAnchor), findsOneWidget);
|
||||
|
||||
final Finder menuMaterial = find.ancestor(
|
||||
of: find.widgetWithText(TextButton, 'Item 0'),
|
||||
matching: find.byType(Material),
|
||||
).last;
|
||||
Material material = tester.widget<Material>(menuMaterial);
|
||||
expect(material.color, Colors.grey);
|
||||
expect(material.shadowColor, Colors.brown);
|
||||
expect(material.surfaceTintColor, Colors.amberAccent);
|
||||
expect(material.elevation, 10.0);
|
||||
expect(material.shape, const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10.0))));
|
||||
|
||||
final Finder buttonMaterial = find.descendant(
|
||||
of: find.widgetWithText(TextButton, 'Item 0'),
|
||||
matching: find.byType(Material),
|
||||
).last;
|
||||
|
||||
material = tester.widget<Material>(buttonMaterial);
|
||||
expect(material.color, Colors.transparent);
|
||||
expect(material.elevation, 0.0);
|
||||
expect(material.shape, const RoundedRectangleBorder());
|
||||
expect(material.textStyle?.color, theme.colorScheme.onSurface);
|
||||
});
|
||||
|
||||
testWidgets('DropdownMenuTheme overrides ThemeData and defaults', (WidgetTester tester) async {
|
||||
final DropdownMenuThemeData global = DropdownMenuThemeData(
|
||||
textStyle: TextStyle(
|
||||
color: Colors.orange,
|
||||
backgroundColor: Colors.indigo,
|
||||
fontSize: 30.0,
|
||||
shadows: kElevationToShadow[1],
|
||||
decoration: TextDecoration.underline,
|
||||
wordSpacing: 2.0,
|
||||
),
|
||||
menuStyle: const MenuStyle(
|
||||
backgroundColor: MaterialStatePropertyAll<Color>(Colors.grey),
|
||||
shadowColor: MaterialStatePropertyAll<Color>(Colors.brown),
|
||||
surfaceTintColor: MaterialStatePropertyAll<Color>(Colors.amberAccent),
|
||||
elevation: MaterialStatePropertyAll<double>(10.0),
|
||||
shape: MaterialStatePropertyAll<OutlinedBorder>(
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10.0))),
|
||||
),
|
||||
),
|
||||
inputDecorationTheme: const InputDecorationTheme(filled: true, fillColor: Colors.lightGreen),
|
||||
);
|
||||
|
||||
final DropdownMenuThemeData dropdownMenuTheme = DropdownMenuThemeData(
|
||||
textStyle: TextStyle(
|
||||
color: Colors.red,
|
||||
backgroundColor: Colors.orange,
|
||||
fontSize: 27.0,
|
||||
shadows: kElevationToShadow[2],
|
||||
decoration: TextDecoration.lineThrough,
|
||||
wordSpacing: 5.0,
|
||||
),
|
||||
menuStyle: const MenuStyle(
|
||||
backgroundColor: MaterialStatePropertyAll<Color>(Colors.yellow),
|
||||
shadowColor: MaterialStatePropertyAll<Color>(Colors.green),
|
||||
surfaceTintColor: MaterialStatePropertyAll<Color>(Colors.teal),
|
||||
elevation: MaterialStatePropertyAll<double>(15.0),
|
||||
shape: MaterialStatePropertyAll<OutlinedBorder>(
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(8.0))),
|
||||
),
|
||||
),
|
||||
inputDecorationTheme: const InputDecorationTheme(filled: true, fillColor: Colors.blue),
|
||||
);
|
||||
|
||||
final ThemeData theme = ThemeData(dropdownMenuTheme: global);
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: theme,
|
||||
home: DropdownMenuTheme(
|
||||
data: dropdownMenuTheme,
|
||||
child: const Scaffold(
|
||||
body: Center(
|
||||
child: DropdownMenu(
|
||||
dropdownMenuEntries: <DropdownMenuEntry>[
|
||||
DropdownMenuEntry(label: 'Item 0'),
|
||||
DropdownMenuEntry(label: 'Item 1'),
|
||||
DropdownMenuEntry(label: 'Item 2'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
final EditableText editableText = tester.widget(find.byType(EditableText));
|
||||
expect(editableText.style.color, Colors.red);
|
||||
expect(editableText.style.backgroundColor, Colors.orange);
|
||||
expect(editableText.style.fontSize, 27.0);
|
||||
expect(editableText.style.shadows, kElevationToShadow[2]);
|
||||
expect(editableText.style.decoration, TextDecoration.lineThrough);
|
||||
expect(editableText.style.wordSpacing, 5.0);
|
||||
|
||||
final TextField textField = tester.widget(find.byType(TextField));
|
||||
expect(textField.decoration?.filled, isTrue);
|
||||
expect(textField.decoration?.fillColor, Colors.blue);
|
||||
|
||||
await tester.tap(find.widgetWithIcon(IconButton, Icons.arrow_drop_down).first);
|
||||
await tester.pump();
|
||||
expect(find.byType(MenuAnchor), findsOneWidget);
|
||||
|
||||
final Finder menuMaterial = find.ancestor(
|
||||
of: find.widgetWithText(TextButton, 'Item 0'),
|
||||
matching: find.byType(Material),
|
||||
).last;
|
||||
Material material = tester.widget<Material>(menuMaterial);
|
||||
expect(material.color, Colors.yellow);
|
||||
expect(material.shadowColor, Colors.green);
|
||||
expect(material.surfaceTintColor, Colors.teal);
|
||||
expect(material.elevation, 15.0);
|
||||
expect(material.shape, const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(8.0))));
|
||||
|
||||
final Finder buttonMaterial = find.descendant(
|
||||
of: find.widgetWithText(TextButton, 'Item 0'),
|
||||
matching: find.byType(Material),
|
||||
).last;
|
||||
|
||||
material = tester.widget<Material>(buttonMaterial);
|
||||
expect(material.color, Colors.transparent);
|
||||
expect(material.elevation, 0.0);
|
||||
expect(material.shape, const RoundedRectangleBorder());
|
||||
expect(material.textStyle?.color, theme.colorScheme.onSurface);
|
||||
});
|
||||
|
||||
testWidgets('Widget parameters overrides DropdownMenuTheme, ThemeData and defaults', (WidgetTester tester) async {
|
||||
final DropdownMenuThemeData global = DropdownMenuThemeData(
|
||||
textStyle: TextStyle(
|
||||
color: Colors.orange,
|
||||
backgroundColor: Colors.indigo,
|
||||
fontSize: 30.0,
|
||||
shadows: kElevationToShadow[1],
|
||||
decoration: TextDecoration.underline,
|
||||
wordSpacing: 2.0,
|
||||
),
|
||||
menuStyle: const MenuStyle(
|
||||
backgroundColor: MaterialStatePropertyAll<Color>(Colors.grey),
|
||||
shadowColor: MaterialStatePropertyAll<Color>(Colors.brown),
|
||||
surfaceTintColor: MaterialStatePropertyAll<Color>(Colors.amberAccent),
|
||||
elevation: MaterialStatePropertyAll<double>(10.0),
|
||||
shape: MaterialStatePropertyAll<OutlinedBorder>(
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10.0))),
|
||||
),
|
||||
),
|
||||
inputDecorationTheme: const InputDecorationTheme(filled: true, fillColor: Colors.lightGreen),
|
||||
);
|
||||
|
||||
final DropdownMenuThemeData dropdownMenuTheme = DropdownMenuThemeData(
|
||||
textStyle: TextStyle(
|
||||
color: Colors.red,
|
||||
backgroundColor: Colors.orange,
|
||||
fontSize: 27.0,
|
||||
shadows: kElevationToShadow[2],
|
||||
decoration: TextDecoration.lineThrough,
|
||||
wordSpacing: 5.0,
|
||||
),
|
||||
menuStyle: const MenuStyle(
|
||||
backgroundColor: MaterialStatePropertyAll<Color>(Colors.yellow),
|
||||
shadowColor: MaterialStatePropertyAll<Color>(Colors.green),
|
||||
surfaceTintColor: MaterialStatePropertyAll<Color>(Colors.teal),
|
||||
elevation: MaterialStatePropertyAll<double>(15.0),
|
||||
shape: MaterialStatePropertyAll<OutlinedBorder>(
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(8.0))),
|
||||
),
|
||||
),
|
||||
inputDecorationTheme: const InputDecorationTheme(filled: true, fillColor: Colors.blue),
|
||||
);
|
||||
|
||||
final ThemeData theme = ThemeData(dropdownMenuTheme: global);
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: theme,
|
||||
home: DropdownMenuTheme(
|
||||
data: dropdownMenuTheme,
|
||||
child: Scaffold(
|
||||
body: Center(
|
||||
child: DropdownMenu(
|
||||
textStyle: TextStyle(
|
||||
color: Colors.pink,
|
||||
backgroundColor: Colors.cyan,
|
||||
fontSize: 32.0,
|
||||
shadows: kElevationToShadow[3],
|
||||
decoration: TextDecoration.overline,
|
||||
wordSpacing: 3.0,
|
||||
),
|
||||
menuStyle: const MenuStyle(
|
||||
backgroundColor: MaterialStatePropertyAll<Color>(Colors.limeAccent),
|
||||
shadowColor: MaterialStatePropertyAll<Color>(Colors.deepOrangeAccent),
|
||||
surfaceTintColor: MaterialStatePropertyAll<Color>(Colors.lightBlue),
|
||||
elevation: MaterialStatePropertyAll<double>(21.0),
|
||||
shape: MaterialStatePropertyAll<OutlinedBorder>(
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(15.0))),
|
||||
),
|
||||
),
|
||||
inputDecorationTheme: const InputDecorationTheme(filled: true, fillColor: Colors.deepPurple),
|
||||
dropdownMenuEntries: const <DropdownMenuEntry>[
|
||||
DropdownMenuEntry(label: 'Item 0'),
|
||||
DropdownMenuEntry(label: 'Item 1'),
|
||||
DropdownMenuEntry(label: 'Item 2'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
final EditableText editableText = tester.widget(find.byType(EditableText));
|
||||
expect(editableText.style.color, Colors.pink);
|
||||
expect(editableText.style.backgroundColor, Colors.cyan);
|
||||
expect(editableText.style.fontSize, 32.0);
|
||||
expect(editableText.style.shadows, kElevationToShadow[3]);
|
||||
expect(editableText.style.decoration, TextDecoration.overline);
|
||||
expect(editableText.style.wordSpacing, 3.0);
|
||||
|
||||
final TextField textField = tester.widget(find.byType(TextField));
|
||||
expect(textField.decoration?.filled, isTrue);
|
||||
expect(textField.decoration?.fillColor, Colors.deepPurple);
|
||||
|
||||
await tester.tap(find.widgetWithIcon(IconButton, Icons.arrow_drop_down).first);
|
||||
await tester.pump();
|
||||
expect(find.byType(MenuAnchor), findsOneWidget);
|
||||
|
||||
final Finder menuMaterial = find.ancestor(
|
||||
of: find.widgetWithText(TextButton, 'Item 0'),
|
||||
matching: find.byType(Material),
|
||||
).last;
|
||||
Material material = tester.widget<Material>(menuMaterial);
|
||||
expect(material.color, Colors.limeAccent);
|
||||
expect(material.shadowColor, Colors.deepOrangeAccent);
|
||||
expect(material.surfaceTintColor, Colors.lightBlue);
|
||||
expect(material.elevation, 21.0);
|
||||
expect(material.shape, const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(15.0))));
|
||||
|
||||
final Finder buttonMaterial = find.descendant(
|
||||
of: find.widgetWithText(TextButton, 'Item 0'),
|
||||
matching: find.byType(Material),
|
||||
).last;
|
||||
|
||||
material = tester.widget<Material>(buttonMaterial);
|
||||
expect(material.color, Colors.transparent);
|
||||
expect(material.elevation, 0.0);
|
||||
expect(material.shape, const RoundedRectangleBorder());
|
||||
expect(material.textStyle?.color, theme.colorScheme.onSurface);
|
||||
});
|
||||
}
|
@ -784,6 +784,7 @@ void main() {
|
||||
dialogTheme: const DialogTheme(backgroundColor: Colors.black),
|
||||
dividerTheme: const DividerThemeData(color: Colors.black),
|
||||
drawerTheme: const DrawerThemeData(),
|
||||
dropdownMenuTheme: const DropdownMenuThemeData(),
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(style: ElevatedButton.styleFrom(backgroundColor: Colors.green)),
|
||||
expansionTileTheme: const ExpansionTileThemeData(backgroundColor: Colors.black),
|
||||
filledButtonTheme: FilledButtonThemeData(style: FilledButton.styleFrom(foregroundColor: Colors.green)),
|
||||
@ -904,6 +905,7 @@ void main() {
|
||||
dialogTheme: const DialogTheme(backgroundColor: Colors.white),
|
||||
dividerTheme: const DividerThemeData(color: Colors.white),
|
||||
drawerTheme: const DrawerThemeData(),
|
||||
dropdownMenuTheme: const DropdownMenuThemeData(),
|
||||
elevatedButtonTheme: const ElevatedButtonThemeData(),
|
||||
expansionTileTheme: const ExpansionTileThemeData(backgroundColor: Colors.black),
|
||||
filledButtonTheme: const FilledButtonThemeData(),
|
||||
@ -1253,6 +1255,7 @@ void main() {
|
||||
'dialogTheme',
|
||||
'dividerTheme',
|
||||
'drawerTheme',
|
||||
'dropdownMenuTheme',
|
||||
'elevatedButtonTheme',
|
||||
'expansionTileTheme',
|
||||
'filledButtonTheme',
|
||||
|
Loading…
Reference in New Issue
Block a user