Unverified Commit 8b86d238 authored by Qun Cheng's avatar Qun Cheng Committed by GitHub

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: 's avatarQun Cheng <quncheng@google.com>
parent 1cb16a1e
// 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';
......
// 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());
}
}
// 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,10 +2440,13 @@ class _MenuDirectionalFocusAction extends DirectionalFocusAction {
// correct node.
if (currentMenu.widget.childFocusNode != null) {
final FocusTraversalPolicy? policy = FocusTraversalGroup.maybeOf(primaryFocus!.context!);
if (currentMenu.widget.childFocusNode!.nearestScope != null) {
policy?.invalidateScopeData(currentMenu.widget.childFocusNode!.nearestScope!);
}
return false;
}
return false;
}
bool _moveToNextTopLevel(_MenuAnchorState currentMenu) {
final _MenuAnchorState? sibling = currentMenu._topLevel._nextSibling;
......@@ -2455,10 +2474,13 @@ class _MenuDirectionalFocusAction extends DirectionalFocusAction {
// correct node.
if (currentMenu.widget.childFocusNode != null) {
final FocusTraversalPolicy? policy = FocusTraversalGroup.maybeOf(primaryFocus!.context!);
if (currentMenu.widget.childFocusNode!.nearestScope != null) {
policy?.invalidateScopeData(currentMenu.widget.childFocusNode!.nearestScope!);
}
return false;
}
return false;
}
bool _moveToPreviousTopLevel(_MenuAnchorState currentMenu) {
final _MenuAnchorState? sibling = currentMenu._topLevel._previousSibling;
......@@ -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,13 +3327,8 @@ class _MenuPanelState extends State<_MenuPanel> {
);
}
}
return ConstrainedBox(
constraints: effectiveConstraints,
child: UnconstrainedBox(
constrainedAxis: widget.orientation,
clipBehavior: Clip.hardEdge,
alignment: AlignmentDirectional.centerStart,
child: _intrinsicCrossSize(
Widget menuPanel = _intrinsicCrossSize(
child: Material(
elevation: elevation,
shape: shape,
......@@ -3326,8 +3351,20 @@ class _MenuPanelState extends State<_MenuPanel> {
),
),
),
),
),
);
if (widget.crossAxisUnconstrained) {
menuPanel = UnconstrainedBox(
constrainedAxis: widget.orientation,
clipBehavior: Clip.hardEdge,
alignment: AlignmentDirectional.centerStart,
child: menuPanel,
);
}
return ConstrainedBox(
constraints: effectiveConstraints,
child: menuPanel,
);
}
......@@ -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));
......
// 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;
}
// 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',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment