diff --git a/CHANGELOG.md b/CHANGELOG.md index 0aa88215c..51e09ba77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Date format: DD/MM/YYYY - Added `.initialValue`, `.selectionControls`, `.mouseCursor`, `.textDirection`, `.scribbleEnabled` and `.enableIMEPersonalizedLearning` to `TextBox` - Added `AutoFillClient` to `TextBox` - Added `UnmanagedRestorationScope` to `TextFormBox` +- Added `AutoSuggestBox.form`, that uses `TextFormBox` instead of `TextBox` ([#353](https://github.com/bdlukaa/fluent_ui/pull/353)) ## [3.12.0] - Flutter 3.0 - [13/05/2022] diff --git a/example/lib/screens/forms.dart b/example/lib/screens/forms.dart index 7978c7329..df2e040bf 100644 --- a/example/lib/screens/forms.dart +++ b/example/lib/screens/forms.dart @@ -97,7 +97,7 @@ class _FormsState extends State { ), ]), const SizedBox(height: 20), - Row(children: [ + Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: TextBox( readOnly: true, @@ -114,7 +114,13 @@ class _FormsState extends State { ), const SizedBox(width: 10), Expanded( - child: AutoSuggestBox( + child: AutoSuggestBox.form( + autovalidateMode: AutovalidateMode.always, + validator: (t) { + if (t == null || t.isEmpty) return 'emtpy'; + + return null; + }, items: values, placeholder: 'Pick a color', trailingIcon: IconButton( diff --git a/lib/src/controls/form/auto_suggest_box.dart b/lib/src/controls/form/auto_suggest_box.dart index 52483d7b5..93e811bb7 100644 --- a/lib/src/controls/form/auto_suggest_box.dart +++ b/lib/src/controls/form/auto_suggest_box.dart @@ -21,9 +21,10 @@ enum TextChangedReason { /// /// See also: /// -/// * -/// * [TextBox], which is used by this widget to enter user text input -/// * [Overlay], which is used to show the suggestion popup +/// * +/// * [TextBox], which is used by this widget to enter user text input +/// * [TextFormBox], which is used by this widget by Form +/// * [Overlay], which is used to show the suggestion popup class AutoSuggestBox extends StatefulWidget { /// Creates a fluent-styled auto suggest box. const AutoSuggestBox({ @@ -50,6 +51,36 @@ class AutoSuggestBox extends StatefulWidget { this.scrollPadding = const EdgeInsets.all(20.0), this.selectionHeightStyle = ui.BoxHeightStyle.tight, this.selectionWidthStyle = ui.BoxWidthStyle.tight, + }) : autovalidateMode = AutovalidateMode.disabled, + validator = null, + super(key: key); + + const AutoSuggestBox.form({ + Key? key, + required this.items, + this.controller, + this.onChanged, + this.onSelected, + this.leadingIcon, + this.trailingIcon, + this.clearButtonEnabled = true, + this.placeholder, + this.placeholderStyle, + this.style, + this.decoration, + this.foregroundDecoration, + this.highlightColor, + this.cursorColor, + this.cursorHeight, + this.cursorRadius, + this.cursorWidth = 1.5, + this.showCursor, + this.keyboardAppearance, + this.scrollPadding = const EdgeInsets.all(20.0), + this.selectionHeightStyle = ui.BoxHeightStyle.tight, + this.selectionWidthStyle = ui.BoxWidthStyle.tight, + this.validator, + this.autovalidateMode = AutovalidateMode.disabled, }) : super(key: key); /// The list of items to display to the user to pick @@ -148,6 +179,14 @@ class AutoSuggestBox extends StatefulWidget { /// {@macro flutter.widgets.editableText.scrollPadding} final EdgeInsets scrollPadding; + /// An optional method that validates an input. Returns an error string to + /// display if the input is invalid, or null otherwise. + final FormFieldValidator? validator; + + /// Used to enable/disable this form field auto validation and update its + /// error text. + final AutovalidateMode autovalidateMode; + @override _AutoSuggestBoxState createState() => _AutoSuggestBoxState(); @@ -184,7 +223,7 @@ class _AutoSuggestBoxState extends State { late TextEditingController controller; final FocusScopeNode overlayNode = FocusScopeNode(); - final clearGlobalKey = GlobalKey(); + Size _boxSize = Size.zero; @override void initState() { @@ -193,6 +232,18 @@ class _AutoSuggestBoxState extends State { controller.addListener(() { if (!mounted) return; if (controller.text.length < 2) setState(() {}); + + // Update the overlay when the text box size has changed + WidgetsBinding.instance.addPostFrameCallback((_) async { + final box = _textBoxKey.currentContext!.findRenderObject() as RenderBox; + + if (_boxSize != box.size) { + _dismissOverlay(); + setState(() {}); + _showOverlay(); + _boxSize = box.size; + } + }); }); focusNode.addListener(_handleFocusChanged); } @@ -274,60 +325,90 @@ class _AutoSuggestBoxState extends State { } } + void _onChanged(String text) { + widget.onChanged?.call(text, TextChangedReason.userInput); + _showOverlay(); + } + + /// Whether a [TextFormBox] is used instead of a [TextBox] + bool get useForm => widget.validator != null; + @override Widget build(BuildContext context) { assert(debugCheckHasFluentTheme(context)); assert(debugCheckHasFluentLocalizations(context)); + final suffix = Row(mainAxisSize: MainAxisSize.min, children: [ + if (widget.trailingIcon != null) widget.trailingIcon!, + if (widget.clearButtonEnabled && controller.text.isNotEmpty) + Padding( + padding: const EdgeInsetsDirectional.only(start: 2.0), + child: IconButton( + icon: const Icon(FluentIcons.chrome_close), + onPressed: () { + controller.clear(); + focusNode.unfocus(); + }, + ), + ), + ]); + return CompositedTransformTarget( link: _layerLink, child: Actions( actions: { DirectionalFocusIntent: _DirectionalFocusAction(), }, - child: TextBox( - key: _textBoxKey, - controller: controller, - focusNode: focusNode, - placeholder: widget.placeholder, - placeholderStyle: widget.placeholderStyle, - clipBehavior: - _entry != null ? Clip.none : Clip.antiAliasWithSaveLayer, - prefix: widget.leadingIcon, - clearGlobalKey: clearGlobalKey, - suffix: Row(mainAxisSize: MainAxisSize.min, children: [ - if (widget.trailingIcon != null) widget.trailingIcon!, - if (widget.clearButtonEnabled && controller.text.isNotEmpty) - Padding( - padding: const EdgeInsetsDirectional.only(start: 2.0), - child: IconButton( - key: clearGlobalKey, - icon: const Icon(FluentIcons.chrome_close), - onPressed: () { - controller.clear(); - focusNode.unfocus(); - }, - ), + child: useForm + ? TextFormBox( + key: _textBoxKey, + controller: controller, + focusNode: focusNode, + placeholder: widget.placeholder, + placeholderStyle: widget.placeholderStyle, + clipBehavior: Clip.antiAliasWithSaveLayer, + prefix: widget.leadingIcon, + suffix: suffix, + suffixMode: OverlayVisibilityMode.always, + onChanged: _onChanged, + style: widget.style, + decoration: widget.decoration, + highlightColor: widget.highlightColor, + cursorColor: widget.cursorColor, + cursorHeight: widget.cursorHeight, + cursorRadius: widget.cursorRadius ?? const Radius.circular(2.0), + cursorWidth: widget.cursorWidth, + showCursor: widget.showCursor, + scrollPadding: widget.scrollPadding, + selectionHeightStyle: widget.selectionHeightStyle, + selectionWidthStyle: widget.selectionWidthStyle, + validator: widget.validator, + autovalidateMode: widget.autovalidateMode, + ) + : TextBox( + key: _textBoxKey, + controller: controller, + focusNode: focusNode, + placeholder: widget.placeholder, + placeholderStyle: widget.placeholderStyle, + clipBehavior: Clip.antiAliasWithSaveLayer, + prefix: widget.leadingIcon, + suffix: suffix, + suffixMode: OverlayVisibilityMode.always, + onChanged: _onChanged, + style: widget.style, + decoration: widget.decoration, + foregroundDecoration: widget.foregroundDecoration, + highlightColor: widget.highlightColor, + cursorColor: widget.cursorColor, + cursorHeight: widget.cursorHeight, + cursorRadius: widget.cursorRadius, + cursorWidth: widget.cursorWidth, + showCursor: widget.showCursor, + scrollPadding: widget.scrollPadding, + selectionHeightStyle: widget.selectionHeightStyle, + selectionWidthStyle: widget.selectionWidthStyle, ), - ]), - suffixMode: OverlayVisibilityMode.always, - onChanged: (text) { - widget.onChanged?.call(text, TextChangedReason.userInput); - _showOverlay(); - }, - style: widget.style, - decoration: widget.decoration, - foregroundDecoration: widget.foregroundDecoration, - highlightColor: widget.highlightColor, - cursorColor: widget.cursorColor, - cursorHeight: widget.cursorHeight, - cursorRadius: widget.cursorRadius, - cursorWidth: widget.cursorWidth, - showCursor: widget.showCursor, - scrollPadding: widget.scrollPadding, - selectionHeightStyle: widget.selectionHeightStyle, - selectionWidthStyle: widget.selectionWidthStyle, - ), ), ); } diff --git a/lib/src/controls/form/text_form_box.dart b/lib/src/controls/form/text_form_box.dart index 168d05ed0..fc346fd57 100644 --- a/lib/src/controls/form/text_form_box.dart +++ b/lib/src/controls/form/text_form_box.dart @@ -102,6 +102,8 @@ class TextFormBox extends FormField { bool enableIMEPersonalizedLearning = true, MouseCursor? mouseCursor, bool scribbleEnabled = true, + Color? highlightColor, + Color? errorHighlightColor, }) : assert(initialValue == null || controller == null), assert(obscuringCharacter.length == 1), assert(maxLines == null || maxLines > 0), @@ -135,8 +137,7 @@ class TextFormBox extends FormField { } return FormRow( - padding: - EdgeInsets.only(bottom: (field.errorText == null) ? 16.0 : 0), + padding: EdgeInsets.zero, error: (field.errorText == null) ? null : Text(field.errorText!), child: UnmanagedRestorationScope( bucket: field.bucket, @@ -189,7 +190,9 @@ class TextFormBox extends FormField { prefixMode: prefixMode, suffix: suffix, suffixMode: suffixMode, - highlightColor: (field.errorText == null) ? null : Colors.red, + highlightColor: (field.errorText == null) + ? highlightColor + : errorHighlightColor ?? Colors.red, dragStartBehavior: dragStartBehavior, minHeight: minHeight, padding: padding,