Unverified Commit 8ee26efb authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Wire in focusNode, focusColor, autofocus, and dropdownColor to DropdownButtonFormField (#54706)

parent 45c52508
...@@ -799,6 +799,8 @@ class DropdownButton<T> extends StatefulWidget { ...@@ -799,6 +799,8 @@ class DropdownButton<T> extends StatefulWidget {
/// defaults, so do not need to be specified). The boolean [isDense] and /// defaults, so do not need to be specified). The boolean [isDense] and
/// [isExpanded] arguments must not be null. /// [isExpanded] arguments must not be null.
/// ///
/// The [autofocus] argument must not be null.
///
/// The [dropdownColor] argument specifies the background color of the /// The [dropdownColor] argument specifies the background color of the
/// dropdown when it is open. If it is null, the current theme's /// dropdown when it is open. If it is null, the current theme's
/// [ThemeData.canvasColor] will be used instead. /// [ThemeData.canvasColor] will be used instead.
...@@ -825,6 +827,8 @@ class DropdownButton<T> extends StatefulWidget { ...@@ -825,6 +827,8 @@ class DropdownButton<T> extends StatefulWidget {
this.focusNode, this.focusNode,
this.autofocus = false, this.autofocus = false,
this.dropdownColor, this.dropdownColor,
// When adding new arguments, consider adding similar arguments to
// DropdownButtonFormField.
}) : assert(items == null || items.isEmpty || value == null || }) : assert(items == null || items.isEmpty || value == null ||
items.where((DropdownMenuItem<T> item) { items.where((DropdownMenuItem<T> item) {
return item.value == value; return item.value == value;
...@@ -1426,25 +1430,26 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi ...@@ -1426,25 +1430,26 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi
} }
} }
/// A convenience widget that wraps a [DropdownButton] in a [FormField]. /// A convenience widget that makes a [DropdownButton] into a [FormField].
class DropdownButtonFormField<T> extends FormField<T> { class DropdownButtonFormField<T> extends FormField<T> {
/// Creates a [DropdownButton] widget wrapped in an [InputDecorator] and /// Creates a [DropdownButton] widget that is a [FormField], wrapped in an
/// [FormField]. /// [InputDecorator].
///
/// For a description of the `onSaved`, `validator`, or `autovalidate`
/// parameters, see [FormField]. For the rest (other than [decoration]), see
/// [DropdownButton].
/// ///
/// The [DropdownButton] [items] parameters must not be null. /// The `items`, `elevation`, `iconSize`, `isDense`, `isExpanded`,
/// `autofocus`, and `decoration` parameters must not be null.
DropdownButtonFormField({ DropdownButtonFormField({
Key key, Key key,
T value,
@required List<DropdownMenuItem<T>> items, @required List<DropdownMenuItem<T>> items,
DropdownButtonBuilder selectedItemBuilder, DropdownButtonBuilder selectedItemBuilder,
T value,
Widget hint, Widget hint,
Widget disabledHint,
@required this.onChanged, @required this.onChanged,
VoidCallback onTap, VoidCallback onTap,
this.decoration = const InputDecoration(),
FormFieldSetter<T> onSaved,
FormFieldValidator<T> validator,
bool autovalidate = false,
Widget disabledHint,
int elevation = 8, int elevation = 8,
TextStyle style, TextStyle style,
Widget icon, Widget icon,
...@@ -1454,6 +1459,14 @@ class DropdownButtonFormField<T> extends FormField<T> { ...@@ -1454,6 +1459,14 @@ class DropdownButtonFormField<T> extends FormField<T> {
bool isDense = true, bool isDense = true,
bool isExpanded = false, bool isExpanded = false,
double itemHeight, double itemHeight,
Color focusColor,
FocusNode focusNode,
bool autofocus = false,
Color dropdownColor,
InputDecoration decoration,
FormFieldSetter<T> onSaved,
FormFieldValidator<T> validator,
bool autovalidate = false,
}) : assert(items == null || items.isEmpty || value == null || }) : assert(items == null || items.isEmpty || value == null ||
items.where((DropdownMenuItem<T> item) { items.where((DropdownMenuItem<T> item) {
return item.value == value; return item.value == value;
...@@ -1463,12 +1476,13 @@ class DropdownButtonFormField<T> extends FormField<T> { ...@@ -1463,12 +1476,13 @@ class DropdownButtonFormField<T> extends FormField<T> {
'Either zero or 2 or more [DropdownMenuItem]s were detected ' 'Either zero or 2 or more [DropdownMenuItem]s were detected '
'with the same value', 'with the same value',
), ),
assert(decoration != null),
assert(elevation != null), assert(elevation != null),
assert(iconSize != null), assert(iconSize != null),
assert(isDense != null), assert(isDense != null),
assert(isExpanded != null), assert(isExpanded != null),
assert(itemHeight == null || itemHeight > 0), assert(itemHeight == null || itemHeight >= kMinInteractiveDimension),
assert(autofocus != null),
decoration = decoration ?? InputDecoration(focusColor: focusColor),
super( super(
key: key, key: key,
onSaved: onSaved, onSaved: onSaved,
...@@ -1477,21 +1491,29 @@ class DropdownButtonFormField<T> extends FormField<T> { ...@@ -1477,21 +1491,29 @@ class DropdownButtonFormField<T> extends FormField<T> {
autovalidate: autovalidate, autovalidate: autovalidate,
builder: (FormFieldState<T> field) { builder: (FormFieldState<T> field) {
final _DropdownButtonFormFieldState<T> state = field as _DropdownButtonFormFieldState<T>; final _DropdownButtonFormFieldState<T> state = field as _DropdownButtonFormFieldState<T>;
final InputDecoration effectiveDecoration = decoration.applyDefaults( final InputDecoration decorationArg = decoration ?? InputDecoration(focusColor: focusColor);
final InputDecoration effectiveDecoration = decorationArg.applyDefaults(
Theme.of(field.context).inputDecorationTheme, Theme.of(field.context).inputDecorationTheme,
); );
// An unfocusable Focus widget so that this widget can detect if its
// descendants have focus or not.
return Focus(
canRequestFocus: false,
skipTraversal: true,
child: Builder(builder: (BuildContext context) {
return InputDecorator( return InputDecorator(
decoration: effectiveDecoration.copyWith(errorText: field.errorText), decoration: effectiveDecoration.copyWith(errorText: field.errorText),
isEmpty: state.value == null, isEmpty: state.value == null,
isFocused: Focus.of(context).hasFocus,
child: DropdownButtonHideUnderline( child: DropdownButtonHideUnderline(
child: DropdownButton<T>( child: DropdownButton<T>(
value: state.value,
items: items, items: items,
selectedItemBuilder: selectedItemBuilder, selectedItemBuilder: selectedItemBuilder,
value: state.value,
hint: hint, hint: hint,
disabledHint: disabledHint,
onChanged: onChanged == null ? null : state.didChange, onChanged: onChanged == null ? null : state.didChange,
onTap: onTap, onTap: onTap,
disabledHint: disabledHint,
elevation: elevation, elevation: elevation,
style: style, style: style,
icon: icon, icon: icon,
...@@ -1501,9 +1523,15 @@ class DropdownButtonFormField<T> extends FormField<T> { ...@@ -1501,9 +1523,15 @@ class DropdownButtonFormField<T> extends FormField<T> {
isDense: isDense, isDense: isDense,
isExpanded: isExpanded, isExpanded: isExpanded,
itemHeight: itemHeight, itemHeight: itemHeight,
focusColor: focusColor,
focusNode: focusNode,
autofocus: autofocus,
dropdownColor: dropdownColor,
), ),
), ),
); );
}),
);
}, },
); );
...@@ -1512,11 +1540,11 @@ class DropdownButtonFormField<T> extends FormField<T> { ...@@ -1512,11 +1540,11 @@ class DropdownButtonFormField<T> extends FormField<T> {
/// The decoration to show around the dropdown button form field. /// The decoration to show around the dropdown button form field.
/// ///
/// By default, draws a horizontal line under the dropdown button field but can be /// By default, draws a horizontal line under the dropdown button field but
/// configured to show an icon, label, hint text, and error text. /// can be configured to show an icon, label, hint text, and error text.
/// ///
/// Specify null to remove the decoration entirely (including the /// If not specified, an [InputDecorator] with the `focusColor` set to the
/// extra padding introduced by the decoration to save space for the labels). /// supplied `focusColor` (if any) will be used.
final InputDecoration decoration; final InputDecoration decoration;
@override @override
......
...@@ -30,6 +30,92 @@ Finder _iconRichText(Key iconKey) { ...@@ -30,6 +30,92 @@ Finder _iconRichText(Key iconKey) {
); );
} }
Widget buildDropdown({
bool isFormField,
Key buttonKey,
String value = 'two',
ValueChanged<String> onChanged,
VoidCallback onTap,
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,
List<Widget> Function(BuildContext) selectedItemBuilder,
double itemHeight = kMinInteractiveDimension,
Alignment alignment = Alignment.center,
TextDirection textDirection = TextDirection.ltr,
Size mediaSize,
FocusNode focusNode,
bool autofocus = false,
Color focusColor,
Color dropdownColor,
}) {
final List<DropdownMenuItem<String>> listItems = 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();
if (isFormField) {
return Form(
child: DropdownButtonFormField<String>(
key: buttonKey,
value: value,
hint: hint,
disabledHint: disabledHint,
onChanged: onChanged,
onTap: onTap,
icon: icon,
iconSize: iconSize,
iconDisabledColor: iconDisabledColor,
iconEnabledColor: iconEnabledColor,
isDense: isDense,
isExpanded: isExpanded,
// No underline attribute
focusNode: focusNode,
autofocus: autofocus,
focusColor: focusColor,
dropdownColor: dropdownColor,
items: listItems,
selectedItemBuilder: selectedItemBuilder,
itemHeight: itemHeight,
),
);
}
return DropdownButton<String>(
key: buttonKey,
value: value,
hint: hint,
disabledHint: disabledHint,
onChanged: onChanged,
onTap: onTap,
icon: icon,
iconSize: iconSize,
iconDisabledColor: iconDisabledColor,
iconEnabledColor: iconEnabledColor,
isDense: isDense,
isExpanded: isExpanded,
underline: underline,
focusNode: focusNode,
autofocus: autofocus,
focusColor: focusColor,
dropdownColor: dropdownColor,
items: listItems,
selectedItemBuilder: selectedItemBuilder,
itemHeight: itemHeight,
);
}
Widget buildFrame({ Widget buildFrame({
Key buttonKey, Key buttonKey,
String value = 'two', String value = 'two',
...@@ -54,6 +140,7 @@ Widget buildFrame({ ...@@ -54,6 +140,7 @@ Widget buildFrame({
bool autofocus = false, bool autofocus = false,
Color focusColor, Color focusColor,
Color dropdownColor, Color dropdownColor,
bool isFormField = false,
}) { }) {
return TestApp( return TestApp(
textDirection: textDirection, textDirection: textDirection,
...@@ -62,8 +149,9 @@ Widget buildFrame({ ...@@ -62,8 +149,9 @@ Widget buildFrame({
child: Align( child: Align(
alignment: alignment, alignment: alignment,
child: RepaintBoundary( child: RepaintBoundary(
child: DropdownButton<String>( child: buildDropdown(
key: buttonKey, isFormField: isFormField,
buttonKey: buttonKey,
value: value, value: value,
hint: hint, hint: hint,
disabledHint: disabledHint, disabledHint: disabledHint,
...@@ -80,16 +168,9 @@ Widget buildFrame({ ...@@ -80,16 +168,9 @@ Widget buildFrame({
autofocus: autofocus, autofocus: autofocus,
focusColor: focusColor, focusColor: focusColor,
dropdownColor: dropdownColor, dropdownColor: dropdownColor,
items: items == null ? null : items.map<DropdownMenuItem<String>>((String item) { items: items,
return DropdownMenuItem<String>(
key: ValueKey<String>(item),
value: item,
child: Text(item, key: ValueKey<String>(item + 'Text')),
);
}).toList(),
selectedItemBuilder: selectedItemBuilder, selectedItemBuilder: selectedItemBuilder,
itemHeight: itemHeight, itemHeight: itemHeight,),
),
), ),
), ),
), ),
...@@ -176,12 +257,14 @@ void verifyPaintedShadow(Finder customPaint, int elevation) { ...@@ -176,12 +257,14 @@ void verifyPaintedShadow(Finder customPaint, int elevation) {
); );
} }
Future<void> checkDropdownColor(WidgetTester tester, {Color color}) async { Future<void> checkDropdownColor(WidgetTester tester, {Color color, bool isFormField = false }) async {
const String text = 'foo'; const String text = 'foo';
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
home: Material( home: Material(
child: DropdownButton<String>( child: isFormField
? Form(
child: DropdownButtonFormField<String>(
dropdownColor: color, dropdownColor: color,
value: text, value: text,
items: const <DropdownMenuItem<String>>[ items: const <DropdownMenuItem<String>>[
...@@ -190,7 +273,19 @@ Future<void> checkDropdownColor(WidgetTester tester, {Color color}) async { ...@@ -190,7 +273,19 @@ Future<void> checkDropdownColor(WidgetTester tester, {Color color}) async {
child: Text(text), child: Text(text),
), ),
], ],
onChanged: (_) { }, onChanged: (_) {},
),
)
: DropdownButton<String>(
dropdownColor: color,
value: text,
items: const <DropdownMenuItem<String>>[
DropdownMenuItem<String>(
value: text,
child: Text(text),
),
],
onChanged: (_) {},
), ),
), ),
), ),
...@@ -1815,10 +1910,14 @@ void main() { ...@@ -1815,10 +1910,14 @@ void main() {
await checkDropdownColor(tester); await checkDropdownColor(tester);
}); });
testWidgets('DropdownButton uses dropdownColor when expanded when given', (WidgetTester tester) async { testWidgets('DropdownButton uses dropdownColor when expanded', (WidgetTester tester) async {
await checkDropdownColor(tester, color: const Color.fromRGBO(120, 220, 70, 0.8)); await checkDropdownColor(tester, color: const Color.fromRGBO(120, 220, 70, 0.8));
}); });
testWidgets('DropdownButtonFormField uses dropdownColor when expanded', (WidgetTester tester) async {
await checkDropdownColor(tester, color: const Color.fromRGBO(120, 220, 70, 0.8), isFormField: true);
});
testWidgets('DropdownButton hint displays properly when selectedItemBuilder is defined', (WidgetTester tester) async { testWidgets('DropdownButton hint displays properly when selectedItemBuilder is defined', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/42340 // Regression test for https://github.com/flutter/flutter/issues/42340
final List<String> items = <String>['1', '2', '3']; final List<String> items = <String>['1', '2', '3'];
...@@ -2100,6 +2199,20 @@ void main() { ...@@ -2100,6 +2199,20 @@ void main() {
expect(buttonFinder, paints ..rrect(rrect: const RRect.fromLTRBXY(0.0, 0.0, 104.0, 48.0, 4.0, 4.0), color: const Color(0xff00ff00))); expect(buttonFinder, paints ..rrect(rrect: const RRect.fromLTRBXY(0.0, 0.0, 104.0, 48.0, 4.0, 4.0), color: const Color(0xff00ff00)));
}); });
testWidgets('DropdownButtonFormField can be focused, and has focusColor', (WidgetTester tester) async {
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
final UniqueKey buttonKey = UniqueKey();
final FocusNode focusNode = FocusNode(debugLabel: 'DropdownButtonFormField');
await tester.pumpWidget(buildFrame(isFormField: true, buttonKey: buttonKey, onChanged: onChanged, focusNode: focusNode, autofocus: true));
await tester.pump(); // Pump a frame for autofocus to take effect.
expect(focusNode.hasPrimaryFocus, isTrue);
final Finder buttonFinder = find.descendant(of: find.byKey(buttonKey), matching: find.byType(InputDecorator));
expect(buttonFinder, paints ..rrect(rrect: const RRect.fromLTRBXY(0.0, 12.0, 800.0, 60.0, 4.0, 4.0), color: const Color(0x1f000000)));
await tester.pumpWidget(buildFrame(isFormField: true, buttonKey: buttonKey, onChanged: onChanged, focusNode: focusNode, focusColor: const Color(0xff00ff00)));
expect(buttonFinder, paints ..rrect(rrect: const RRect.fromLTRBXY(0.0, 12.0, 800.0, 60.0, 4.0, 4.0), color: const Color(0xff00ff00)));
});
testWidgets("DropdownButton won't be focused if not enabled", (WidgetTester tester) async { testWidgets("DropdownButton won't be focused if not enabled", (WidgetTester tester) async {
final UniqueKey buttonKey = UniqueKey(); final UniqueKey buttonKey = UniqueKey();
final FocusNode focusNode = FocusNode(debugLabel: 'DropdownButton'); final FocusNode focusNode = FocusNode(debugLabel: 'DropdownButton');
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment