mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Add DropdownMenuFormField (#163721)
## Description This PR introduces DropdownMenuFormField. ## Related Issue Fixes [Create DropdownMenuFormField](https://github.com/flutter/flutter/issues/141941) Fixes [Add validator to DropdownMenu](https://github.com/flutter/flutter/issues/152131) ## Tests Adds 41 tests.
This commit is contained in:
parent
6ce1251e75
commit
183b9ea27e
@ -78,6 +78,7 @@ export 'src/material/drawer_header.dart';
|
||||
export 'src/material/drawer_theme.dart';
|
||||
export 'src/material/dropdown.dart';
|
||||
export 'src/material/dropdown_menu.dart';
|
||||
export 'src/material/dropdown_menu_form_field.dart';
|
||||
export 'src/material/dropdown_menu_theme.dart';
|
||||
export 'src/material/elevated_button.dart';
|
||||
export 'src/material/elevated_button_theme.dart';
|
||||
|
||||
235
packages/flutter/lib/src/material/dropdown_menu_form_field.dart
Normal file
235
packages/flutter/lib/src/material/dropdown_menu_form_field.dart
Normal file
@ -0,0 +1,235 @@
|
||||
// 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.
|
||||
|
||||
/// @docImport 'text_theme.dart';
|
||||
library;
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'dropdown_menu.dart';
|
||||
import 'input_decorator.dart';
|
||||
import 'menu_style.dart';
|
||||
|
||||
/// A [FormField] that contains a [DropdownMenu].
|
||||
///
|
||||
/// This is a convenience widget that wraps a [DropdownMenu] widget in a
|
||||
/// [FormField].
|
||||
///
|
||||
/// A [Form] ancestor is not required. The [Form] allows one to
|
||||
/// save, reset, or validate multiple fields at once. To use without a [Form],
|
||||
/// pass a [GlobalKey] to the constructor and use [GlobalKey.currentState] to
|
||||
/// save or reset the form field.
|
||||
///
|
||||
/// The `value` parameter maps to [FormField.initialValue].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [DropdownMenu], which is the underlying text field without the [Form]
|
||||
/// integration.
|
||||
class DropdownMenuFormField<T> extends FormField<T> {
|
||||
/// Creates a [DropdownMenu] widget that is a [FormField].
|
||||
///
|
||||
/// For a description of the `onSaved`, `validator`, or `autovalidateMode`
|
||||
/// parameters, see [FormField]. For the rest, see [DropdownMenu].
|
||||
DropdownMenuFormField({
|
||||
super.key,
|
||||
bool enabled = true,
|
||||
double? width,
|
||||
double? menuHeight,
|
||||
Widget? leadingIcon,
|
||||
Widget? trailingIcon,
|
||||
Widget? label,
|
||||
String? hintText,
|
||||
String? helperText,
|
||||
Widget? selectedTrailingIcon,
|
||||
bool enableFilter = false,
|
||||
bool enableSearch = true,
|
||||
TextInputType? keyboardType,
|
||||
TextStyle? textStyle,
|
||||
TextAlign textAlign = TextAlign.start,
|
||||
InputDecorationTheme? inputDecorationTheme,
|
||||
MenuStyle? menuStyle,
|
||||
this.controller,
|
||||
T? initialSelection,
|
||||
this.onSelected,
|
||||
FocusNode? focusNode,
|
||||
bool? requestFocusOnTap,
|
||||
EdgeInsetsGeometry? expandedInsets,
|
||||
Offset? alignmentOffset,
|
||||
FilterCallback<T>? filterCallback,
|
||||
SearchCallback<T>? searchCallback,
|
||||
required this.dropdownMenuEntries,
|
||||
List<TextInputFormatter>? inputFormatters,
|
||||
DropdownMenuCloseBehavior closeBehavior = DropdownMenuCloseBehavior.all,
|
||||
int maxLines = 1,
|
||||
TextInputAction? textInputAction,
|
||||
super.restorationId,
|
||||
super.onSaved,
|
||||
AutovalidateMode autovalidateMode = AutovalidateMode.disabled,
|
||||
super.validator,
|
||||
super.forceErrorText,
|
||||
}) : super(
|
||||
initialValue: initialSelection,
|
||||
autovalidateMode: autovalidateMode,
|
||||
builder: (FormFieldState<T> field) {
|
||||
final _DropdownMenuFormFieldState<T> state = field as _DropdownMenuFormFieldState<T>;
|
||||
void onSelectedHandler(T? value) {
|
||||
field.didChange(value);
|
||||
onSelected?.call(value);
|
||||
}
|
||||
|
||||
return UnmanagedRestorationScope(
|
||||
bucket: field.bucket,
|
||||
child: DropdownMenu<T>(
|
||||
restorationId: restorationId,
|
||||
enabled: enabled,
|
||||
width: width,
|
||||
menuHeight: menuHeight,
|
||||
leadingIcon: leadingIcon,
|
||||
trailingIcon: trailingIcon,
|
||||
label: label,
|
||||
hintText: hintText,
|
||||
helperText: helperText,
|
||||
errorText: state.errorText,
|
||||
selectedTrailingIcon: selectedTrailingIcon,
|
||||
enableFilter: enableFilter,
|
||||
enableSearch: enableSearch,
|
||||
keyboardType: keyboardType,
|
||||
textStyle: textStyle,
|
||||
textAlign: textAlign,
|
||||
inputDecorationTheme: inputDecorationTheme,
|
||||
menuStyle: menuStyle,
|
||||
controller: controller,
|
||||
initialSelection: state.value,
|
||||
onSelected: onSelectedHandler,
|
||||
focusNode: focusNode,
|
||||
requestFocusOnTap: requestFocusOnTap,
|
||||
expandedInsets: expandedInsets,
|
||||
alignmentOffset: alignmentOffset,
|
||||
filterCallback: filterCallback,
|
||||
searchCallback: searchCallback,
|
||||
inputFormatters: inputFormatters,
|
||||
closeBehavior: closeBehavior,
|
||||
dropdownMenuEntries: dropdownMenuEntries,
|
||||
maxLines: maxLines,
|
||||
textInputAction: textInputAction,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
/// The callback is called when a selection is made.
|
||||
///
|
||||
/// Defaults to null. If null, only the text field is updated.
|
||||
final ValueChanged<T?>? onSelected;
|
||||
|
||||
/// Controls the text being edited.
|
||||
///
|
||||
/// If null, this widget will create its own [TextEditingController].
|
||||
final TextEditingController? controller;
|
||||
|
||||
/// Descriptions of the menu items in the [DropdownMenuFormField].
|
||||
///
|
||||
/// 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<T>> dropdownMenuEntries;
|
||||
|
||||
@override
|
||||
FormFieldState<T> createState() => _DropdownMenuFormFieldState<T>();
|
||||
}
|
||||
|
||||
class _DropdownMenuFormFieldState<T> extends FormFieldState<T> {
|
||||
DropdownMenuFormField<T> get _dropdownMenuFormField => widget as DropdownMenuFormField<T>;
|
||||
|
||||
RestorableTextEditingController? _restorableController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_createRestorableController(widget.initialValue);
|
||||
}
|
||||
|
||||
void _createRestorableController(T? initialValue) {
|
||||
assert(_restorableController == null);
|
||||
_restorableController = RestorableTextEditingController.fromValue(
|
||||
TextEditingValue(text: _findLabelByValue(initialValue)),
|
||||
);
|
||||
if (!restorePending) {
|
||||
_registerRestorableController();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(DropdownMenuFormField<T> oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.initialValue != widget.initialValue && !hasInteractedByUser) {
|
||||
setValue(widget.initialValue);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_restorableController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChange(T? value) {
|
||||
super.didChange(value);
|
||||
_dropdownMenuFormField.onSelected?.call(value);
|
||||
_updateRestorableController(value);
|
||||
}
|
||||
|
||||
@override
|
||||
void reset() {
|
||||
super.reset();
|
||||
_dropdownMenuFormField.onSelected?.call(value);
|
||||
_updateRestorableController(widget.initialValue);
|
||||
}
|
||||
|
||||
void _updateRestorableController(T? value) {
|
||||
if (_restorableController != null) {
|
||||
_restorableController!.value.value = TextEditingValue(text: _findLabelByValue(value));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
|
||||
super.restoreState(oldBucket, initialRestore);
|
||||
if (_restorableController != null) {
|
||||
_registerRestorableController();
|
||||
// Make sure to update the internal [DropdownMenuFieldState] value to sync up with
|
||||
// text editing controller value if it matches one of the item label.
|
||||
final T? matchingValue = _findValueByLabel(_restorableController!.value.text);
|
||||
if (matchingValue != null) {
|
||||
setValue(matchingValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _registerRestorableController() {
|
||||
assert(_restorableController != null);
|
||||
registerForRestoration(_restorableController!, 'controller');
|
||||
}
|
||||
|
||||
T? _findValueByLabel(String label) {
|
||||
for (final DropdownMenuEntry<T> entry in _dropdownMenuFormField.dropdownMenuEntries) {
|
||||
if (entry.label == label) {
|
||||
return entry.value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String _findLabelByValue(T? value) {
|
||||
for (final DropdownMenuEntry<T> entry in _dropdownMenuFormField.dropdownMenuEntries) {
|
||||
if (entry.value == value) {
|
||||
return entry.label;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
1179
packages/flutter/test/material/dropdown_menu_form_field_test.dart
Normal file
1179
packages/flutter/test/material/dropdown_menu_form_field_test.dart
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user