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_header.dart';
|
||||||
export 'src/material/drawer_theme.dart';
|
export 'src/material/drawer_theme.dart';
|
||||||
export 'src/material/dropdown.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.dart';
|
||||||
export 'src/material/elevated_button_theme.dart';
|
export 'src/material/elevated_button_theme.dart';
|
||||||
export 'src/material/elevation_overlay.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.anchorTapClosesMenu = false,
|
||||||
this.onOpen,
|
this.onOpen,
|
||||||
this.onClose,
|
this.onClose,
|
||||||
|
this.crossAxisUnconstrained = true,
|
||||||
required this.menuChildren,
|
required this.menuChildren,
|
||||||
this.builder,
|
this.builder,
|
||||||
this.child,
|
this.child,
|
||||||
@ -213,6 +214,14 @@ class MenuAnchor extends StatefulWidget {
|
|||||||
/// A callback that is invoked when the menu is closed.
|
/// A callback that is invoked when the menu is closed.
|
||||||
final VoidCallback? onClose;
|
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
|
/// A list of children containing the menu items that are the contents of the
|
||||||
/// menu surrounded by this [MenuAnchor].
|
/// menu surrounded by this [MenuAnchor].
|
||||||
///
|
///
|
||||||
@ -516,6 +525,7 @@ class _MenuAnchorState extends State<MenuAnchor> {
|
|||||||
menuPosition: position,
|
menuPosition: position,
|
||||||
clipBehavior: widget.clipBehavior,
|
clipBehavior: widget.clipBehavior,
|
||||||
menuChildren: widget.menuChildren,
|
menuChildren: widget.menuChildren,
|
||||||
|
crossAxisUnconstrained: widget.crossAxisUnconstrained,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
to: overlay.context,
|
to: overlay.context,
|
||||||
@ -791,6 +801,7 @@ class MenuItemButton extends StatefulWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
this.onPressed,
|
this.onPressed,
|
||||||
this.onHover,
|
this.onHover,
|
||||||
|
this.requestFocusOnHover = true,
|
||||||
this.onFocusChange,
|
this.onFocusChange,
|
||||||
this.focusNode,
|
this.focusNode,
|
||||||
this.shortcut,
|
this.shortcut,
|
||||||
@ -817,6 +828,11 @@ class MenuItemButton extends StatefulWidget {
|
|||||||
/// area and false if a pointer has exited.
|
/// area and false if a pointer has exited.
|
||||||
final ValueChanged<bool>? onHover;
|
final ValueChanged<bool>? onHover;
|
||||||
|
|
||||||
|
/// Determine if hovering can request focus.
|
||||||
|
///
|
||||||
|
/// Defaults to true.
|
||||||
|
final bool requestFocusOnHover;
|
||||||
|
|
||||||
/// Handler called when the focus changes.
|
/// Handler called when the focus changes.
|
||||||
///
|
///
|
||||||
/// Called with true if this widget's node gains focus, and false if it loses
|
/// 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) {
|
void _handleHover(bool hovering) {
|
||||||
widget.onHover?.call(hovering);
|
widget.onHover?.call(hovering);
|
||||||
if (hovering) {
|
if (hovering && widget.requestFocusOnHover) {
|
||||||
assert(_debugMenuInfo('Requesting focus for $_focusNode from hover'));
|
assert(_debugMenuInfo('Requesting focus for $_focusNode from hover'));
|
||||||
_focusNode.requestFocus();
|
_focusNode.requestFocus();
|
||||||
}
|
}
|
||||||
@ -2424,10 +2440,13 @@ class _MenuDirectionalFocusAction extends DirectionalFocusAction {
|
|||||||
// correct node.
|
// correct node.
|
||||||
if (currentMenu.widget.childFocusNode != null) {
|
if (currentMenu.widget.childFocusNode != null) {
|
||||||
final FocusTraversalPolicy? policy = FocusTraversalGroup.maybeOf(primaryFocus!.context!);
|
final FocusTraversalPolicy? policy = FocusTraversalGroup.maybeOf(primaryFocus!.context!);
|
||||||
|
if (currentMenu.widget.childFocusNode!.nearestScope != null) {
|
||||||
policy?.invalidateScopeData(currentMenu.widget.childFocusNode!.nearestScope!);
|
policy?.invalidateScopeData(currentMenu.widget.childFocusNode!.nearestScope!);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
bool _moveToNextTopLevel(_MenuAnchorState currentMenu) {
|
bool _moveToNextTopLevel(_MenuAnchorState currentMenu) {
|
||||||
final _MenuAnchorState? sibling = currentMenu._topLevel._nextSibling;
|
final _MenuAnchorState? sibling = currentMenu._topLevel._nextSibling;
|
||||||
@ -2455,10 +2474,13 @@ class _MenuDirectionalFocusAction extends DirectionalFocusAction {
|
|||||||
// correct node.
|
// correct node.
|
||||||
if (currentMenu.widget.childFocusNode != null) {
|
if (currentMenu.widget.childFocusNode != null) {
|
||||||
final FocusTraversalPolicy? policy = FocusTraversalGroup.maybeOf(primaryFocus!.context!);
|
final FocusTraversalPolicy? policy = FocusTraversalGroup.maybeOf(primaryFocus!.context!);
|
||||||
|
if (currentMenu.widget.childFocusNode!.nearestScope != null) {
|
||||||
policy?.invalidateScopeData(currentMenu.widget.childFocusNode!.nearestScope!);
|
policy?.invalidateScopeData(currentMenu.widget.childFocusNode!.nearestScope!);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
bool _moveToPreviousTopLevel(_MenuAnchorState currentMenu) {
|
bool _moveToPreviousTopLevel(_MenuAnchorState currentMenu) {
|
||||||
final _MenuAnchorState? sibling = currentMenu._topLevel._previousSibling;
|
final _MenuAnchorState? sibling = currentMenu._topLevel._previousSibling;
|
||||||
@ -3198,6 +3220,7 @@ class _MenuPanel extends StatefulWidget {
|
|||||||
required this.menuStyle,
|
required this.menuStyle,
|
||||||
this.clipBehavior = Clip.none,
|
this.clipBehavior = Clip.none,
|
||||||
required this.orientation,
|
required this.orientation,
|
||||||
|
this.crossAxisUnconstrained = true,
|
||||||
required this.children,
|
required this.children,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -3209,6 +3232,13 @@ class _MenuPanel extends StatefulWidget {
|
|||||||
/// Defaults to [Clip.none].
|
/// Defaults to [Clip.none].
|
||||||
final Clip clipBehavior;
|
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.
|
/// The layout orientation of this panel.
|
||||||
final Axis orientation;
|
final Axis orientation;
|
||||||
|
|
||||||
@ -3297,13 +3327,8 @@ class _MenuPanelState extends State<_MenuPanel> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ConstrainedBox(
|
|
||||||
constraints: effectiveConstraints,
|
Widget menuPanel = _intrinsicCrossSize(
|
||||||
child: UnconstrainedBox(
|
|
||||||
constrainedAxis: widget.orientation,
|
|
||||||
clipBehavior: Clip.hardEdge,
|
|
||||||
alignment: AlignmentDirectional.centerStart,
|
|
||||||
child: _intrinsicCrossSize(
|
|
||||||
child: Material(
|
child: Material(
|
||||||
elevation: elevation,
|
elevation: elevation,
|
||||||
shape: shape,
|
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.menuPosition,
|
||||||
required this.alignmentOffset,
|
required this.alignmentOffset,
|
||||||
required this.clipBehavior,
|
required this.clipBehavior,
|
||||||
|
this.crossAxisUnconstrained = true,
|
||||||
required this.menuChildren,
|
required this.menuChildren,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -3357,6 +3395,7 @@ class _Submenu extends StatelessWidget {
|
|||||||
final Offset? menuPosition;
|
final Offset? menuPosition;
|
||||||
final Offset alignmentOffset;
|
final Offset alignmentOffset;
|
||||||
final Clip clipBehavior;
|
final Clip clipBehavior;
|
||||||
|
final bool crossAxisUnconstrained;
|
||||||
final List<Widget> menuChildren;
|
final List<Widget> menuChildren;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -3454,6 +3493,7 @@ class _Submenu extends StatelessWidget {
|
|||||||
menuStyle: menuStyle,
|
menuStyle: menuStyle,
|
||||||
clipBehavior: clipBehavior,
|
clipBehavior: clipBehavior,
|
||||||
orientation: anchor._orientation,
|
orientation: anchor._orientation,
|
||||||
|
crossAxisUnconstrained: crossAxisUnconstrained,
|
||||||
children: menuChildren,
|
children: menuChildren,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -25,6 +25,7 @@ import 'data_table_theme.dart';
|
|||||||
import 'dialog_theme.dart';
|
import 'dialog_theme.dart';
|
||||||
import 'divider_theme.dart';
|
import 'divider_theme.dart';
|
||||||
import 'drawer_theme.dart';
|
import 'drawer_theme.dart';
|
||||||
|
import 'dropdown_menu_theme.dart';
|
||||||
import 'elevated_button_theme.dart';
|
import 'elevated_button_theme.dart';
|
||||||
import 'expansion_tile_theme.dart';
|
import 'expansion_tile_theme.dart';
|
||||||
import 'filled_button_theme.dart';
|
import 'filled_button_theme.dart';
|
||||||
@ -348,6 +349,7 @@ class ThemeData with Diagnosticable {
|
|||||||
DialogTheme? dialogTheme,
|
DialogTheme? dialogTheme,
|
||||||
DividerThemeData? dividerTheme,
|
DividerThemeData? dividerTheme,
|
||||||
DrawerThemeData? drawerTheme,
|
DrawerThemeData? drawerTheme,
|
||||||
|
DropdownMenuThemeData? dropdownMenuTheme,
|
||||||
ElevatedButtonThemeData? elevatedButtonTheme,
|
ElevatedButtonThemeData? elevatedButtonTheme,
|
||||||
ExpansionTileThemeData? expansionTileTheme,
|
ExpansionTileThemeData? expansionTileTheme,
|
||||||
FilledButtonThemeData? filledButtonTheme,
|
FilledButtonThemeData? filledButtonTheme,
|
||||||
@ -602,6 +604,7 @@ class ThemeData with Diagnosticable {
|
|||||||
dialogTheme ??= const DialogTheme();
|
dialogTheme ??= const DialogTheme();
|
||||||
dividerTheme ??= const DividerThemeData();
|
dividerTheme ??= const DividerThemeData();
|
||||||
drawerTheme ??= const DrawerThemeData();
|
drawerTheme ??= const DrawerThemeData();
|
||||||
|
dropdownMenuTheme ??= const DropdownMenuThemeData();
|
||||||
elevatedButtonTheme ??= const ElevatedButtonThemeData();
|
elevatedButtonTheme ??= const ElevatedButtonThemeData();
|
||||||
expansionTileTheme ??= const ExpansionTileThemeData();
|
expansionTileTheme ??= const ExpansionTileThemeData();
|
||||||
filledButtonTheme ??= const FilledButtonThemeData();
|
filledButtonTheme ??= const FilledButtonThemeData();
|
||||||
@ -699,6 +702,7 @@ class ThemeData with Diagnosticable {
|
|||||||
dialogTheme: dialogTheme,
|
dialogTheme: dialogTheme,
|
||||||
dividerTheme: dividerTheme,
|
dividerTheme: dividerTheme,
|
||||||
drawerTheme: drawerTheme,
|
drawerTheme: drawerTheme,
|
||||||
|
dropdownMenuTheme: dropdownMenuTheme,
|
||||||
elevatedButtonTheme: elevatedButtonTheme,
|
elevatedButtonTheme: elevatedButtonTheme,
|
||||||
expansionTileTheme: expansionTileTheme,
|
expansionTileTheme: expansionTileTheme,
|
||||||
filledButtonTheme: filledButtonTheme,
|
filledButtonTheme: filledButtonTheme,
|
||||||
@ -812,6 +816,7 @@ class ThemeData with Diagnosticable {
|
|||||||
required this.dialogTheme,
|
required this.dialogTheme,
|
||||||
required this.dividerTheme,
|
required this.dividerTheme,
|
||||||
required this.drawerTheme,
|
required this.drawerTheme,
|
||||||
|
required this.dropdownMenuTheme,
|
||||||
required this.elevatedButtonTheme,
|
required this.elevatedButtonTheme,
|
||||||
required this.expansionTileTheme,
|
required this.expansionTileTheme,
|
||||||
required this.filledButtonTheme,
|
required this.filledButtonTheme,
|
||||||
@ -983,6 +988,7 @@ class ThemeData with Diagnosticable {
|
|||||||
assert(dialogTheme != null),
|
assert(dialogTheme != null),
|
||||||
assert(dividerTheme != null),
|
assert(dividerTheme != null),
|
||||||
assert(drawerTheme != null),
|
assert(drawerTheme != null),
|
||||||
|
assert(dropdownMenuTheme != null),
|
||||||
assert(elevatedButtonTheme != null),
|
assert(elevatedButtonTheme != null),
|
||||||
assert(expansionTileTheme != null),
|
assert(expansionTileTheme != null),
|
||||||
assert(filledButtonTheme != null),
|
assert(filledButtonTheme != null),
|
||||||
@ -1554,6 +1560,9 @@ class ThemeData with Diagnosticable {
|
|||||||
/// A theme for customizing the appearance and layout of [Drawer] widgets.
|
/// A theme for customizing the appearance and layout of [Drawer] widgets.
|
||||||
final DrawerThemeData drawerTheme;
|
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
|
/// A theme for customizing the appearance and internal layout of
|
||||||
/// [ElevatedButton]s.
|
/// [ElevatedButton]s.
|
||||||
final ElevatedButtonThemeData elevatedButtonTheme;
|
final ElevatedButtonThemeData elevatedButtonTheme;
|
||||||
@ -1816,6 +1825,13 @@ class ThemeData with Diagnosticable {
|
|||||||
Color get toggleableActiveColor => _toggleableActiveColor!;
|
Color get toggleableActiveColor => _toggleableActiveColor!;
|
||||||
final Color? _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.
|
/// Creates a copy of this theme but with the given fields replaced with the new values.
|
||||||
///
|
///
|
||||||
/// The [brightness] value is applied to the [colorScheme].
|
/// The [brightness] value is applied to the [colorScheme].
|
||||||
@ -1883,6 +1899,7 @@ class ThemeData with Diagnosticable {
|
|||||||
DialogTheme? dialogTheme,
|
DialogTheme? dialogTheme,
|
||||||
DividerThemeData? dividerTheme,
|
DividerThemeData? dividerTheme,
|
||||||
DrawerThemeData? drawerTheme,
|
DrawerThemeData? drawerTheme,
|
||||||
|
DropdownMenuThemeData? dropdownMenuTheme,
|
||||||
ElevatedButtonThemeData? elevatedButtonTheme,
|
ElevatedButtonThemeData? elevatedButtonTheme,
|
||||||
ExpansionTileThemeData? expansionTileTheme,
|
ExpansionTileThemeData? expansionTileTheme,
|
||||||
FilledButtonThemeData? filledButtonTheme,
|
FilledButtonThemeData? filledButtonTheme,
|
||||||
@ -2047,6 +2064,7 @@ class ThemeData with Diagnosticable {
|
|||||||
dialogTheme: dialogTheme ?? this.dialogTheme,
|
dialogTheme: dialogTheme ?? this.dialogTheme,
|
||||||
dividerTheme: dividerTheme ?? this.dividerTheme,
|
dividerTheme: dividerTheme ?? this.dividerTheme,
|
||||||
drawerTheme: drawerTheme ?? this.drawerTheme,
|
drawerTheme: drawerTheme ?? this.drawerTheme,
|
||||||
|
dropdownMenuTheme: dropdownMenuTheme ?? this.dropdownMenuTheme,
|
||||||
elevatedButtonTheme: elevatedButtonTheme ?? this.elevatedButtonTheme,
|
elevatedButtonTheme: elevatedButtonTheme ?? this.elevatedButtonTheme,
|
||||||
expansionTileTheme: expansionTileTheme ?? this.expansionTileTheme,
|
expansionTileTheme: expansionTileTheme ?? this.expansionTileTheme,
|
||||||
filledButtonTheme: filledButtonTheme ?? this.filledButtonTheme,
|
filledButtonTheme: filledButtonTheme ?? this.filledButtonTheme,
|
||||||
@ -2089,14 +2107,7 @@ class ThemeData with Diagnosticable {
|
|||||||
bottomAppBarColor: bottomAppBarColor ?? _bottomAppBarColor,
|
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
|
// 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 =
|
static final _FifoCache<_IdentityThemeDataCacheKey, ThemeData> _localizedThemeDataCache =
|
||||||
_FifoCache<_IdentityThemeDataCacheKey, ThemeData>(_localizedThemeDataCacheSize);
|
_FifoCache<_IdentityThemeDataCacheKey, ThemeData>(_localizedThemeDataCacheSize);
|
||||||
|
|
||||||
@ -2253,6 +2264,7 @@ class ThemeData with Diagnosticable {
|
|||||||
dialogTheme: DialogTheme.lerp(a.dialogTheme, b.dialogTheme, t),
|
dialogTheme: DialogTheme.lerp(a.dialogTheme, b.dialogTheme, t),
|
||||||
dividerTheme: DividerThemeData.lerp(a.dividerTheme, b.dividerTheme, t),
|
dividerTheme: DividerThemeData.lerp(a.dividerTheme, b.dividerTheme, t),
|
||||||
drawerTheme: DrawerThemeData.lerp(a.drawerTheme, b.drawerTheme, 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)!,
|
elevatedButtonTheme: ElevatedButtonThemeData.lerp(a.elevatedButtonTheme, b.elevatedButtonTheme, t)!,
|
||||||
expansionTileTheme: ExpansionTileThemeData.lerp(a.expansionTileTheme, b.expansionTileTheme, t)!,
|
expansionTileTheme: ExpansionTileThemeData.lerp(a.expansionTileTheme, b.expansionTileTheme, t)!,
|
||||||
filledButtonTheme: FilledButtonThemeData.lerp(a.filledButtonTheme, b.filledButtonTheme, t)!,
|
filledButtonTheme: FilledButtonThemeData.lerp(a.filledButtonTheme, b.filledButtonTheme, t)!,
|
||||||
@ -2361,6 +2373,7 @@ class ThemeData with Diagnosticable {
|
|||||||
other.dialogTheme == dialogTheme &&
|
other.dialogTheme == dialogTheme &&
|
||||||
other.dividerTheme == dividerTheme &&
|
other.dividerTheme == dividerTheme &&
|
||||||
other.drawerTheme == drawerTheme &&
|
other.drawerTheme == drawerTheme &&
|
||||||
|
other.dropdownMenuTheme == dropdownMenuTheme &&
|
||||||
other.elevatedButtonTheme == elevatedButtonTheme &&
|
other.elevatedButtonTheme == elevatedButtonTheme &&
|
||||||
other.expansionTileTheme == expansionTileTheme &&
|
other.expansionTileTheme == expansionTileTheme &&
|
||||||
other.filledButtonTheme == filledButtonTheme &&
|
other.filledButtonTheme == filledButtonTheme &&
|
||||||
@ -2466,6 +2479,7 @@ class ThemeData with Diagnosticable {
|
|||||||
dialogTheme,
|
dialogTheme,
|
||||||
dividerTheme,
|
dividerTheme,
|
||||||
drawerTheme,
|
drawerTheme,
|
||||||
|
dropdownMenuTheme,
|
||||||
elevatedButtonTheme,
|
elevatedButtonTheme,
|
||||||
expansionTileTheme,
|
expansionTileTheme,
|
||||||
filledButtonTheme,
|
filledButtonTheme,
|
||||||
@ -2573,6 +2587,7 @@ class ThemeData with Diagnosticable {
|
|||||||
properties.add(DiagnosticsProperty<DialogTheme>('dialogTheme', dialogTheme, defaultValue: defaultData.dialogTheme, level: DiagnosticLevel.debug));
|
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<DividerThemeData>('dividerTheme', dividerTheme, defaultValue: defaultData.dividerTheme, level: DiagnosticLevel.debug));
|
||||||
properties.add(DiagnosticsProperty<DrawerThemeData>('drawerTheme', drawerTheme, defaultValue: defaultData.drawerTheme, 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<ElevatedButtonThemeData>('elevatedButtonTheme', elevatedButtonTheme, defaultValue: defaultData.elevatedButtonTheme, level: DiagnosticLevel.debug));
|
||||||
properties.add(DiagnosticsProperty<ExpansionTileThemeData>('expansionTileTheme', expansionTileTheme, level: DiagnosticLevel.debug));
|
properties.add(DiagnosticsProperty<ExpansionTileThemeData>('expansionTileTheme', expansionTileTheme, level: DiagnosticLevel.debug));
|
||||||
properties.add(DiagnosticsProperty<FilledButtonThemeData>('filledButtonTheme', filledButtonTheme, defaultValue: defaultData.filledButtonTheme, 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),
|
dialogTheme: const DialogTheme(backgroundColor: Colors.black),
|
||||||
dividerTheme: const DividerThemeData(color: Colors.black),
|
dividerTheme: const DividerThemeData(color: Colors.black),
|
||||||
drawerTheme: const DrawerThemeData(),
|
drawerTheme: const DrawerThemeData(),
|
||||||
|
dropdownMenuTheme: const DropdownMenuThemeData(),
|
||||||
elevatedButtonTheme: ElevatedButtonThemeData(style: ElevatedButton.styleFrom(backgroundColor: Colors.green)),
|
elevatedButtonTheme: ElevatedButtonThemeData(style: ElevatedButton.styleFrom(backgroundColor: Colors.green)),
|
||||||
expansionTileTheme: const ExpansionTileThemeData(backgroundColor: Colors.black),
|
expansionTileTheme: const ExpansionTileThemeData(backgroundColor: Colors.black),
|
||||||
filledButtonTheme: FilledButtonThemeData(style: FilledButton.styleFrom(foregroundColor: Colors.green)),
|
filledButtonTheme: FilledButtonThemeData(style: FilledButton.styleFrom(foregroundColor: Colors.green)),
|
||||||
@ -904,6 +905,7 @@ void main() {
|
|||||||
dialogTheme: const DialogTheme(backgroundColor: Colors.white),
|
dialogTheme: const DialogTheme(backgroundColor: Colors.white),
|
||||||
dividerTheme: const DividerThemeData(color: Colors.white),
|
dividerTheme: const DividerThemeData(color: Colors.white),
|
||||||
drawerTheme: const DrawerThemeData(),
|
drawerTheme: const DrawerThemeData(),
|
||||||
|
dropdownMenuTheme: const DropdownMenuThemeData(),
|
||||||
elevatedButtonTheme: const ElevatedButtonThemeData(),
|
elevatedButtonTheme: const ElevatedButtonThemeData(),
|
||||||
expansionTileTheme: const ExpansionTileThemeData(backgroundColor: Colors.black),
|
expansionTileTheme: const ExpansionTileThemeData(backgroundColor: Colors.black),
|
||||||
filledButtonTheme: const FilledButtonThemeData(),
|
filledButtonTheme: const FilledButtonThemeData(),
|
||||||
@ -1253,6 +1255,7 @@ void main() {
|
|||||||
'dialogTheme',
|
'dialogTheme',
|
||||||
'dividerTheme',
|
'dividerTheme',
|
||||||
'drawerTheme',
|
'drawerTheme',
|
||||||
|
'dropdownMenuTheme',
|
||||||
'elevatedButtonTheme',
|
'elevatedButtonTheme',
|
||||||
'expansionTileTheme',
|
'expansionTileTheme',
|
||||||
'filledButtonTheme',
|
'filledButtonTheme',
|
||||||
|
Loading…
Reference in New Issue
Block a user