mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Added properties in DropdownButtonFormField to match DropdownButton (#36998)
* Added properties in DropdownFormField to match DropdownButton * Minor style guide related changes
This commit is contained in:
parent
77f71ef48f
commit
45d57e780f
3
AUTHORS
3
AUTHORS
@ -39,4 +39,5 @@ Marco Scannadinari <m@scannadinari.co.uk>
|
|||||||
Frederik Schweiger <mail@flschweiger.net>
|
Frederik Schweiger <mail@flschweiger.net>
|
||||||
Martin Staadecker <machstg@gmail.com>
|
Martin Staadecker <machstg@gmail.com>
|
||||||
Igor Katsuba <katsuba.igor@gmail.com>
|
Igor Katsuba <katsuba.igor@gmail.com>
|
||||||
Diego Velásquez <diego.velasquez.lopez@gmail.com>
|
Diego Velásquez <diego.velasquez.lopez@gmail.com>
|
||||||
|
Sarbagya Dhaubanjar <mail@sarbagyastha.com.np>
|
@ -184,24 +184,24 @@ class _DropdownMenuState<T> extends State<_DropdownMenu<T>> {
|
|||||||
explicitChildNodes: true,
|
explicitChildNodes: true,
|
||||||
label: localizations.popupMenuLabel,
|
label: localizations.popupMenuLabel,
|
||||||
child: Material(
|
child: Material(
|
||||||
type: MaterialType.transparency,
|
type: MaterialType.transparency,
|
||||||
textStyle: route.style,
|
textStyle: route.style,
|
||||||
child: ScrollConfiguration(
|
child: ScrollConfiguration(
|
||||||
behavior: const _DropdownScrollBehavior(),
|
behavior: const _DropdownScrollBehavior(),
|
||||||
child: Scrollbar(
|
child: Scrollbar(
|
||||||
child: ListView(
|
child: ListView(
|
||||||
controller: widget.route.scrollController,
|
controller: widget.route.scrollController,
|
||||||
padding: kMaterialListPadding,
|
padding: kMaterialListPadding,
|
||||||
itemExtent: _kMenuItemHeight,
|
itemExtent: _kMenuItemHeight,
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
children: children,
|
children: children,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -253,7 +253,7 @@ class _DropdownMenuRouteLayout<T> extends SingleChildLayoutDelegate {
|
|||||||
double left;
|
double left;
|
||||||
switch (textDirection) {
|
switch (textDirection) {
|
||||||
case TextDirection.rtl:
|
case TextDirection.rtl:
|
||||||
left = buttonRect.right.clamp(0.0, size.width) - childSize.width;
|
left = buttonRect.right.clamp(0.0, size.width) - childSize.width;
|
||||||
break;
|
break;
|
||||||
case TextDirection.ltr:
|
case TextDirection.ltr:
|
||||||
left = buttonRect.left.clamp(0.0, size.width - childSize.width);
|
left = buttonRect.left.clamp(0.0, size.width - childSize.width);
|
||||||
@ -637,7 +637,7 @@ class DropdownButton<T> extends StatefulWidget {
|
|||||||
/// if the first item were selected.
|
/// if the first item were selected.
|
||||||
final T value;
|
final T value;
|
||||||
|
|
||||||
/// Displayed if [value] is null.
|
/// A placeholder widget that is displayed if no item is selected, i.e. if [value] is null.
|
||||||
final Widget hint;
|
final Widget hint;
|
||||||
|
|
||||||
/// A message to show when the dropdown is disabled.
|
/// A message to show when the dropdown is disabled.
|
||||||
@ -645,12 +645,14 @@ class DropdownButton<T> extends StatefulWidget {
|
|||||||
/// Displayed if [items] or [onChanged] is null.
|
/// Displayed if [items] or [onChanged] is null.
|
||||||
final Widget disabledHint;
|
final Widget disabledHint;
|
||||||
|
|
||||||
|
/// {@template flutter.material.dropdownButton.onChanged}
|
||||||
/// Called when the user selects an item.
|
/// Called when the user selects an item.
|
||||||
///
|
///
|
||||||
/// If the [onChanged] callback is null or the list of [items] is null
|
/// If the [onChanged] callback is null or the list of [items] is null
|
||||||
/// then the dropdown button will be disabled, i.e. its arrow will be
|
/// then the dropdown button will be disabled, i.e. its arrow will be
|
||||||
/// displayed in grey and it will not respond to input. A disabled button
|
/// displayed in grey and it will not respond to input. A disabled button
|
||||||
/// will display the [disabledHint] widget if it is non-null.
|
/// will display the [disabledHint] widget if it is non-null.
|
||||||
|
/// {@endtemplate}
|
||||||
final ValueChanged<T> onChanged;
|
final ValueChanged<T> onChanged;
|
||||||
|
|
||||||
/// The z-coordinate at which to place the menu when open.
|
/// The z-coordinate at which to place the menu when open.
|
||||||
@ -777,7 +779,7 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi
|
|||||||
final Rect itemRect = itemBox.localToGlobal(Offset.zero) & itemBox.size;
|
final Rect itemRect = itemBox.localToGlobal(Offset.zero) & itemBox.size;
|
||||||
final TextDirection textDirection = Directionality.of(context);
|
final TextDirection textDirection = Directionality.of(context);
|
||||||
final EdgeInsetsGeometry menuMargin = ButtonTheme.of(context).alignedDropdown
|
final EdgeInsetsGeometry menuMargin = ButtonTheme.of(context).alignedDropdown
|
||||||
?_kAlignedMenuMargin
|
? _kAlignedMenuMargin
|
||||||
: _kUnalignedMenuMargin;
|
: _kUnalignedMenuMargin;
|
||||||
|
|
||||||
assert(_dropdownRoute == null);
|
assert(_dropdownRoute == null);
|
||||||
@ -813,22 +815,20 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi
|
|||||||
Color get _iconColor {
|
Color get _iconColor {
|
||||||
// These colors are not defined in the Material Design spec.
|
// These colors are not defined in the Material Design spec.
|
||||||
if (_enabled) {
|
if (_enabled) {
|
||||||
if (widget.iconEnabledColor != null) {
|
if (widget.iconEnabledColor != null)
|
||||||
return widget.iconEnabledColor;
|
return widget.iconEnabledColor;
|
||||||
}
|
|
||||||
|
|
||||||
switch(Theme.of(context).brightness) {
|
switch (Theme.of(context).brightness) {
|
||||||
case Brightness.light:
|
case Brightness.light:
|
||||||
return Colors.grey.shade700;
|
return Colors.grey.shade700;
|
||||||
case Brightness.dark:
|
case Brightness.dark:
|
||||||
return Colors.white70;
|
return Colors.white70;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (widget.iconDisabledColor != null) {
|
if (widget.iconDisabledColor != null)
|
||||||
return widget.iconDisabledColor;
|
return widget.iconDisabledColor;
|
||||||
}
|
|
||||||
|
|
||||||
switch(Theme.of(context).brightness) {
|
switch (Theme.of(context).brightness) {
|
||||||
case Brightness.light:
|
case Brightness.light:
|
||||||
return Colors.grey.shade400;
|
return Colors.grey.shade400;
|
||||||
case Brightness.dark:
|
case Brightness.dark:
|
||||||
@ -852,8 +852,9 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi
|
|||||||
final List<Widget> items = _enabled ? List<Widget>.from(widget.items) : <Widget>[];
|
final List<Widget> items = _enabled ? List<Widget>.from(widget.items) : <Widget>[];
|
||||||
int hintIndex;
|
int hintIndex;
|
||||||
if (widget.hint != null || (!_enabled && widget.disabledHint != null)) {
|
if (widget.hint != null || (!_enabled && widget.disabledHint != null)) {
|
||||||
final Widget emplacedHint =
|
final Widget emplacedHint = _enabled
|
||||||
_enabled ? widget.hint : DropdownMenuItem<Widget>(child: widget.disabledHint ?? widget.hint);
|
? widget.hint
|
||||||
|
: DropdownMenuItem<Widget>(child: widget.disabledHint ?? widget.hint);
|
||||||
hintIndex = items.length;
|
hintIndex = items.length;
|
||||||
items.add(DefaultTextStyle(
|
items.add(DefaultTextStyle(
|
||||||
style: _textStyle.copyWith(color: Theme.of(context).hintColor),
|
style: _textStyle.copyWith(color: Theme.of(context).hintColor),
|
||||||
@ -893,7 +894,9 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi
|
|||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
widget.isExpanded ? Expanded(child: innerItemsWidget) : innerItemsWidget,
|
widget.isExpanded
|
||||||
|
? Expanded(child: innerItemsWidget)
|
||||||
|
: innerItemsWidget,
|
||||||
IconTheme(
|
IconTheme(
|
||||||
data: IconThemeData(
|
data: IconThemeData(
|
||||||
color: _iconColor,
|
color: _iconColor,
|
||||||
@ -918,7 +921,12 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi
|
|||||||
child: widget.underline ?? Container(
|
child: widget.underline ?? Container(
|
||||||
height: 1.0,
|
height: 1.0,
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
border: Border(bottom: BorderSide(color: Color(0xFFBDBDBD), width: 0.0))
|
border: Border(
|
||||||
|
bottom: BorderSide(
|
||||||
|
color: Color(0xFFBDBDBD),
|
||||||
|
width: 0.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -947,39 +955,73 @@ class DropdownButtonFormField<T> extends FormField<T> {
|
|||||||
Key key,
|
Key key,
|
||||||
T value,
|
T value,
|
||||||
@required List<DropdownMenuItem<T>> items,
|
@required List<DropdownMenuItem<T>> items,
|
||||||
this.onChanged,
|
Widget hint,
|
||||||
InputDecoration decoration = const InputDecoration(),
|
@required this.onChanged,
|
||||||
|
this.decoration = const InputDecoration(),
|
||||||
FormFieldSetter<T> onSaved,
|
FormFieldSetter<T> onSaved,
|
||||||
FormFieldValidator<T> validator,
|
FormFieldValidator<T> validator,
|
||||||
Widget hint,
|
bool autovalidate = false,
|
||||||
}) : assert(decoration != null),
|
Widget disabledHint,
|
||||||
|
int elevation = 8,
|
||||||
|
TextStyle style,
|
||||||
|
Widget icon,
|
||||||
|
Color iconDisabledColor,
|
||||||
|
Color iconEnabledColor,
|
||||||
|
double iconSize = 24.0,
|
||||||
|
bool isDense = false,
|
||||||
|
bool isExpanded = false,
|
||||||
|
}) : assert(items == null || items.isEmpty || value == null || items.where((DropdownMenuItem<T> item) => item.value == value).length == 1),
|
||||||
|
assert(decoration != null),
|
||||||
|
assert(elevation != null),
|
||||||
|
assert(iconSize != null),
|
||||||
|
assert(isDense != null),
|
||||||
|
assert(isExpanded != null),
|
||||||
super(
|
super(
|
||||||
key: key,
|
key: key,
|
||||||
onSaved: onSaved,
|
onSaved: onSaved,
|
||||||
initialValue: value,
|
initialValue: value,
|
||||||
validator: validator,
|
validator: validator,
|
||||||
|
autovalidate: autovalidate,
|
||||||
builder: (FormFieldState<T> field) {
|
builder: (FormFieldState<T> field) {
|
||||||
final InputDecoration effectiveDecoration = decoration
|
final InputDecoration effectiveDecoration = decoration.applyDefaults(
|
||||||
.applyDefaults(Theme.of(field.context).inputDecorationTheme);
|
Theme.of(field.context).inputDecorationTheme,
|
||||||
|
);
|
||||||
return InputDecorator(
|
return InputDecorator(
|
||||||
decoration: effectiveDecoration.copyWith(errorText: field.errorText),
|
decoration: effectiveDecoration.copyWith(errorText: field.errorText),
|
||||||
isEmpty: value == null,
|
isEmpty: value == null,
|
||||||
child: DropdownButtonHideUnderline(
|
child: DropdownButtonHideUnderline(
|
||||||
child: DropdownButton<T>(
|
child: DropdownButton<T>(
|
||||||
isDense: true,
|
|
||||||
value: value,
|
value: value,
|
||||||
items: items,
|
items: items,
|
||||||
hint: hint,
|
hint: hint,
|
||||||
onChanged: field.didChange,
|
onChanged: onChanged == null ? null : field.didChange,
|
||||||
|
disabledHint: disabledHint,
|
||||||
|
elevation: elevation,
|
||||||
|
style: style,
|
||||||
|
icon: icon,
|
||||||
|
iconDisabledColor: iconDisabledColor,
|
||||||
|
iconEnabledColor: iconEnabledColor,
|
||||||
|
iconSize: iconSize,
|
||||||
|
isDense: isDense,
|
||||||
|
isExpanded: isExpanded,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Called when the user selects an item.
|
/// {@macro flutter.material.dropdownButton.onChanged}
|
||||||
final ValueChanged<T> onChanged;
|
final ValueChanged<T> onChanged;
|
||||||
|
|
||||||
|
/// The decoration to show around the dropdown button form field.
|
||||||
|
///
|
||||||
|
/// By default, draws a horizontal line under the dropdown button field but can be
|
||||||
|
/// configured to show an icon, label, hint text, and error text.
|
||||||
|
///
|
||||||
|
/// Specify null to remove the decoration entirely (including the
|
||||||
|
/// extra padding introduced by the decoration to save space for the labels).
|
||||||
|
final InputDecoration decoration;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FormFieldState<T> createState() => _DropdownButtonFormFieldState<T>();
|
FormFieldState<T> createState() => _DropdownButtonFormFieldState<T>();
|
||||||
}
|
}
|
||||||
@ -991,7 +1033,7 @@ class _DropdownButtonFormFieldState<T> extends FormFieldState<T> {
|
|||||||
@override
|
@override
|
||||||
void didChange(T value) {
|
void didChange(T value) {
|
||||||
super.didChange(value);
|
super.didChange(value);
|
||||||
if (widget.onChanged != null)
|
assert(widget.onChanged != null);
|
||||||
widget.onChanged(value);
|
widget.onChanged(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ import 'package:flutter_test/flutter_test.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
|
|
||||||
|
import '../rendering/mock_canvas.dart';
|
||||||
import '../widgets/semantics_tester.dart';
|
import '../widgets/semantics_tester.dart';
|
||||||
|
|
||||||
const List<String> menuItems = <String>['one', 'two', 'three', 'four'];
|
const List<String> menuItems = <String>['one', 'two', 'three', 'four'];
|
||||||
@ -76,6 +77,59 @@ Widget buildFrame({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget buildFormFrame({
|
||||||
|
Key buttonKey,
|
||||||
|
bool autovalidate = false,
|
||||||
|
int elevation = 8,
|
||||||
|
String value = 'two',
|
||||||
|
ValueChanged<String> onChanged,
|
||||||
|
Widget icon,
|
||||||
|
Color iconDisabledColor,
|
||||||
|
Color iconEnabledColor,
|
||||||
|
double iconSize = 24.0,
|
||||||
|
bool isDense = false,
|
||||||
|
bool isExpanded = false,
|
||||||
|
Widget hint,
|
||||||
|
Widget disabledHint,
|
||||||
|
Widget underline,
|
||||||
|
List<String> items = menuItems,
|
||||||
|
Alignment alignment = Alignment.center,
|
||||||
|
TextDirection textDirection = TextDirection.ltr,
|
||||||
|
}) {
|
||||||
|
return TestApp(
|
||||||
|
textDirection: textDirection,
|
||||||
|
child: Material(
|
||||||
|
child: Align(
|
||||||
|
alignment: alignment,
|
||||||
|
child: RepaintBoundary(
|
||||||
|
child: DropdownButtonFormField<String>(
|
||||||
|
key: buttonKey,
|
||||||
|
autovalidate: autovalidate,
|
||||||
|
elevation: elevation,
|
||||||
|
value: value,
|
||||||
|
hint: hint,
|
||||||
|
disabledHint: disabledHint,
|
||||||
|
onChanged: onChanged,
|
||||||
|
icon: icon,
|
||||||
|
iconSize: iconSize,
|
||||||
|
iconDisabledColor: iconDisabledColor,
|
||||||
|
iconEnabledColor: iconEnabledColor,
|
||||||
|
isDense: isDense,
|
||||||
|
isExpanded: isExpanded,
|
||||||
|
items: items == null ? null : items.map<DropdownMenuItem<String>>((String item) {
|
||||||
|
return DropdownMenuItem<String>(
|
||||||
|
key: ValueKey<String>(item),
|
||||||
|
value: item,
|
||||||
|
child: Text(item, key: ValueKey<String>(item + 'Text')),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
class TestApp extends StatefulWidget {
|
class TestApp extends StatefulWidget {
|
||||||
const TestApp({ this.textDirection, this.child });
|
const TestApp({ this.textDirection, this.child });
|
||||||
final TextDirection textDirection;
|
final TextDirection textDirection;
|
||||||
@ -125,6 +179,29 @@ void checkSelectedItemTextGeometry(WidgetTester tester, String value) {
|
|||||||
expect(box0.size, equals(box1.size));
|
expect(box0.size, equals(box1.size));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void verifyPaintedShadow(Finder customPaint, int elevation) {
|
||||||
|
const Rect originalRectangle = Rect.fromLTRB(0.0, 0.0, 800, 208.0);
|
||||||
|
|
||||||
|
final List<BoxShadow> boxShadows = List<BoxShadow>.generate(3, (int index) => kElevationToShadow[elevation][index]);
|
||||||
|
final List<RRect> rrects = List<RRect>.generate(3, (int index) {
|
||||||
|
return RRect.fromRectAndRadius(
|
||||||
|
originalRectangle.shift(
|
||||||
|
boxShadows[index].offset
|
||||||
|
).inflate(boxShadows[index].spreadRadius),
|
||||||
|
const Radius.circular(2.0),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
customPaint,
|
||||||
|
paints
|
||||||
|
..save()
|
||||||
|
..rrect(rrect: rrects[0], color: boxShadows[0].color, hasMaskFilter: true)
|
||||||
|
..rrect(rrect: rrects[1], color: boxShadows[1].color, hasMaskFilter: true)
|
||||||
|
..rrect(rrect: rrects[2], color: boxShadows[2].color, hasMaskFilter: true),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
bool sameGeometry(RenderBox box1, RenderBox box2) {
|
bool sameGeometry(RenderBox box1, RenderBox box2) {
|
||||||
expect(box1.localToGlobal(Offset.zero), equals(box2.localToGlobal(Offset.zero)));
|
expect(box1.localToGlobal(Offset.zero), equals(box2.localToGlobal(Offset.zero)));
|
||||||
expect(box1.size.height, equals(box2.size.height));
|
expect(box1.size.height, equals(box2.size.height));
|
||||||
@ -257,48 +334,6 @@ void main() {
|
|||||||
expect(value, equals('two'));
|
expect(value, equals('two'));
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('Dropdown form field', (WidgetTester tester) async {
|
|
||||||
String value = 'one';
|
|
||||||
|
|
||||||
await tester.pumpWidget(
|
|
||||||
StatefulBuilder(
|
|
||||||
builder: (BuildContext context, StateSetter setState) {
|
|
||||||
return MaterialApp(
|
|
||||||
home: Material(
|
|
||||||
child: DropdownButtonFormField<String>(
|
|
||||||
value: value,
|
|
||||||
hint: const Text('Select Value'),
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
prefixIcon: Icon(Icons.fastfood)
|
|
||||||
),
|
|
||||||
items: menuItems.map((String val) {
|
|
||||||
return DropdownMenuItem<String>(
|
|
||||||
value: val,
|
|
||||||
child: Text(val),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
onChanged: (String v) {
|
|
||||||
setState(() {
|
|
||||||
value = v;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
validator: (String v) => v == null ? 'Must select value' : null,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(value, equals('one'));
|
|
||||||
await tester.tap(find.text('one'));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
await tester.tap(find.text('three').last);
|
|
||||||
await tester.pump();
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expect(value, equals('three'));
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('Dropdown in ListView', (WidgetTester tester) async {
|
testWidgets('Dropdown in ListView', (WidgetTester tester) async {
|
||||||
// Regression test for https://github.com/flutter/flutter/issues/12053
|
// Regression test for https://github.com/flutter/flutter/issues/12053
|
||||||
// Positions a DropdownButton at the left and right edges of the screen,
|
// Positions a DropdownButton at the left and right edges of the screen,
|
||||||
@ -1281,4 +1316,329 @@ void main() {
|
|||||||
await tester.pumpWidget(buildFrame(buttonKey: buttonKey, value: 'two', onChanged: onChanged));
|
await tester.pumpWidget(buildFrame(buttonKey: buttonKey, value: 'two', onChanged: onChanged));
|
||||||
expect(tester.widget<DecoratedBox>(decoratedBox).decoration, defaultDecoration);
|
expect(tester.widget<DecoratedBox>(decoratedBox).decoration, defaultDecoration);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('Dropdown form field with autovalidation test', (WidgetTester tester) async {
|
||||||
|
String value = 'one';
|
||||||
|
int _validateCalled = 0;
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
StatefulBuilder(
|
||||||
|
builder: (BuildContext context, StateSetter setState) {
|
||||||
|
return MaterialApp(
|
||||||
|
home: Material(
|
||||||
|
child: DropdownButtonFormField<String>(
|
||||||
|
value: value,
|
||||||
|
hint: const Text('Select Value'),
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
prefixIcon: Icon(Icons.fastfood)
|
||||||
|
),
|
||||||
|
items: menuItems.map((String value) {
|
||||||
|
return DropdownMenuItem<String>(
|
||||||
|
value: value,
|
||||||
|
child: Text(value),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
onChanged: (String newValue) {
|
||||||
|
setState(() {
|
||||||
|
value = newValue;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
validator: (String currentValue) {
|
||||||
|
_validateCalled++;
|
||||||
|
return currentValue == null ? 'Must select value' : null;
|
||||||
|
},
|
||||||
|
autovalidate: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(_validateCalled, 1);
|
||||||
|
expect(value, equals('one'));
|
||||||
|
await tester.tap(find.text('one'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.text('three').last);
|
||||||
|
await tester.pump();
|
||||||
|
expect(_validateCalled, 2);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(value, equals('three'));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Arrow icon aligns with the edge of button in form field when expanded', (WidgetTester tester) async {
|
||||||
|
final Key buttonKey = UniqueKey();
|
||||||
|
|
||||||
|
// There shouldn't be overflow when expanded although list contains longer items.
|
||||||
|
final List<String> items = <String>[
|
||||||
|
'1234567890',
|
||||||
|
'abcdefghijklmnopqrstuvwxyz1234567890abcdefghijklmnopqrstuvwxyz1234567890',
|
||||||
|
];
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
buildFormFrame(
|
||||||
|
buttonKey: buttonKey,
|
||||||
|
value: '1234567890',
|
||||||
|
isExpanded: true,
|
||||||
|
onChanged: onChanged,
|
||||||
|
items: items,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final RenderBox buttonBox = tester.renderObject<RenderBox>(
|
||||||
|
find.byKey(buttonKey),
|
||||||
|
);
|
||||||
|
expect(buttonBox.attached, isTrue);
|
||||||
|
|
||||||
|
final RenderBox arrowIcon = tester.renderObject<RenderBox>(
|
||||||
|
find.byIcon(Icons.arrow_drop_down),
|
||||||
|
);
|
||||||
|
expect(arrowIcon.attached, isTrue);
|
||||||
|
|
||||||
|
// Arrow icon should be aligned with far right of button when expanded
|
||||||
|
expect(
|
||||||
|
arrowIcon.localToGlobal(Offset.zero).dx,
|
||||||
|
buttonBox.size.centerRight(Offset(-arrowIcon.size.width, 0.0)).dx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Dropdown button form field with isDense:true aligns selected menu item', (WidgetTester tester) async {
|
||||||
|
final Key buttonKey = UniqueKey();
|
||||||
|
const String value = 'two';
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
buildFormFrame(
|
||||||
|
buttonKey: buttonKey,
|
||||||
|
value: value,
|
||||||
|
isDense: true,
|
||||||
|
onChanged: onChanged,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final RenderBox buttonBox = tester.renderObject<RenderBox>(
|
||||||
|
find.byKey(buttonKey),
|
||||||
|
);
|
||||||
|
expect(buttonBox.attached, isTrue);
|
||||||
|
|
||||||
|
await tester.tap(find.text('two'));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(seconds: 1)); // finish the menu animation
|
||||||
|
|
||||||
|
// The selected dropdown item is both in menu we just popped up, and in
|
||||||
|
// the IndexedStack contained by the dropdown button. Both of them should
|
||||||
|
// have the same vertical center as the button.
|
||||||
|
final List<RenderBox> itemBoxes = tester.renderObjectList<RenderBox>(
|
||||||
|
find.byKey(const ValueKey<String>('two')),
|
||||||
|
).toList();
|
||||||
|
expect(itemBoxes.length, equals(2));
|
||||||
|
|
||||||
|
// When isDense is true, the button's height is reduced. The menu items'
|
||||||
|
// heights are not.
|
||||||
|
final List<double> itemBoxesHeight = itemBoxes.map<double>((RenderBox box) => box.size.height).toList();
|
||||||
|
final double menuItemHeight = itemBoxesHeight.reduce(math.max);
|
||||||
|
expect(menuItemHeight, greaterThanOrEqualTo(buttonBox.size.height));
|
||||||
|
|
||||||
|
for (RenderBox itemBox in itemBoxes) {
|
||||||
|
expect(itemBox.attached, isTrue);
|
||||||
|
final Offset buttonBoxCenter = buttonBox.size.center(buttonBox.localToGlobal(Offset.zero));
|
||||||
|
final Offset itemBoxCenter = itemBox.size.center(itemBox.localToGlobal(Offset.zero));
|
||||||
|
expect(buttonBoxCenter.dy, equals(itemBoxCenter.dy));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Dropdown button form field - custom text style', (WidgetTester tester) async {
|
||||||
|
const String value = 'foo';
|
||||||
|
final UniqueKey itemKey = UniqueKey();
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
TestApp(
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
child: Material(
|
||||||
|
child: DropdownButtonFormField<String>(
|
||||||
|
value: value,
|
||||||
|
items: <DropdownMenuItem<String>>[
|
||||||
|
DropdownMenuItem<String>(
|
||||||
|
key: itemKey,
|
||||||
|
value: 'foo',
|
||||||
|
child: const Text(value),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
isDense: true,
|
||||||
|
onChanged: (_) { },
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.amber,
|
||||||
|
fontSize: 20.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final RichText richText = tester.widget<RichText>(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byKey(itemKey),
|
||||||
|
matching: find.byType(RichText),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(richText.text.style.color, Colors.amber);
|
||||||
|
expect(richText.text.style.fontSize, 20.0);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Dropdown form field - disabledHint displays when the items list is empty, when items is null', (WidgetTester tester) async {
|
||||||
|
final Key buttonKey = UniqueKey();
|
||||||
|
|
||||||
|
Widget build({ List<String> items }){
|
||||||
|
return buildFormFrame(
|
||||||
|
items: items,
|
||||||
|
buttonKey: buttonKey,
|
||||||
|
value: null,
|
||||||
|
hint: const Text('enabled'),
|
||||||
|
disabledHint: const Text('disabled'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// [disabledHint] should display when [items] is null
|
||||||
|
await tester.pumpWidget(build(items: null));
|
||||||
|
expect(find.text('enabled'), findsNothing);
|
||||||
|
expect(find.text('disabled'), findsOneWidget);
|
||||||
|
|
||||||
|
// [disabledHint] should display when [items] is an empty list.
|
||||||
|
await tester.pumpWidget(build(items: <String>[]));
|
||||||
|
expect(find.text('enabled'), findsNothing);
|
||||||
|
expect(find.text('disabled'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Dropdown form field - disabledHint displays when onChanged is null', (WidgetTester tester) async {
|
||||||
|
final Key buttonKey = UniqueKey();
|
||||||
|
|
||||||
|
Widget build({ List<String> items, ValueChanged<String> onChanged }){
|
||||||
|
return buildFormFrame(
|
||||||
|
items: items,
|
||||||
|
buttonKey: buttonKey,
|
||||||
|
value: null,
|
||||||
|
onChanged: onChanged,
|
||||||
|
hint: const Text('enabled'),
|
||||||
|
disabledHint: const Text('disabled'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await tester.pumpWidget(build(items: menuItems, onChanged: null));
|
||||||
|
expect(find.text('enabled'), findsNothing);
|
||||||
|
expect(find.text('disabled'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Dropdown form field - disabled hint should be of same size as enabled hint', (WidgetTester tester) async {
|
||||||
|
final Key buttonKey = UniqueKey();
|
||||||
|
|
||||||
|
Widget build({ List<String> items}){
|
||||||
|
return buildFormFrame(
|
||||||
|
items: items,
|
||||||
|
buttonKey: buttonKey,
|
||||||
|
value: null,
|
||||||
|
hint: const Text('enabled'),
|
||||||
|
disabledHint: const Text('disabled'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await tester.pumpWidget(build(items: null));
|
||||||
|
final RenderBox disabledHintBox = tester.renderObject<RenderBox>(
|
||||||
|
find.byKey(buttonKey),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(build(items: menuItems));
|
||||||
|
final RenderBox enabledHintBox = tester.renderObject<RenderBox>(
|
||||||
|
find.byKey(buttonKey),
|
||||||
|
);
|
||||||
|
expect(enabledHintBox.localToGlobal(Offset.zero), equals(disabledHintBox.localToGlobal(Offset.zero)));
|
||||||
|
expect(enabledHintBox.size, equals(disabledHintBox.size));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Dropdown form field - Custom icon size and colors', (WidgetTester tester) async {
|
||||||
|
final Key iconKey = UniqueKey();
|
||||||
|
final Icon customIcon = Icon(Icons.assessment, key: iconKey);
|
||||||
|
|
||||||
|
await tester.pumpWidget(buildFormFrame(
|
||||||
|
icon: customIcon,
|
||||||
|
iconSize: 30.0,
|
||||||
|
iconEnabledColor: Colors.pink,
|
||||||
|
iconDisabledColor: Colors.orange,
|
||||||
|
onChanged: onChanged,
|
||||||
|
));
|
||||||
|
|
||||||
|
// test for size
|
||||||
|
final RenderBox icon = tester.renderObject(find.byKey(iconKey));
|
||||||
|
expect(icon.size, const Size(30.0, 30.0));
|
||||||
|
|
||||||
|
// test for enabled color
|
||||||
|
final RichText enabledRichText = tester.widget<RichText>(_iconRichText(iconKey));
|
||||||
|
expect(enabledRichText.text.style.color, Colors.pink);
|
||||||
|
|
||||||
|
// test for disabled color
|
||||||
|
await tester.pumpWidget(buildFormFrame(
|
||||||
|
icon: customIcon,
|
||||||
|
iconSize: 30.0,
|
||||||
|
iconEnabledColor: Colors.pink,
|
||||||
|
iconDisabledColor: Colors.orange,
|
||||||
|
items: null,
|
||||||
|
));
|
||||||
|
|
||||||
|
final RichText disabledRichText = tester.widget<RichText>(_iconRichText(iconKey));
|
||||||
|
expect(disabledRichText.text.style.color, Colors.orange);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Dropdown form field - default elevation', (WidgetTester tester) async {
|
||||||
|
final Key buttonKey = UniqueKey();
|
||||||
|
debugDisableShadows = false;
|
||||||
|
await tester.pumpWidget(buildFormFrame(
|
||||||
|
buttonKey: buttonKey,
|
||||||
|
items: menuItems,
|
||||||
|
onChanged: onChanged,
|
||||||
|
));
|
||||||
|
await tester.tap(find.byKey(buttonKey));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final Finder customPaint = find.ancestor(
|
||||||
|
of: find.text('one').last,
|
||||||
|
matching: find.byType(CustomPaint),
|
||||||
|
).last;
|
||||||
|
|
||||||
|
// Verifying whether or not default elevation(i.e. 8) paints desired shadow
|
||||||
|
verifyPaintedShadow(customPaint, 8);
|
||||||
|
debugDisableShadows = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Dropdown form field - custom elevation', (WidgetTester tester) async {
|
||||||
|
debugDisableShadows = false;
|
||||||
|
final Key buttonKeyOne = UniqueKey();
|
||||||
|
final Key buttonKeyTwo = UniqueKey();
|
||||||
|
|
||||||
|
await tester.pumpWidget(buildFormFrame(
|
||||||
|
buttonKey: buttonKeyOne,
|
||||||
|
items: menuItems,
|
||||||
|
elevation: 16,
|
||||||
|
onChanged: onChanged,
|
||||||
|
));
|
||||||
|
await tester.tap(find.byKey(buttonKeyOne));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final Finder customPaintOne = find.ancestor(
|
||||||
|
of: find.text('one').last,
|
||||||
|
matching: find.byType(CustomPaint),
|
||||||
|
).last;
|
||||||
|
|
||||||
|
verifyPaintedShadow(customPaintOne, 16);
|
||||||
|
await tester.tap(find.text('one').last);
|
||||||
|
await tester.pumpWidget(buildFormFrame(
|
||||||
|
buttonKey: buttonKeyTwo,
|
||||||
|
items: menuItems,
|
||||||
|
elevation: 24,
|
||||||
|
onChanged: onChanged,
|
||||||
|
));
|
||||||
|
await tester.tap(find.byKey(buttonKeyTwo));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final Finder customPaintTwo = find.ancestor(
|
||||||
|
of: find.text('one').last,
|
||||||
|
matching: find.byType(CustomPaint),
|
||||||
|
).last;
|
||||||
|
|
||||||
|
verifyPaintedShadow(customPaintTwo, 24);
|
||||||
|
debugDisableShadows = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user