diff --git a/example/lib/src/storybook/stories/text_input.dart b/example/lib/src/storybook/stories/text_input.dart index e83b7a4a..95ede53f 100644 --- a/example/lib/src/storybook/stories/text_input.dart +++ b/example/lib/src/storybook/stories/text_input.dart @@ -5,6 +5,7 @@ import 'package:moon_design/moon_design.dart'; import 'package:storybook_flutter/storybook_flutter.dart'; TextEditingController _textController = TextEditingController(); +TextEditingController _dateController = TextEditingController(); TextEditingController _passwordController = TextEditingController(); bool _hidePassword = true; @@ -82,6 +83,17 @@ class TextInputStory extends Story { final inactiveBorderColor = colorTable(context)[inactiveBorderColorKnob ?? 40]; + final hoverBorderColorKnob = context.knobs.nullable.options( + label: "hoverBorderColor", + description: "MoonColors variants for MoonTextInput hover border.", + enabled: false, + initial: 0, + // piccolo + options: colorOptions, + ); + + final hoverBorderColor = colorTable(context)[hoverBorderColorKnob ?? 40]; + final errorColorKnob = context.knobs.nullable.options( label: "errorColor", description: "MoonColors variants for MoonTextInput error.", @@ -141,6 +153,7 @@ class TextInputStory extends Story { mainAxisAlignment: MainAxisAlignment.center, children: [ MoonFormTextInput( + hoverBorderColor: hoverBorderColor, controller: _textController, enabled: enabledKnob, textInputSize: textInputSizeKnob, @@ -152,8 +165,8 @@ class TextInputStory extends Story { inactiveBorderColor: inactiveBorderColor, errorColor: errorColor, borderRadius: borderRadius, - hintText: "Enter your text here (over 10 characters)", - validator: (String? value) => value?.length != null && value!.length < 10 + hintText: "Enter text (over 10 characters)", + validator: (String? value) => value != null && value.length < 10 ? "The text should be longer than 10 characters." : null, leading: showLeadingKnob @@ -199,7 +212,7 @@ class TextInputStory extends Story { validator: (String? value) => value != "123abc" ? "Wrong password." : null, leading: showLeadingKnob ? const MoonIcon( - MoonIcons.search_24, + MoonIcons.password_24, size: 24, ) : null, @@ -228,6 +241,56 @@ class TextInputStory extends Story { ); }, ), + const SizedBox(height: 16), + MoonFormTextInput( + readOnly: true, + hoverBorderColor: hoverBorderColor, + controller: _dateController, + enabled: enabledKnob, + textInputSize: textInputSizeKnob, + textColor: textColor, + hintTextColor: hintTextColor, + backgroundColor: backgroundColor, + activeBorderColor: activeBorderColor, + inactiveBorderColor: inactiveBorderColor, + errorColor: errorColor, + borderRadius: borderRadius, + hintText: "Pick a date", + validator: (String? value) => value != null && value.isEmpty ? "Pick a date." : null, + onTap: () async { + final DateTime? pickedDate = await showDatePicker( + context: context, + initialDate: DateTime.now(), + firstDate: DateTime.now(), + lastDate: DateTime(2050), + ); + + if (pickedDate != null) { + _dateController.text = "${pickedDate.toLocal()}".split(" ")[0]; + } + }, + leading: showLeadingKnob + ? const MoonIcon( + MoonIcons.calendar_24, + size: 24, + ) + : null, + trailing: showTrailingKnob + ? MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + child: const MoonIcon( + MoonIcons.close_small_24, + size: 24, + ), + onTap: () => _dateController.clear(), + ), + ) + : null, + helper: showHelperKnob ? const Text("Supporting text") : null, + errorBuilder: (BuildContext context, String? errorText) => + StoryErrorWidget(errorText: errorText!), + ), const SizedBox(height: 32), MoonFilledButton( label: const Text("Submit"), diff --git a/example/lib/src/storybook/stories/text_input_group.dart b/example/lib/src/storybook/stories/text_input_group.dart index 66d33a66..91f93d9a 100644 --- a/example/lib/src/storybook/stories/text_input_group.dart +++ b/example/lib/src/storybook/stories/text_input_group.dart @@ -132,7 +132,7 @@ class TextInputGroupStory extends Story { activeBorderColor: activeBorderColor, errorColor: errorColor, borderRadius: borderRadius, - hintText: "Enter your text here (over 10 characters)", + hintText: "Enter text (over 10 characters)", validator: (String? value) => value?.length != null && value!.length < 10 ? "The text should be longer than 10 characters." : null, diff --git a/lib/src/theme/text_input/text_input_theme.dart b/lib/src/theme/text_input/text_input_theme.dart index 653596eb..15f1e6a2 100644 --- a/lib/src/theme/text_input/text_input_theme.dart +++ b/lib/src/theme/text_input/text_input_theme.dart @@ -32,8 +32,8 @@ class MoonTextInputTheme extends ThemeExtension with Diagnos inactiveBorderColor: tokens.colors.beerus, errorColor: tokens.colors.chiChi100, hoverBorderColor: tokens.colors.beerus, - textColor: tokens.colors.trunks, - helperTextColor: tokens.colors.trunks, + textColor: tokens.colors.textPrimary, + helperTextColor: tokens.colors.textSecondary, ), properties = properties ?? MoonTextInputProperties( diff --git a/lib/src/utils/squircle/squircle_border.dart b/lib/src/utils/squircle/squircle_border.dart index f81742a1..bca2cb72 100644 --- a/lib/src/utils/squircle/squircle_border.dart +++ b/lib/src/utils/squircle/squircle_border.dart @@ -1,5 +1,9 @@ +import 'dart:math'; +import 'dart:ui'; + import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:moon_design/src/utils/color_premul_lerp.dart'; import 'package:moon_design/src/utils/squircle/squircle_border_radius.dart'; import 'package:moon_design/src/utils/squircle/squircle_radius.dart'; @@ -19,7 +23,7 @@ class MoonSquircleBorder extends OutlinedBorder { final BorderAlign borderAlign; const MoonSquircleBorder({ - super.side = BorderSide.none, + super.side = MoonSquicleBorderSide.none, this.borderRadius = MoonSquircleBorderRadius.zero, this.borderAlign = BorderAlign.inside, }); @@ -49,7 +53,7 @@ class MoonSquircleBorder extends OutlinedBorder { ShapeBorder? lerpFrom(ShapeBorder? a, double t) { if (a is MoonSquircleBorder) { return MoonSquircleBorder( - side: BorderSide.lerp(a.side, side, t), + side: MoonSquicleBorderSide.lerp(a.side, side, t), borderRadius: MoonSquircleBorderRadius.lerp(a.borderRadius, borderRadius, t)!, ); } @@ -60,7 +64,7 @@ class MoonSquircleBorder extends OutlinedBorder { ShapeBorder? lerpTo(ShapeBorder? b, double t) { if (b is MoonSquircleBorder) { return MoonSquircleBorder( - side: BorderSide.lerp(side, b.side, t), + side: MoonSquicleBorderSide.lerp(side, b.side, t), borderRadius: MoonSquircleBorderRadius.lerp(borderRadius, b.borderRadius, t)!, ); } @@ -211,3 +215,304 @@ class MoonSquircleBorder extends OutlinedBorder { return '${objectRuntimeType(this, 'MoonSquircleBorder')}($side, $borderRadius, $borderAlign)'; } } + +class MoonSquicleBorderSide with Diagnosticable { + /// The border is drawn fully inside of the border path. + /// + /// This is a constant for use with [strokeAlign]. + /// + /// This is the default value for [strokeAlign]. + static const double strokeAlignInside = -1.0; + + /// The border is drawn on the center of the border path, with half of the + /// [BorderSide.width] on the inside, and the other half on the outside of + /// the path. + /// + /// This is a constant for use with [strokeAlign]. + static const double strokeAlignCenter = 0.0; + + /// The border is drawn on the outside of the border path. + /// + /// This is a constant for use with [strokeAlign]. + static const double strokeAlignOutside = 1.0; + + /// Creates a [BorderSide] that represents the addition of the two given + /// [BorderSide]s. + /// + /// It is only valid to call this if [canMerge] returns true for the two + /// sides. + /// + /// If one of the sides is zero-width with [BorderStyle.none], then the other + /// side is return as-is. If both of the sides are zero-width with + /// [BorderStyle.none], then [BorderSide.none] is returned. + /// + /// The arguments must not be null. + static BorderSide merge(BorderSide a, BorderSide b) { + assert(canMerge(a, b)); + final bool aIsNone = a.style == BorderStyle.none && a.width == 0.0; + final bool bIsNone = b.style == BorderStyle.none && b.width == 0.0; + if (aIsNone && bIsNone) { + return BorderSide.none; + } + if (aIsNone) { + return b; + } + if (bIsNone) { + return a; + } + assert(a.color == b.color); + assert(a.style == b.style); + return BorderSide( + color: a.color, // == b.color + width: a.width + b.width, + strokeAlign: max(a.strokeAlign, b.strokeAlign), + style: a.style, // == b.style + ); + } + + /// The color of this side of the border. + final Color color; + + /// The width of this side of the border, in logical pixels. + /// + /// Setting width to 0.0 will result in a hairline border. This means that + /// the border will have the width of one physical pixel. Hairline + /// rendering takes shortcuts when the path overlaps a pixel more than once. + /// This means that it will render faster than otherwise, but it might + /// double-hit pixels, giving it a slightly darker/lighter result. + /// + /// To omit the border entirely, set the [style] to [BorderStyle.none]. + final double width; + + /// The style of this side of the border. + /// + /// To omit a side, set [style] to [BorderStyle.none]. This skips + /// painting the border, but the border still has a [width]. + final BorderStyle style; + + /// A hairline black border that is not rendered. + static const BorderSide none = BorderSide.none; + + /// The relative position of the stroke on a [BorderSide] in an + /// [OutlinedBorder] or [Border]. + /// + /// Values typically range from -1.0 ([strokeAlignInside], inside border, + /// default) to 1.0 ([strokeAlignOutside], outside border), without any + /// bound constraints (e.g., a value of -2.0 is not typical, but allowed). + /// A value of 0 ([strokeAlignCenter]) will center the border on the edge + /// of the widget. + /// + /// When set to [strokeAlignInside], the stroke is drawn completely inside + /// the widget. For [strokeAlignCenter] and [strokeAlignOutside], a property + /// such as [Container.clipBehavior] can be used in an outside widget to clip + /// it. If [Container.decoration] has a border, the container may incorporate + /// [width] as additional padding: + /// - [strokeAlignInside] provides padding with full [width]. + /// - [strokeAlignCenter] provides padding with half [width]. + /// - [strokeAlignOutside] provides zero padding, as stroke is drawn entirely outside. + /// + /// This property is not honored by [toPaint] (because the [Paint] object + /// cannot represent it); it is intended that classes that use [BorderSide] + /// objects implement this property when painting borders by suitably + /// inflating or deflating their regions. + /// + /// {@tool dartpad} + /// This example shows an animation of how [strokeAlign] affects the drawing + /// when applied to borders of various shapes. + /// + /// ** See code in examples/api/lib/painting/borders/border_side.stroke_align.0.dart ** + /// {@end-tool} + final double strokeAlign; + + /// Creates the side of a border with premultiplied alpha color. + /// + /// By default, the border is 1.0 logical pixels wide and solid black. + const MoonSquicleBorderSide({ + this.color = const Color(0xFF000000), + this.width = 1.0, + this.style = BorderStyle.solid, + this.strokeAlign = strokeAlignInside, + }) : assert(width >= 0.0); + + /// Creates a copy of this border but with the given fields replaced with the new values. + BorderSide copyWith({ + Color? color, + double? width, + BorderStyle? style, + double? strokeAlign, + }) { + return BorderSide( + color: color ?? this.color, + width: width ?? this.width, + style: style ?? this.style, + strokeAlign: strokeAlign ?? this.strokeAlign, + ); + } + + /// Creates a copy of this border side description but with the width scaled + /// by the factor `t`. + /// + /// The `t` argument represents the multiplicand, or the position on the + /// timeline for an interpolation from nothing to `this`, with 0.0 meaning + /// that the object returned should be the nil variant of this object, 1.0 + /// meaning that no change should be applied, returning `this` (or something + /// equivalent to `this`), and other values meaning that the object should be + /// multiplied by `t`. Negative values are treated like zero. + /// + /// Since a zero width is normally painted as a hairline width rather than no + /// border at all, the zero factor is special-cased to instead change the + /// style to [BorderStyle.none]. + /// + /// Values for `t` are usually obtained from an [Animation], such as + /// an [AnimationController]. + BorderSide scale(double t) { + return BorderSide( + color: color, + width: max(0.0, width * t), + style: t <= 0.0 ? BorderStyle.none : style, + ); + } + + /// Create a [Paint] object that, if used to stroke a line, will draw the line + /// in this border's style. + /// + /// The [strokeAlign] property is not reflected in the [Paint]; consumers must + /// implement that directly by inflating or deflating their region appropriately. + /// + /// Not all borders use this method to paint their border sides. For example, + /// non-uniform rectangular [Border]s have beveled edges and so paint their + /// border sides as filled shapes rather than using a stroke. + Paint toPaint() { + switch (style) { + case BorderStyle.solid: + return Paint() + ..color = color + ..strokeWidth = width + ..style = PaintingStyle.stroke; + case BorderStyle.none: + return Paint() + ..color = const Color(0x00000000) + ..strokeWidth = 0.0 + ..style = PaintingStyle.stroke; + } + } + + /// Whether the two given [BorderSide]s can be merged using + /// [BorderSide.merge]. + /// + /// Two sides can be merged if one or both are zero-width with + /// [BorderStyle.none], or if they both have the same color and style. + /// + /// The arguments must not be null. + static bool canMerge(BorderSide a, BorderSide b) { + if ((a.style == BorderStyle.none && a.width == 0.0) || (b.style == BorderStyle.none && b.width == 0.0)) { + return true; + } + return a.style == b.style && a.color == b.color; + } + + /// Linearly interpolate between two border sides. + /// + /// The arguments must not be null. + /// + /// {@macro dart.ui.shadow.lerp} + static BorderSide lerp(BorderSide a, BorderSide b, double t) { + if (identical(a, b)) { + return a; + } + if (t == 0.0) { + return a; + } + if (t == 1.0) { + return b; + } + final double width = lerpDouble(a.width, b.width, t)!; + if (width < 0.0) { + return BorderSide.none; + } + if (a.style == b.style && a.strokeAlign == b.strokeAlign) { + return BorderSide( + color: colorPremulLerp(a.color, b.color, t)!, + width: width, + style: a.style, // == b.style + strokeAlign: a.strokeAlign, // == b.strokeAlign + ); + } + final Color colorA; + final Color colorB; + switch (a.style) { + case BorderStyle.solid: + colorA = a.color; + case BorderStyle.none: + colorA = a.color.withAlpha(0x00); + } + switch (b.style) { + case BorderStyle.solid: + colorB = b.color; + case BorderStyle.none: + colorB = b.color.withAlpha(0x00); + } + if (a.strokeAlign != b.strokeAlign) { + return BorderSide( + color: colorPremulLerp(colorA, colorB, t)!, + width: width, + strokeAlign: lerpDouble(a.strokeAlign, b.strokeAlign, t)!, + ); + } + return BorderSide( + color: colorPremulLerp(colorA, colorB, t)!, + width: width, + strokeAlign: a.strokeAlign, // == b.strokeAlign + ); + } + + /// Get the amount of the stroke width that lies inside of the [BorderSide]. + /// + /// For example, this will return the [width] for a [strokeAlign] of -1, half + /// the [width] for a [strokeAlign] of 0, and 0 for a [strokeAlign] of 1. + double get strokeInset => width * (1 - (1 + strokeAlign) / 2); + + /// Get the amount of the stroke width that lies outside of the [BorderSide]. + /// + /// For example, this will return 0 for a [strokeAlign] of -1, half the + /// [width] for a [strokeAlign] of 0, and the [width] for a [strokeAlign] + /// of 1. + double get strokeOutset => width * (1 + strokeAlign) / 2; + + /// The offset of the stroke, taking into account the stroke alignment. + /// + /// For example, this will return the negative [width] of the stroke + /// for a [strokeAlign] of -1, 0 for a [strokeAlign] of 0, and the + /// [width] for a [strokeAlign] of -1. + double get strokeOffset => width * strokeAlign; + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return other is BorderSide && + other.color == color && + other.width == width && + other.style == style && + other.strokeAlign == strokeAlign; + } + + @override + int get hashCode => Object.hash(color, width, style, strokeAlign); + + @override + String toStringShort() => 'BorderSide'; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('color', color, defaultValue: const Color(0xFF000000))); + properties.add(DoubleProperty('width', width, defaultValue: 1.0)); + properties.add(DoubleProperty('strokeAlign', strokeAlign, defaultValue: strokeAlignInside)); + properties.add(EnumProperty('style', style, defaultValue: BorderStyle.solid)); + } +} diff --git a/lib/src/widgets/text_input/form_text_input.dart b/lib/src/widgets/text_input/form_text_input.dart index 989cf083..034df0d8 100644 --- a/lib/src/widgets/text_input/form_text_input.dart +++ b/lib/src/widgets/text_input/form_text_input.dart @@ -1,578 +1,535 @@ import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:moon_design/src/widgets/text_input/text_input.dart'; -export 'package:flutter/services.dart' - show SmartDashesType, SmartQuotesType, TextCapitalization, TextInputAction, TextInputType; +export 'package:flutter/services.dart' show SmartDashesType, SmartQuotesType; typedef MoonFormTextInputErrorCallback = void Function(String? errorText); -/// A [FormField] that contains a [MoonTextInput]. -/// -/// This is a convenience widget that wraps a [MoonTextInput] 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` (see [GlobalKey]) to the constructor and use -/// [GlobalKey.currentState] to save or reset the form field. -/// -/// When a [controller] is specified, its [TextEditingController.text] -/// defines the [initialValue]. If this [FormField] is part of a scrolling -/// container that lazily constructs its children, like a [ListView] or a -/// [CustomScrollView], then a [controller] should be specified. -/// The controller's lifetime should be managed by a stateful widget ancestor -/// of the scrolling container. -/// -/// If a [controller] is not specified, [initialValue] can be used to give -/// the automatically generated controller an initial value. -/// -/// {@macro flutter.material.textfield.wantKeepAlive} -/// -/// Remember to call [TextEditingController.dispose] of the [TextEditingController] -/// when it is no longer needed. This will ensure any resources used by the object -/// are discarded. -/// -/// -/// -/// {@tool snippet} -/// -/// Creates a [MoonFormTextInput] with a validator function. -/// -/// ![If the user enters valid text, the MoonTextInput appears normally without any warnings to the user](https://flutter.github.io/assets-for-api-docs/assets/material/text_form_field.png) -/// -/// ![If the user enters invalid text, the error message returned from the validator function is displayed in dark red underneath the input](https://flutter.github.io/assets-for-api-docs/assets/material/text_form_field_error.png) -class MoonFormTextInput extends StatefulWidget { - /// If [maxLength] is set to this value, only the "current input length" part of the character counter is shown. - static const int noMaxLength = -1; - - static Widget _defaultContextMenuBuilder(BuildContext context, EditableTextState editableTextState) { +class MoonFormTextInput extends FormField { + final MoonFormTextInputConfiguration configuration; + + /// Creates a [MoonFormTextInput] that contains a [MoonTextInput]. + /// + /// When a [controller] is specified, [initialValue] must be null (the default). If [controller] is null, + /// then a [TextEditingController] will be constructed automatically and its `text` will be initialized to + /// [initialValue] or the empty string. + /// + /// For documentation about the various parameters, see the [MoonTextInput] class. + MoonFormTextInput({ + super.key, + // MDS props + bool hasFloatingLabel = false, + BorderRadiusGeometry? borderRadius, + Color? backgroundColor, + Color? activeBorderColor, + Color? inactiveBorderColor, + Color? errorColor, + Color? hoverBorderColor, + Color? textColor, + Color? hintTextColor, + Decoration? decoration, + double? gap, + double? height, + Duration? transitionDuration, + Curve? transitionCurve, + EdgeInsetsGeometry? padding, + EdgeInsetsGeometry? helperPadding, + MoonFormTextInputErrorCallback? onError, + MoonTextInputSize? textInputSize, + String? errorText, + String? hintText, + String? initialValue, + TextStyle? helperTextStyle, + MoonTextInputErrorBuilder? errorBuilder, + Widget? leading, + Widget? trailing, + Widget? helper, + + // Flutter props + this.controller, + FocusNode? focusNode, + TextInputType? keyboardType, + TextCapitalization textCapitalization = TextCapitalization.none, + TextInputAction? textInputAction, + TextStyle? style, + StrutStyle? strutStyle, + TextDirection? textDirection, + TextAlign textAlign = TextAlign.start, + TextAlignVertical? textAlignVertical, + bool autofocus = false, + bool readOnly = false, + bool? showCursor, + String obscuringCharacter = '•', + bool obscureText = false, + bool autocorrect = true, + SmartDashesType? smartDashesType, + SmartQuotesType? smartQuotesType, + bool enableSuggestions = true, + MaxLengthEnforcement? maxLengthEnforcement, + int? maxLines = 1, + int? minLines, + bool expands = false, + int? maxLength, + ValueChanged? onChanged, + GestureTapCallback? onTap, + TapRegionCallback? onTapOutside, + VoidCallback? onEditingComplete, + ValueChanged? onSubmitted, + super.onSaved, + super.validator, + List? inputFormatters, + bool? enabled, + double cursorWidth = 2.0, + double? cursorHeight, + Radius? cursorRadius, + Color? cursorColor, + Brightness? keyboardAppearance, + EdgeInsets scrollPadding = const EdgeInsets.all(20.0), + bool? enableInteractiveSelection, + TextSelectionControls? selectionControls, + ScrollPhysics? scrollPhysics, + Iterable? autofillHints, + AutovalidateMode? autovalidateMode, + ScrollController? scrollController, + super.restorationId, + bool enableIMEPersonalizedLearning = true, + MouseCursor? mouseCursor, + EditableTextContextMenuBuilder? contextMenuBuilder = defaultContextMenuBuilder, + SpellCheckConfiguration? spellCheckConfiguration, + TextMagnifierConfiguration? magnifierConfiguration, + + // Flutter missing props + AppPrivateCommandCallback? onAppPrivateCommand, + bool canRequestFocus = true, + bool scribbleEnabled = true, + bool? cursorOpacityAnimates, + Clip clipBehavior = Clip.hardEdge, + ContentInsertionConfiguration? contentInsertionConfiguration, + DragStartBehavior dragStartBehavior = DragStartBehavior.start, + ui.BoxHeightStyle selectionHeightStyle = ui.BoxHeightStyle.tight, + ui.BoxWidthStyle selectionWidthStyle = ui.BoxWidthStyle.tight, + UndoHistoryController? undoController, + }) : assert(initialValue == null || controller == null), + assert(obscuringCharacter.length == 1), + assert(maxLines == null || maxLines > 0), + assert(minLines == null || minLines > 0), + assert( + (maxLines == null) || (minLines == null) || (maxLines >= minLines), + "minLines can't be greater than maxLines", + ), + assert( + !expands || (maxLines == null && minLines == null), + "minLines and maxLines must be null when expands is true.", + ), + assert(!obscureText || maxLines == 1, "Obscured fields cannot be multiline."), + assert(maxLength == null || maxLength == MoonTextInput.noMaxLength || maxLength > 0), + configuration = MoonFormTextInputConfiguration( + activeBorderColor: activeBorderColor, + autocorrect: autocorrect, + autofillHints: autofillHints, + autofocus: autofocus, + backgroundColor: backgroundColor, + borderRadius: borderRadius, + canRequestFocus: canRequestFocus, + clipBehavior: clipBehavior, + contentInsertionConfiguration: contentInsertionConfiguration, + contextMenuBuilder: contextMenuBuilder, + controller: controller, + cursorColor: cursorColor, + cursorHeight: cursorHeight, + cursorOpacityAnimates: cursorOpacityAnimates, + cursorRadius: cursorRadius, + cursorWidth: cursorWidth, + decoration: decoration, + dragStartBehavior: dragStartBehavior, + enabled: enabled ?? true, + enableIMEPersonalizedLearning: enableIMEPersonalizedLearning, + enableInteractiveSelection: enableInteractiveSelection ?? (!obscureText || !readOnly), + enableSuggestions: enableSuggestions, + errorBuilder: errorBuilder, + errorColor: errorColor, + errorText: errorText, + expands: expands, + focusNode: focusNode, + gap: gap, + hasFloatingLabel: hasFloatingLabel, + height: height, + helper: helper, + helperPadding: helperPadding, + helperTextStyle: helperTextStyle, + hintText: hintText, + hintTextColor: hintTextColor, + hoverBorderColor: hoverBorderColor, + inactiveBorderColor: inactiveBorderColor, + initialValue: controller != null ? controller.text : (initialValue ?? ""), + inputFormatters: inputFormatters, + keyboardAppearance: keyboardAppearance, + keyboardType: keyboardType, + leading: leading, + magnifierConfiguration: magnifierConfiguration, + maxLength: maxLength, + maxLengthEnforcement: maxLengthEnforcement, + maxLines: maxLines, + minLines: minLines, + mouseCursor: mouseCursor, + obscureText: obscureText, + obscuringCharacter: obscuringCharacter, + onAppPrivateCommand: onAppPrivateCommand, + onChanged: onChanged, + onEditingComplete: onEditingComplete, + onSubmitted: onSubmitted, + onTap: onTap, + onTapOutside: onTapOutside, + padding: padding, + readOnly: readOnly, + restorationId: restorationId, + scribbleEnabled: scribbleEnabled, + scrollController: scrollController, + scrollPadding: scrollPadding, + scrollPhysics: scrollPhysics, + selectionControls: selectionControls, + selectionHeightStyle: selectionHeightStyle, + selectionWidthStyle: selectionWidthStyle, + showCursor: showCursor, + smartDashesType: smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled), + smartQuotesType: smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled), + spellCheckConfiguration: spellCheckConfiguration, + strutStyle: strutStyle, + style: style, + textAlign: textAlign, + textAlignVertical: textAlignVertical, + textCapitalization: textCapitalization, + textColor: textColor, + textDirection: textDirection, + textInputAction: textInputAction, + textInputSize: textInputSize, + trailing: trailing, + transitionCurve: transitionCurve, + transitionDuration: transitionDuration, + undoController: undoController, + autovalidateMode: autovalidateMode ?? AutovalidateMode.disabled, + ), + super( + initialValue: controller != null ? controller.text : (initialValue ?? ""), + enabled: enabled ?? true, + autovalidateMode: autovalidateMode ?? AutovalidateMode.disabled, + builder: (FormFieldState field) { + final _MoonFormTextInputState state = field as _MoonFormTextInputState; + + if (field.hasError) { + onError?.call(field.errorText); + } + + void onChangedHandler(String value) { + field.didChange(value); + if (onChanged != null) { + onChanged(value); + } + } + + return UnmanagedRestorationScope( + bucket: field.bucket, + child: MoonTextInput( + activeBorderColor: activeBorderColor, + autocorrect: autocorrect, + autofillHints: autofillHints, + autofocus: autofocus, + backgroundColor: backgroundColor, + borderRadius: borderRadius, + canRequestFocus: canRequestFocus, + clipBehavior: clipBehavior, + contentInsertionConfiguration: contentInsertionConfiguration, + contextMenuBuilder: contextMenuBuilder, + controller: state._effectiveController, + cursorColor: cursorColor, + cursorHeight: cursorHeight, + cursorOpacityAnimates: cursorOpacityAnimates, + cursorRadius: cursorRadius, + cursorWidth: cursorWidth, + decoration: decoration, + dragStartBehavior: dragStartBehavior, + enabled: enabled ?? true, + enableIMEPersonalizedLearning: enableIMEPersonalizedLearning, + enableInteractiveSelection: enableInteractiveSelection ?? (!obscureText || !readOnly), + enableSuggestions: enableSuggestions, + errorBuilder: errorBuilder, + errorColor: errorColor, + errorText: field.errorText ?? errorText, + expands: expands, + focusNode: focusNode, + gap: gap, + hasFloatingLabel: hasFloatingLabel, + height: height, + helper: helper, + helperPadding: helperPadding, + helperTextStyle: helperTextStyle, + hintText: hintText, + hintTextColor: hintTextColor, + hoverBorderColor: hoverBorderColor, + inactiveBorderColor: inactiveBorderColor, + initialValue: initialValue, + inputFormatters: inputFormatters, + keyboardAppearance: keyboardAppearance, + keyboardType: keyboardType, + leading: leading, + magnifierConfiguration: magnifierConfiguration, + maxLength: maxLength, + maxLengthEnforcement: maxLengthEnforcement, + maxLines: maxLines, + minLines: minLines, + mouseCursor: mouseCursor, + obscureText: obscureText, + obscuringCharacter: obscuringCharacter, + onAppPrivateCommand: onAppPrivateCommand, + onChanged: onChangedHandler, + onEditingComplete: onEditingComplete, + onSubmitted: onSubmitted, + onTap: onTap, + onTapOutside: onTapOutside, + padding: padding, + readOnly: readOnly, + restorationId: restorationId, + scribbleEnabled: scribbleEnabled, + scrollController: scrollController, + scrollPadding: scrollPadding, + scrollPhysics: scrollPhysics, + selectionControls: selectionControls, + selectionHeightStyle: selectionHeightStyle, + selectionWidthStyle: selectionWidthStyle, + showCursor: showCursor, + smartDashesType: smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled), + smartQuotesType: smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled), + spellCheckConfiguration: spellCheckConfiguration, + strutStyle: strutStyle, + style: style, + textAlign: textAlign, + textAlignVertical: textAlignVertical, + textCapitalization: textCapitalization, + textColor: textColor, + textDirection: textDirection, + textInputAction: textInputAction, + textInputSize: textInputSize, + trailing: trailing, + transitionCurve: transitionCurve, + transitionDuration: transitionDuration, + undoController: undoController, + ), + ); + }, + ); + + /// Controls the text being edited. + /// + /// If null, this widget will create its own [TextEditingController] and initialize its [TextEditingController.text] + /// with [initialValue]. + final TextEditingController? controller; + + static Widget defaultContextMenuBuilder(BuildContext context, EditableTextState editableTextState) { return AdaptiveTextSelectionToolbar.editableText( editableTextState: editableTextState, ); } - // MDS props + @override + FormFieldState createState() => _MoonFormTextInputState(); +} - /// Whether the form text input has floating label. - final bool hasFloatingLabel; +class _MoonFormTextInputState extends FormFieldState { + RestorableTextEditingController? _controller; - /// The border radius of the form text input. - final BorderRadiusGeometry? borderRadius; + TextEditingController get _effectiveController => _moonFormTextInput.controller ?? _controller!.value; - /// The background color of the form text input. - final Color? backgroundColor; + MoonFormTextInput get _moonFormTextInput => super.widget as MoonFormTextInput; - /// The border color of the active or focused form text input. - final Color? activeBorderColor; + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + super.restoreState(oldBucket, initialRestore); + if (_controller != null) { + _registerController(); + } + // Make sure to update the internal [FormFieldState] value to sync up with text editing controller value. + setValue(_effectiveController.text); + } - /// The border color of the inactive form text input. - final Color? inactiveBorderColor; + void _registerController() { + assert(_controller != null); + registerForRestoration(_controller!, 'controller'); + } - /// The color used for form text input error state. - final Color? errorColor; + void _createLocalController([TextEditingValue? value]) { + assert(_controller == null); + _controller = value == null ? RestorableTextEditingController() : RestorableTextEditingController.fromValue(value); + if (!restorePending) { + _registerController(); + } + } - /// The border color of the hovered form text input. - final Color? hoverBorderColor; + @override + void initState() { + super.initState(); + if (_moonFormTextInput.controller == null) { + _createLocalController(widget.initialValue != null ? TextEditingValue(text: widget.initialValue!) : null); + } else { + _moonFormTextInput.controller!.addListener(_handleControllerChanged); + } + } - /// The text color of the form text input. - final Color? textColor; + @override + void didUpdateWidget(MoonFormTextInput oldWidget) { + super.didUpdateWidget(oldWidget); + if (_moonFormTextInput.controller != oldWidget.controller) { + oldWidget.controller?.removeListener(_handleControllerChanged); + _moonFormTextInput.controller?.addListener(_handleControllerChanged); + + if (oldWidget.controller != null && _moonFormTextInput.controller == null) { + _createLocalController(oldWidget.controller!.value); + } + + if (_moonFormTextInput.controller != null) { + setValue(_moonFormTextInput.controller!.text); + if (oldWidget.controller == null) { + unregisterFromRestoration(_controller!); + _controller!.dispose(); + _controller = null; + } + } + } + } - /// The text color of the hint in form text input. - final Color? hintTextColor; + @override + void dispose() { + _moonFormTextInput.controller?.removeListener(_handleControllerChanged); + _controller?.dispose(); + super.dispose(); + } - /// Custom decoration for the form text input. - final Decoration? decoration; + @override + void didChange(String? value) { + super.didChange(value); - /// The gap between the leading, trailing widgets and the form text input area. - final double? gap; + if (_effectiveController.text != value) { + _effectiveController.text = value ?? ""; + } + } - /// The height of the form text input (this does not include the space taken by [MoonFormTextInput.errorBuilder]). - final double? height; + @override + void reset() { + // setState will be called in the superclass, so even though state is being manipulated, no setState call is needed + // here. + _effectiveController.text = widget.initialValue ?? ''; + super.reset(); + } - /// The transition duration for disable animation. - final Duration? transitionDuration; + void _handleControllerChanged() { + // Suppress changes that originated from within this class. + // + // In the case where a controller has been passed in to this widget, we register this change listener. In these + // cases, we'll also receive change notifications for changes originating from within this class -- for example, + // the reset() method. In such cases, the FormField value will already have been set. + if (_effectiveController.text != value) { + didChange(_effectiveController.text); + } + } +} - /// The transition curve for disable animation. +class MoonFormTextInputConfiguration { + // MDS props + final bool hasFloatingLabel; + final BorderRadiusGeometry? borderRadius; + final Color? backgroundColor; + final Color? activeBorderColor; + final Color? inactiveBorderColor; + final Color? errorColor; + final Color? hoverBorderColor; + final Color? textColor; + final Color? hintTextColor; + final Decoration? decoration; + final double? gap; + final double? height; + final Duration? transitionDuration; final Curve? transitionCurve; - - /// The padding of the form text input. final EdgeInsetsGeometry? padding; - - /// The padding around helper widget or error builder. final EdgeInsetsGeometry? helperPadding; - - /// Callback for when the form text input has an error. final MoonFormTextInputErrorCallback? onError; - - /// The size of the form text input. final MoonTextInputSize? textInputSize; - - /// The text for the error. final String? errorText; - - /// The text for the hint. final String? hintText; - - /// The initial value of the form text input. final String? initialValue; - - /// The text style to use for the error state text. final TextStyle? helperTextStyle; - - /// Builder for the error widget. final MoonTextInputErrorBuilder? errorBuilder; - - /// The widget in the leading slot of the form text input. final Widget? leading; - - /// The widget in the trailing slot of the form text input. final Widget? trailing; - - /// The widget in the helper slot of the form text input. final Widget? helper; - /////// // Flutter props - - /// {@macro flutter.widgets.magnifier.TextMagnifierConfiguration.intro} - /// - /// {@macro flutter.widgets.magnifier.intro} - /// - /// {@macro flutter.widgets.magnifier.TextMagnifierConfiguration.details} - /// - /// By default, builds a [CupertinoTextMagnifier] on iOS and [TextMagnifier] - /// on Android, and builds nothing on all other platforms. If it is desired to - /// suppress the magnifier, consider passing [TextMagnifierConfiguration.disabled]. - /// - /// {@tool dartpad} - /// This sample demonstrates how to customize the magnifier that this text field uses. - /// - /// ** See code in examples/api/lib/widgets/text_magnifier/text_magnifier.0.dart ** - /// {@end-tool} final TextMagnifierConfiguration? magnifierConfiguration; - - /// Controls the text being edited. - /// - /// If null, this widget will create its own [TextEditingController]. final TextEditingController? controller; - - /// Defines the keyboard focus for this widget. - /// - /// The [focusNode] is a long-lived object that's typically managed by a - /// [StatefulWidget] parent. See [FocusNode] for more information. - /// - /// To give the keyboard focus to this widget, provide a [focusNode] and then - /// use the current [FocusScope] to request the focus: - /// - /// ```dart - /// FocusScope.of(context).requestFocus(myFocusNode); - /// ``` - /// - /// This happens automatically when the widget is tapped. - /// - /// To be notified when the widget gains or loses the focus, add a listener - /// to the [focusNode]: - /// - /// ```dart - /// myFocusNode.addListener(() { print(myFocusNode.hasFocus); }); - /// ``` - /// - /// If null, this widget will create its own [FocusNode]. - /// - /// ## Keyboard - /// - /// Requesting the focus will typically cause the keyboard to be shown - /// if it's not showing already. - /// - /// On Android, the user can hide the keyboard - without changing the focus - - /// with the system back button. They can restore the keyboard's visibility - /// by tapping on a text field. The user might hide the keyboard and - /// switch to a physical keyboard, or they might just need to get it - /// out of the way for a moment, to expose something it's - /// obscuring. In this case requesting the focus again will not - /// cause the focus to change, and will not make the keyboard visible. - /// - /// This widget builds an [EditableText] and will ensure that the keyboard is - /// showing when it is tapped by calling [EditableTextState.requestKeyboard()]. final FocusNode? focusNode; - - /// {@macro flutter.widgets.editableText.keyboardType} final TextInputType keyboardType; - - /// The type of action button to use for the keyboard. - /// - /// Defaults to [TextInputAction.newline] if [keyboardType] is - /// [TextInputType.multiline] and [TextInputAction.done] otherwise. final TextInputAction? textInputAction; - - /// {@macro flutter.widgets.editableText.textCapitalization} final TextCapitalization textCapitalization; - - /// The style to use for the text being edited. - /// - /// This text style is also used as the base style for the [decoration]. - /// - /// If null, defaults to the `titleMedium` text style from the current [Theme]. final TextStyle? style; - - /// {@macro flutter.widgets.editableText.strutStyle} final StrutStyle? strutStyle; - - /// {@macro flutter.widgets.editableText.textAlign} final TextAlign textAlign; - - /// {@macro flutter.material.InputDecorator.textAlignVertical} final TextAlignVertical? textAlignVertical; - - /// {@macro flutter.widgets.editableText.textDirection} final TextDirection? textDirection; - - /// {@macro flutter.widgets.editableText.autofocus} final bool autofocus; - - /// {@macro flutter.widgets.editableText.obscuringCharacter} final String obscuringCharacter; - - /// {@macro flutter.widgets.editableText.obscureText} final bool obscureText; - - /// {@macro flutter.widgets.editableText.autocorrect} final bool autocorrect; - - /// {@macro flutter.services.TextInputConfiguration.smartDashesType} final SmartDashesType smartDashesType; - - /// {@macro flutter.services.TextInputConfiguration.smartQuotesType} final SmartQuotesType smartQuotesType; - - /// {@macro flutter.services.TextInputConfiguration.enableSuggestions} final bool enableSuggestions; - - /// {@macro flutter.widgets.editableText.maxLines} - /// * [expands], which determines whether the field should fill the height of - /// its parent. final int? maxLines; - - /// {@macro flutter.widgets.editableText.minLines} - /// * [expands], which determines whether the field should fill the height of - /// its parent. final int? minLines; - - /// {@macro flutter.widgets.editableText.expands} final bool expands; - - /// {@macro flutter.widgets.editableText.readOnly} final bool readOnly; - - /// Configuration of toolbar options. - /// - /// If not set, select all and paste will default to be enabled. Copy and cut - /// will be disabled if [obscureText] is true. If [readOnly] is true, - /// paste and cut will be disabled regardless. - - /// {@macro flutter.widgets.editableText.showCursor} final bool? showCursor; - - /// The maximum number of characters (Unicode grapheme clusters) to allow in - /// the text field. - /// - /// If set, a character counter will be displayed below the - /// field showing how many characters have been entered. If set to a number - /// greater than 0, it will also display the maximum number allowed. If set - /// to [MoonFormTextInput.noMaxLength] then only the current character count is displayed. - /// - /// After [maxLength] characters have been input, additional input - /// is ignored, unless [maxLengthEnforcement] is set to - /// [MaxLengthEnforcement.none]. - /// - /// The text field enforces the length with a [LengthLimitingTextInputFormatter], - /// which is evaluated after the supplied [inputFormatters], if any. - /// - /// This value must be either null, [MoonFormTextInput.noMaxLength], or greater than 0. - /// If null (the default) then there is no limit to the number of characters - /// that can be entered. If set to [MoonFormTextInput.noMaxLength], then no limit will - /// be enforced, but the number of characters entered will still be displayed. - /// - /// Whitespace characters (e.g. newline, space, tab) are included in the - /// character count. - /// - /// If [maxLengthEnforcement] is [MaxLengthEnforcement.none], then more than - /// [maxLength] characters may be entered, but the error counter and divider - /// will switch to the [decoration]'s [InputDecoration.errorStyle] when the - /// limit is exceeded. - /// - /// {@macro flutter.services.lengthLimitingTextInputFormatter.maxLength} final int? maxLength; - - /// Determines how the [maxLength] limit should be enforced. - /// - /// {@macro flutter.services.textFormatter.effectiveMaxLengthEnforcement} - /// - /// {@macro flutter.services.textFormatter.maxLengthEnforcement} final MaxLengthEnforcement? maxLengthEnforcement; - - /// {@macro flutter.widgets.editableText.onChanged} - /// - /// See also: - /// - /// * [inputFormatters], which are called before [onChanged] - /// runs and can validate and change ("format") the input value. - /// * [onEditingComplete], [onSubmitted]: - /// which are more specialized input change notifications. final ValueChanged? onChanged; - - /// {@macro flutter.widgets.editableText.onEditingComplete} final VoidCallback? onEditingComplete; - - /// {@macro flutter.widgets.editableText.onSubmitted} - /// - /// See also: - /// - /// * [TextInputAction.next] and [TextInputAction.previous], which - /// automatically shift the focus to the next/previous focusable item when - /// the user is done editing. final ValueChanged? onSubmitted; - - /// {@macro flutter.widgets.editableText.onAppPrivateCommand} final AppPrivateCommandCallback? onAppPrivateCommand; - - /// {@macro flutter.widgets.editableText.inputFormatters} final List? inputFormatters; - - /// If false the text field is "disabled": it ignores taps and its - /// [decoration] is rendered in grey. - /// - /// If non-null this property overrides the [decoration]'s - /// [InputDecoration.enabled] property. final bool enabled; - - /// {@macro flutter.widgets.editableText.cursorWidth} final double cursorWidth; - - /// {@macro flutter.widgets.editableText.cursorHeight} final double? cursorHeight; - - /// {@macro flutter.widgets.editableText.cursorRadius} final Radius? cursorRadius; - - /// {@macro flutter.widgets.editableText.cursorOpacityAnimates} final bool? cursorOpacityAnimates; - - /// The color of the cursor. - /// - /// The cursor indicates the current location of text insertion point in - /// the field. - /// - /// If this is null it will default to the ambient - /// [DefaultSelectionStyle.cursorColor]. If that is null, and the - /// [ThemeData.platform] is [TargetPlatform.iOS] or [TargetPlatform.macOS] - /// it will use [CupertinoThemeData.primaryColor]. Otherwise it will use - /// the value of [ColorScheme.primary] of [ThemeData.colorScheme]. final Color? cursorColor; - - /// Controls how tall the selection highlight boxes are computed to be. - /// - /// See [ui.BoxHeightStyle] for details on available styles. final ui.BoxHeightStyle selectionHeightStyle; - - /// Controls how wide the selection highlight boxes are computed to be. - /// - /// See [ui.BoxWidthStyle] for details on available styles. final ui.BoxWidthStyle selectionWidthStyle; - - /// The appearance of the keyboard. - /// - /// This setting is only honored on iOS devices. - /// - /// If unset, defaults to [ThemeData.brightness]. final Brightness? keyboardAppearance; - - /// {@macro flutter.widgets.editableText.scrollPadding} final EdgeInsets scrollPadding; - - /// {@macro flutter.widgets.editableText.enableInteractiveSelection} final bool enableInteractiveSelection; - - /// {@macro flutter.widgets.editableText.selectionControls} final TextSelectionControls? selectionControls; - - /// {@macro flutter.widgets.scrollable.dragStartBehavior} final DragStartBehavior dragStartBehavior; - - /// {@template flutter.material.textfield.onTap} - /// Called for each distinct tap except for every second tap of a double tap. - /// - /// The text field builds a [GestureDetector] to handle input events like tap, - /// to trigger focus requests, to move the caret, adjust the selection, etc. - /// Handling some of those events by wrapping the text field with a competing - /// GestureDetector is problematic. - /// - /// To unconditionally handle taps, without interfering with the text field's - /// internal gesture detector, provide this callback. - /// - /// If the text field is created with [enabled] false, taps will not be - /// recognized. - /// - /// To be notified when the text field gains or loses the focus, provide a - /// [focusNode] and add a listener to that. - /// - /// To listen to arbitrary pointer events without competing with the - /// text field's internal gesture detector, use a [Listener]. - /// {@endtemplate} final GestureTapCallback? onTap; - - /// {@macro flutter.widgets.editableText.onTapOutside} - /// - /// {@tool dartpad} - /// This example shows how to use a `TextFieldTapRegion` to wrap a set of - /// "spinner" buttons that increment and decrement a value in the [MoonFormTextInput] - /// without causing the text field to lose keyboard focus. - /// - /// This example includes a generic `SpinnerField` class that you can copy - /// into your own project and customize. - /// - /// ** See code in examples/api/lib/widgets/tap_region/text_field_tap_region.0.dart ** - /// {@end-tool} - /// - /// See also: - /// - /// * [TapRegion] for how the region group is determined. final TapRegionCallback? onTapOutside; - - /// The cursor for a mouse pointer when it enters or is hovering over the - /// widget. - /// - /// If [mouseCursor] is a [MaterialStateProperty], - /// [MaterialStateProperty.resolve] is used for the following [MaterialState]s: - /// - /// * [MaterialState.error]. - /// * [MaterialState.hovered]. - /// * [MaterialState.focused]. - /// * [MaterialState.disabled]. - /// - /// If this property is null, [MaterialStateMouseCursor.textable] will be used. - /// - /// The [mouseCursor] is the only property of [MoonFormTextInput] that controls the - /// appearance of the mouse pointer. All other properties related to "cursor" - /// stand for the text cursor, which is usually a blinking vertical line at - /// the editing position. final MouseCursor? mouseCursor; - - /// {@macro flutter.widgets.editableText.scrollPhysics} final ScrollPhysics? scrollPhysics; - - /// {@macro flutter.widgets.editableText.scrollController} final ScrollController? scrollController; - - /// {@macro flutter.widgets.editableText.autofillHints} - /// {@macro flutter.services.AutofillConfiguration.autofillHints} final Iterable? autofillHints; - - /// {@macro flutter.material.Material.clipBehavior} - /// - /// Defaults to [Clip.hardEdge]. final Clip clipBehavior; - - /// {@template flutter.material.textfield.restorationId} - /// Restoration ID to save and restore the state of the text field. - /// - /// If non-null, the text field will persist and restore its current scroll - /// offset and - if no [controller] has been provided - the content of the - /// text field. If a [controller] has been provided, it is the responsibility - /// of the owner of that controller to persist and restore it, e.g. by using - /// a [RestorableTextEditingController]. - /// - /// The state of this widget is persisted in a [RestorationBucket] claimed - /// from the surrounding [RestorationScope] using the provided restoration ID. - /// - /// See also: - /// - /// * [RestorationManager], which explains how state restoration works in - /// Flutter. - /// {@endtemplate} final String? restorationId; - - /// {@macro flutter.widgets.editableText.scribbleEnabled} final bool scribbleEnabled; - - /// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning} final bool enableIMEPersonalizedLearning; - - /// {@macro flutter.widgets.editableText.contentInsertionConfiguration} final ContentInsertionConfiguration? contentInsertionConfiguration; - - /// {@macro flutter.widgets.EditableText.contextMenuBuilder} - /// - /// If not provided, will build a default menu based on the platform. - /// - /// See also: - /// - /// * [AdaptiveTextSelectionToolbar], which is built by default. final EditableTextContextMenuBuilder? contextMenuBuilder; - - /// Determine whether this text field can request the primary focus. - /// - /// Defaults to true. If false, the text field will not request focus - /// when tapped, or when its context menu is displayed. If false it will not - /// be possible to move the focus to the text field with tab key. final bool canRequestFocus; - - /// {@macro flutter.widgets.undoHistory.controller} final UndoHistoryController? undoController; - - /// {@macro flutter.widgets.EditableText.spellCheckConfiguration} - /// - /// If [SpellCheckConfiguration.misspelledTextStyle] is not specified in this - /// configuration, then [materialMisspelledTextStyle] is used by default. final SpellCheckConfiguration? spellCheckConfiguration; - - /// An optional method to call with the final value when the form is saved via - /// [FormState.save]. final FormFieldSetter? onSaved; - - /// An optional method that validates an input. Returns an error string to - /// display if the input is invalid, or null otherwise. - /// - /// The returned value is exposed by the [FormFieldState.errorText] property. - /// The [MoonFormTextInput] uses this to override the [InputDecoration.errorText] - /// value. - /// - /// Alternating between error and normal state can cause the height of the - /// [MoonFormTextInput] to change if no other subtext decoration is set on the - /// field. To create a field whose height is fixed regardless of whether or - /// not an error is displayed, either wrap the [MoonFormTextInput] in a fixed - /// height parent like [SizedBox], or set the [InputDecoration.helperText] - /// parameter to a space. final FormFieldValidator? validator; - - /// Used to enable/disable this form field auto validation and update its - /// error text. - /// - /// {@template flutter.widgets.FormField.autovalidateMode} - /// If [AutovalidateMode.onUserInteraction], this FormField will only - /// auto-validate after its content changes. If [AutovalidateMode.always], it - /// will auto-validate even without user interaction. If - /// [AutovalidateMode.disabled], auto-validation will be disabled. - /// - /// Defaults to [AutovalidateMode.disabled], cannot be null. - /// {@endtemplate} final AutovalidateMode autovalidateMode; - /// Creates a [FormField] that contains a [MoonTextInput]. - /// - /// When a [controller] is specified, [initialValue] must be null (the - /// default). If [controller] is null, then a [TextEditingController] - /// will be constructed automatically and its `text` will be initialized - /// to [initialValue] or the empty string. - const MoonFormTextInput({ + const MoonFormTextInputConfiguration({ //MDS props this.hasFloatingLabel = false, this.borderRadius, @@ -600,9 +557,7 @@ class MoonFormTextInput extends StatefulWidget { this.leading, this.trailing, this.helper, - ///////////////// // Flutter props - super.key, this.controller, this.focusNode, this.undoController, @@ -657,247 +612,15 @@ class MoonFormTextInput extends StatefulWidget { this.restorationId, this.scribbleEnabled = true, this.enableIMEPersonalizedLearning = true, - this.contextMenuBuilder = _defaultContextMenuBuilder, + this.contextMenuBuilder = MoonFormTextInput.defaultContextMenuBuilder, this.canRequestFocus = true, this.spellCheckConfiguration, this.magnifierConfiguration, this.onSaved, this.validator, this.autovalidateMode = AutovalidateMode.disabled, - }) : assert(obscuringCharacter.length == 1), - smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled), + }) : smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled), smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled), - assert(maxLines == null || maxLines > 0), - assert(minLines == null || minLines > 0), - assert( - (maxLines == null) || (minLines == null) || (maxLines >= minLines), - "minLines can't be greater than maxLines", - ), - assert( - !expands || (maxLines == null && minLines == null), - 'minLines and maxLines must be null when expands is true.', - ), - assert(!obscureText || maxLines == 1, 'Obscured fields cannot be multiline.'), - assert(maxLength == null || maxLength == MoonFormTextInput.noMaxLength || maxLength > 0), - // Assert the following instead of setting it directly to avoid surprising the user by silently changing the value they set. - assert( - !identical(textInputAction, TextInputAction.newline) || - maxLines == 1 || - !identical(keyboardType, TextInputType.text), - 'Use keyboardType TextInputType.multiline when using TextInputAction.newline on a multiline MoonFormTextInput.', - ), keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline), enableInteractiveSelection = enableInteractiveSelection ?? (!readOnly || !obscureText); - - /// {@macro flutter.widgets.editableText.selectionEnabled} - bool get selectionEnabled => enableInteractiveSelection; - - @override - State createState() => _MoonTextInputState(); - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(DiagnosticsProperty('controller', controller, defaultValue: null)); - properties.add(DiagnosticsProperty('focusNode', focusNode, defaultValue: null)); - properties.add(DiagnosticsProperty('undoController', undoController, defaultValue: null)); - properties.add(DiagnosticsProperty('enabled', enabled, defaultValue: null)); - properties.add(DiagnosticsProperty('keyboardType', keyboardType, defaultValue: TextInputType.text)); - properties.add(DiagnosticsProperty('style', style, defaultValue: null)); - properties.add(DiagnosticsProperty('autofocus', autofocus, defaultValue: false)); - properties.add(DiagnosticsProperty('obscuringCharacter', obscuringCharacter, defaultValue: '•')); - properties.add(DiagnosticsProperty('obscureText', obscureText, defaultValue: false)); - properties.add(DiagnosticsProperty('autocorrect', autocorrect, defaultValue: true)); - properties.add( - EnumProperty( - 'smartDashesType', - smartDashesType, - defaultValue: obscureText ? SmartDashesType.disabled : SmartDashesType.enabled, - ), - ); - properties.add( - EnumProperty( - 'smartQuotesType', - smartQuotesType, - defaultValue: obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled, - ), - ); - properties.add(DiagnosticsProperty('enableSuggestions', enableSuggestions, defaultValue: true)); - properties.add(IntProperty('maxLines', maxLines, defaultValue: 1)); - properties.add(IntProperty('minLines', minLines, defaultValue: null)); - properties.add(DiagnosticsProperty('expands', expands, defaultValue: false)); - properties.add(IntProperty('maxLength', maxLength, defaultValue: null)); - properties.add( - EnumProperty('maxLengthEnforcement', maxLengthEnforcement, defaultValue: null), - ); - properties.add(EnumProperty('textInputAction', textInputAction, defaultValue: null)); - properties.add( - EnumProperty('textCapitalization', textCapitalization, defaultValue: TextCapitalization.none), - ); - properties.add(EnumProperty('textAlign', textAlign, defaultValue: TextAlign.start)); - properties.add(DiagnosticsProperty('textAlignVertical', textAlignVertical, defaultValue: null)); - properties.add(EnumProperty('textDirection', textDirection, defaultValue: null)); - properties.add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0)); - properties.add(DoubleProperty('cursorHeight', cursorHeight, defaultValue: null)); - properties.add(DiagnosticsProperty('cursorRadius', cursorRadius, defaultValue: null)); - properties.add(DiagnosticsProperty('cursorOpacityAnimates', cursorOpacityAnimates, defaultValue: null)); - properties.add(ColorProperty('cursorColor', cursorColor, defaultValue: null)); - properties.add(DiagnosticsProperty('keyboardAppearance', keyboardAppearance, defaultValue: null)); - properties.add( - DiagnosticsProperty('scrollPadding', scrollPadding, defaultValue: const EdgeInsets.all(20.0)), - ); - properties.add( - FlagProperty('selectionEnabled', value: selectionEnabled, defaultValue: true, ifFalse: 'selection disabled'), - ); - properties.add( - DiagnosticsProperty('selectionControls', selectionControls, defaultValue: null), - ); - properties.add(DiagnosticsProperty('scrollController', scrollController, defaultValue: null)); - properties.add(DiagnosticsProperty('scrollPhysics', scrollPhysics, defaultValue: null)); - properties.add(DiagnosticsProperty('clipBehavior', clipBehavior, defaultValue: Clip.hardEdge)); - properties.add(DiagnosticsProperty('scribbleEnabled', scribbleEnabled, defaultValue: true)); - properties.add( - DiagnosticsProperty('enableIMEPersonalizedLearning', enableIMEPersonalizedLearning, defaultValue: true), - ); - properties.add( - DiagnosticsProperty( - 'spellCheckConfiguration', - spellCheckConfiguration, - defaultValue: null, - ), - ); - properties.add( - DiagnosticsProperty>( - 'contentCommitMimeTypes', - contentInsertionConfiguration?.allowedMimeTypes ?? const [], - defaultValue: contentInsertionConfiguration == null ? const [] : kDefaultContentInsertionMimeTypes, - ), - ); - } -} - -class _MoonTextInputState extends State { - @override - Widget build(BuildContext context) { - assert(debugCheckHasMaterial(context)); - assert(debugCheckHasMaterialLocalizations(context)); - assert(debugCheckHasDirectionality(context)); - assert( - !(widget.style != null && - widget.style!.inherit == false && - (widget.style!.fontSize == null || widget.style!.textBaseline == null)), - 'inherit false style must supply fontSize and textBaseline', - ); - - return Builder( - builder: (context) { - return FormField( - autovalidateMode: widget.autovalidateMode, - enabled: widget.enabled, - initialValue: widget.controller != null ? widget.controller!.text : (widget.initialValue ?? ""), - onSaved: widget.onSaved, - restorationId: widget.restorationId, - validator: widget.validator, - builder: (FormFieldState field) { - if (field.hasError) { - widget.onError?.call(field.errorText); - } else { - widget.onError?.call(null); - } - - void onChangedHandler(String value) { - field.didChange(value); - widget.onChanged?.call(value); - } - - return MoonTextInput( - activeBorderColor: widget.activeBorderColor, - autocorrect: widget.autocorrect, - autofillHints: widget.autofillHints, - autofocus: widget.autofocus, - backgroundColor: widget.backgroundColor, - borderRadius: widget.borderRadius, - canRequestFocus: widget.canRequestFocus, - clipBehavior: widget.clipBehavior, - contentInsertionConfiguration: widget.contentInsertionConfiguration, - contextMenuBuilder: widget.contextMenuBuilder, - controller: widget.controller, - cursorColor: widget.cursorColor, - cursorHeight: widget.cursorHeight, - cursorOpacityAnimates: widget.cursorOpacityAnimates, - cursorRadius: widget.cursorRadius, - cursorWidth: widget.cursorWidth, - decoration: widget.decoration, - dragStartBehavior: widget.dragStartBehavior, - enabled: widget.enabled, - enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, - enableInteractiveSelection: widget.enableInteractiveSelection, - enableSuggestions: widget.enableSuggestions, - errorBuilder: widget.errorBuilder, - errorColor: widget.errorColor, - errorText: field.errorText, - expands: widget.expands, - focusNode: widget.focusNode, - gap: widget.gap, - hasFloatingLabel: widget.hasFloatingLabel, - height: widget.height, - helper: widget.helper, - helperPadding: widget.helperPadding, - helperTextStyle: widget.helperTextStyle, - hintText: widget.hintText, - hintTextColor: widget.hintTextColor, - hoverBorderColor: widget.hoverBorderColor, - inactiveBorderColor: widget.inactiveBorderColor, - initialValue: widget.initialValue, - inputFormatters: widget.inputFormatters, - keyboardAppearance: widget.keyboardAppearance, - keyboardType: widget.keyboardType, - leading: widget.leading, - magnifierConfiguration: widget.magnifierConfiguration, - maxLength: widget.maxLength, - maxLengthEnforcement: widget.maxLengthEnforcement, - maxLines: widget.maxLines, - minLines: widget.minLines, - mouseCursor: widget.mouseCursor, - obscureText: widget.obscureText, - obscuringCharacter: widget.obscuringCharacter, - onAppPrivateCommand: widget.onAppPrivateCommand, - onChanged: onChangedHandler, - onEditingComplete: widget.onEditingComplete, - onSubmitted: widget.onSubmitted, - onTap: widget.onTap, - onTapOutside: widget.onTapOutside, - padding: widget.padding, - readOnly: widget.readOnly, - restorationId: widget.restorationId, - scribbleEnabled: widget.scribbleEnabled, - scrollController: widget.scrollController, - scrollPadding: widget.scrollPadding, - scrollPhysics: widget.scrollPhysics, - selectionControls: widget.selectionControls, - selectionHeightStyle: widget.selectionHeightStyle, - selectionWidthStyle: widget.selectionWidthStyle, - showCursor: widget.showCursor, - smartDashesType: widget.smartDashesType, - smartQuotesType: widget.smartQuotesType, - spellCheckConfiguration: widget.spellCheckConfiguration, - strutStyle: widget.strutStyle, - style: widget.style, - textAlign: widget.textAlign, - textAlignVertical: widget.textAlignVertical, - textCapitalization: widget.textCapitalization, - textColor: widget.textColor, - textDirection: widget.textDirection, - textInputAction: widget.textInputAction, - textInputSize: widget.textInputSize, - trailing: widget.trailing, - transitionCurve: widget.transitionCurve, - transitionDuration: widget.transitionDuration, - undoController: widget.undoController, - ); - }, - ); - }, - ); - } } diff --git a/lib/src/widgets/text_input/text_input.dart b/lib/src/widgets/text_input/text_input.dart index 258f676b..25b0cc0d 100644 --- a/lib/src/widgets/text_input/text_input.dart +++ b/lib/src/widgets/text_input/text_input.dart @@ -777,7 +777,9 @@ class _MoonTextInputState extends State bool get _isEnabled => widget.enabled; - bool get _hasError => _hasIntrinsicError; + bool get _hasError => _hasIntrinsicError || widget.errorText != null; + + bool get _hasFocus => _effectiveFocusNode.hasFocus; int get _currentLength => _effectiveController.value.text.characters.length; @@ -810,7 +812,7 @@ class _MoonTextInputState extends State return { if (!_isEnabled) MaterialState.disabled, if (_isHovering) MaterialState.hovered, - if (_effectiveFocusNode.hasFocus) MaterialState.focused, + if (_hasFocus) MaterialState.focused, if (_hasError) MaterialState.error, }; } @@ -883,6 +885,8 @@ class _MoonTextInputState extends State } void _handleHover(bool hovering) { + if (!_isEnabled) return; + if (hovering != _isHovering) { setState(() => _isHovering = hovering); } @@ -973,7 +977,7 @@ class _MoonTextInputState extends State _effectiveFocusNode.canRequestFocus = _canRequestFocus; - if (_effectiveFocusNode.hasFocus && widget.readOnly != oldWidget.readOnly && _isEnabled) { + if (_hasFocus && widget.readOnly != oldWidget.readOnly && _isEnabled) { if (_effectiveController.selection.isCollapsed) { _showSelectionHandles = !widget.readOnly; } @@ -1069,8 +1073,16 @@ class _MoonTextInputState extends State final MoonSquircleBorder defaultBorder = MoonSquircleBorder( borderRadius: effectiveBorderRadius.squircleBorderRadius(context), side: BorderSide( - color: _isHovering ? effectiveHoverBorderColor : effectiveInactiveBorderColor, - width: _isHovering ? MoonBorders.borders.activeBorderWidth : MoonBorders.borders.defaultBorderWidth, + color: effectiveInactiveBorderColor, + width: MoonBorders.borders.defaultBorderWidth, + ), + ); + + final MoonSquircleBorder hoverBorder = MoonSquircleBorder( + borderRadius: effectiveBorderRadius.squircleBorderRadius(context), + side: BorderSide( + color: effectiveHoverBorderColor, + width: MoonBorders.borders.activeBorderWidth, ), ); @@ -1090,11 +1102,13 @@ class _MoonTextInputState extends State ), ); - final MoonSquircleBorder resolvedBorder = widget.errorText != null && !widget.readOnly + final MoonSquircleBorder resolvedBorder = _hasError ? errorBorder - : _effectiveFocusNode.hasFocus && !widget.readOnly + : _hasFocus ? focusBorder - : defaultBorder; + : _isHovering + ? hoverBorder + : defaultBorder; bool? cursorOpacityAnimates = widget.cursorOpacityAnimates; Color? autocorrectionTextRectColor; @@ -1168,7 +1182,7 @@ class _MoonTextInputState extends State cursorOffset = Offset(iOSHorizontalOffset / MediaQuery.devicePixelRatioOf(context), 0); handleDidGainAccessibilityFocus = () { // Automatically activate the MoonTextInput when it receives accessibility focus. - if (!_effectiveFocusNode.hasFocus && _effectiveFocusNode.canRequestFocus) { + if (!_hasFocus && _effectiveFocusNode.canRequestFocus) { _effectiveFocusNode.requestFocus(); } }; @@ -1196,7 +1210,7 @@ class _MoonTextInputState extends State selectionColor = selectionStyle.selectionColor ?? theme.colorScheme.primary.withOpacity(0.40); handleDidGainAccessibilityFocus = () { // Automatically activate the MoonTextInput when it receives accessibility focus. - if (!_effectiveFocusNode.hasFocus && _effectiveFocusNode.canRequestFocus) { + if (!_hasFocus && _effectiveFocusNode.canRequestFocus) { _effectiveFocusNode.requestFocus(); } }; @@ -1234,8 +1248,7 @@ class _MoonTextInputState extends State magnifierConfiguration: widget.magnifierConfiguration ?? TextMagnifier.adaptiveMagnifierConfiguration, maxLines: widget.maxLines, minLines: widget.minLines, - mouseCursor: MouseCursor.defer, - // MoonTextInput will handle the cursor + mouseCursor: MouseCursor.defer, // MoonTextInput will handle the cursor obscureText: widget.obscureText, obscuringCharacter: widget.obscuringCharacter, onAppPrivateCommand: widget.onAppPrivateCommand, @@ -1276,13 +1289,13 @@ class _MoonTextInputState extends State child = AnimatedBuilder( animation: Listenable.merge([focusNode, controller]), builder: (BuildContext context, Widget? child) { - return Container( + return _BorderContainer( + backgroundColor: effectiveBackgroundColor, + border: resolvedBorder, + decoration: widget.decoration, height: widget.keyboardType == TextInputType.multiline && widget.height == null ? null : effectiveHeight, - decoration: widget.decoration ?? - ShapeDecorationWithPremultipliedAlpha( - color: effectiveBackgroundColor, - shape: resolvedBorder, - ), + duration: effectiveTransitionDuration, + curve: effectiveTransitionCurve, child: Row( children: [ if (widget.leading != null) @@ -1488,3 +1501,91 @@ class _MoonTextInputSelectionGestureDetectorBuilder extends TextSelectionGesture } } } + +class _BorderContainer extends StatefulWidget { + final Color backgroundColor; + final Decoration? decoration; + final double? height; + final ShapeBorder border; + final Duration duration; + final Curve curve; + final Widget child; + + const _BorderContainer({ + required this.backgroundColor, + this.decoration, + required this.height, + required this.border, + required this.duration, + required this.curve, + required this.child, + }); + + @override + _BorderContainerState createState() => _BorderContainerState(); +} + +class _BorderContainerState extends State<_BorderContainer> with TickerProviderStateMixin { + late AnimationController _controller; + late Animation _borderAnimation; + late ShapeBorderTween _border; + + @override + void initState() { + super.initState(); + + _controller = AnimationController( + duration: widget.duration, + vsync: this, + ); + _borderAnimation = CurvedAnimation( + parent: _controller, + curve: widget.curve, + reverseCurve: widget.curve.flipped, + ); + _border = ShapeBorderTween( + begin: widget.border, + end: widget.border, + ); + } + + @override + void dispose() { + _controller.dispose(); + + super.dispose(); + } + + @override + void didUpdateWidget(_BorderContainer oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.border != oldWidget.border) { + _border = ShapeBorderTween( + begin: oldWidget.border, + end: widget.border, + ); + _controller + ..value = 0.0 + ..forward(); + } + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _borderAnimation, + builder: (context, child) { + return Container( + height: widget.height, + decoration: widget.decoration ?? + ShapeDecorationWithPremultipliedAlpha( + color: widget.backgroundColor, + shape: _border.evaluate(_borderAnimation)!, + ), + child: child, + ); + }, + child: widget.child, + ); + } +} diff --git a/lib/src/widgets/text_input_group/text_input_group.dart b/lib/src/widgets/text_input_group/text_input_group.dart index 598b6c77..59f631cf 100644 --- a/lib/src/widgets/text_input_group/text_input_group.dart +++ b/lib/src/widgets/text_input_group/text_input_group.dart @@ -173,91 +173,91 @@ class _MoonTextInputGroupState extends State { final int derivedIndex = index ~/ 2; final MoonFormTextInput child = MoonFormTextInput( - activeBorderColor: widget.children[derivedIndex].activeBorderColor, - autocorrect: widget.children[derivedIndex].autocorrect, - autofillHints: widget.children[derivedIndex].autofillHints, - autofocus: widget.children[derivedIndex].autofocus, + activeBorderColor: widget.children[derivedIndex].configuration.activeBorderColor, + autocorrect: widget.children[derivedIndex].configuration.autocorrect, + autofillHints: widget.children[derivedIndex].configuration.autofillHints, + autofocus: widget.children[derivedIndex].configuration.autofocus, autovalidateMode: widget.autovalidateMode, backgroundColor: Colors.transparent, - borderRadius: widget.children[derivedIndex].borderRadius, - canRequestFocus: widget.children[derivedIndex].canRequestFocus, - clipBehavior: widget.children[derivedIndex].clipBehavior, - contentInsertionConfiguration: widget.children[derivedIndex].contentInsertionConfiguration, - contextMenuBuilder: widget.children[derivedIndex].contextMenuBuilder, - controller: widget.children[derivedIndex].controller, - cursorColor: widget.children[derivedIndex].cursorColor, - cursorHeight: widget.children[derivedIndex].cursorHeight, - cursorOpacityAnimates: widget.children[derivedIndex].cursorOpacityAnimates, - cursorRadius: widget.children[derivedIndex].cursorRadius, - cursorWidth: widget.children[derivedIndex].cursorWidth, - decoration: widget.children[derivedIndex].decoration, - dragStartBehavior: widget.children[derivedIndex].dragStartBehavior, - enabled: widget.children[derivedIndex].enabled, - enableIMEPersonalizedLearning: widget.children[derivedIndex].enableIMEPersonalizedLearning, - enableInteractiveSelection: widget.children[derivedIndex].enableInteractiveSelection, - enableSuggestions: widget.children[derivedIndex].enableSuggestions, + borderRadius: widget.children[derivedIndex].configuration.borderRadius, + canRequestFocus: widget.children[derivedIndex].configuration.canRequestFocus, + clipBehavior: widget.children[derivedIndex].configuration.clipBehavior, + contentInsertionConfiguration: widget.children[derivedIndex].configuration.contentInsertionConfiguration, + contextMenuBuilder: widget.children[derivedIndex].configuration.contextMenuBuilder, + //controller: widget.children[derivedIndex].configuration.controller, + cursorColor: widget.children[derivedIndex].configuration.cursorColor, + cursorHeight: widget.children[derivedIndex].configuration.cursorHeight, + cursorOpacityAnimates: widget.children[derivedIndex].configuration.cursorOpacityAnimates, + cursorRadius: widget.children[derivedIndex].configuration.cursorRadius, + cursorWidth: widget.children[derivedIndex].configuration.cursorWidth, + decoration: widget.children[derivedIndex].configuration.decoration, + dragStartBehavior: widget.children[derivedIndex].configuration.dragStartBehavior, + enabled: widget.children[derivedIndex].configuration.enabled, + enableIMEPersonalizedLearning: widget.children[derivedIndex].configuration.enableIMEPersonalizedLearning, + enableInteractiveSelection: widget.children[derivedIndex].configuration.enableInteractiveSelection, + enableSuggestions: widget.children[derivedIndex].configuration.enableSuggestions, errorColor: _validatorErrors.length == widget.children.length ? Colors.transparent - : widget.children[derivedIndex].errorColor, - expands: widget.children[derivedIndex].expands, - focusNode: widget.children[derivedIndex].focusNode, - gap: widget.children[derivedIndex].gap, - hasFloatingLabel: widget.children[derivedIndex].hasFloatingLabel, - height: widget.children[derivedIndex].height, - helper: widget.children[derivedIndex].helper, - helperPadding: widget.children[derivedIndex].helperPadding, - helperTextStyle: widget.children[derivedIndex].helperTextStyle, - hintText: widget.children[derivedIndex].hintText, - hintTextColor: widget.children[derivedIndex].hintTextColor, - hoverBorderColor: widget.children[derivedIndex].hoverBorderColor, + : widget.children[derivedIndex].configuration.errorColor, + expands: widget.children[derivedIndex].configuration.expands, + focusNode: widget.children[derivedIndex].configuration.focusNode, + gap: widget.children[derivedIndex].configuration.gap, + hasFloatingLabel: widget.children[derivedIndex].configuration.hasFloatingLabel, + height: widget.children[derivedIndex].configuration.height, + helper: widget.children[derivedIndex].configuration.helper, + helperPadding: widget.children[derivedIndex].configuration.helperPadding, + helperTextStyle: widget.children[derivedIndex].configuration.helperTextStyle, + hintText: widget.children[derivedIndex].configuration.hintText, + hintTextColor: widget.children[derivedIndex].configuration.hintTextColor, + hoverBorderColor: widget.children[derivedIndex].configuration.hoverBorderColor, inactiveBorderColor: Colors.transparent, initialValue: widget.children[derivedIndex].initialValue, - inputFormatters: widget.children[derivedIndex].inputFormatters, - keyboardAppearance: widget.children[derivedIndex].keyboardAppearance, - keyboardType: widget.children[derivedIndex].keyboardType, - leading: widget.children[derivedIndex].leading, - magnifierConfiguration: widget.children[derivedIndex].magnifierConfiguration, - maxLength: widget.children[derivedIndex].maxLength, - maxLengthEnforcement: widget.children[derivedIndex].maxLengthEnforcement, - maxLines: widget.children[derivedIndex].maxLines, - minLines: widget.children[derivedIndex].minLines, - mouseCursor: widget.children[derivedIndex].mouseCursor, - obscureText: widget.children[derivedIndex].obscureText, - obscuringCharacter: widget.children[derivedIndex].obscuringCharacter, - onAppPrivateCommand: widget.children[derivedIndex].onAppPrivateCommand, - onChanged: widget.children[derivedIndex].onChanged, - onEditingComplete: widget.children[derivedIndex].onEditingComplete, + inputFormatters: widget.children[derivedIndex].configuration.inputFormatters, + keyboardAppearance: widget.children[derivedIndex].configuration.keyboardAppearance, + keyboardType: widget.children[derivedIndex].configuration.keyboardType, + leading: widget.children[derivedIndex].configuration.leading, + magnifierConfiguration: widget.children[derivedIndex].configuration.magnifierConfiguration, + maxLength: widget.children[derivedIndex].configuration.maxLength, + maxLengthEnforcement: widget.children[derivedIndex].configuration.maxLengthEnforcement, + maxLines: widget.children[derivedIndex].configuration.maxLines, + minLines: widget.children[derivedIndex].configuration.minLines, + mouseCursor: widget.children[derivedIndex].configuration.mouseCursor, + obscureText: widget.children[derivedIndex].configuration.obscureText, + obscuringCharacter: widget.children[derivedIndex].configuration.obscuringCharacter, + onAppPrivateCommand: widget.children[derivedIndex].configuration.onAppPrivateCommand, + onChanged: widget.children[derivedIndex].configuration.onChanged, + onEditingComplete: widget.children[derivedIndex].configuration.onEditingComplete, onError: _handleValidationError, - onSubmitted: widget.children[derivedIndex].onSubmitted, - onTap: widget.children[derivedIndex].onTap, - onTapOutside: widget.children[derivedIndex].onTapOutside, - padding: widget.children[derivedIndex].padding, - readOnly: widget.children[derivedIndex].readOnly, - restorationId: widget.children[derivedIndex].restorationId, - scribbleEnabled: widget.children[derivedIndex].scribbleEnabled, - scrollController: widget.children[derivedIndex].scrollController, - scrollPadding: widget.children[derivedIndex].scrollPadding, - scrollPhysics: widget.children[derivedIndex].scrollPhysics, - selectionControls: widget.children[derivedIndex].selectionControls, - selectionHeightStyle: widget.children[derivedIndex].selectionHeightStyle, - selectionWidthStyle: widget.children[derivedIndex].selectionWidthStyle, - showCursor: widget.children[derivedIndex].showCursor, - smartDashesType: widget.children[derivedIndex].smartDashesType, - smartQuotesType: widget.children[derivedIndex].smartQuotesType, - spellCheckConfiguration: widget.children[derivedIndex].spellCheckConfiguration, - strutStyle: widget.children[derivedIndex].strutStyle, - style: widget.children[derivedIndex].style, - textAlign: widget.children[derivedIndex].textAlign, - textAlignVertical: widget.children[derivedIndex].textAlignVertical, - textCapitalization: widget.children[derivedIndex].textCapitalization, - textColor: widget.children[derivedIndex].textColor, - textDirection: widget.children[derivedIndex].textDirection, - textInputAction: widget.children[derivedIndex].textInputAction, - textInputSize: widget.children[derivedIndex].textInputSize, - trailing: widget.children[derivedIndex].trailing, - transitionCurve: widget.children[derivedIndex].transitionCurve, - transitionDuration: widget.children[derivedIndex].transitionDuration, - undoController: widget.children[derivedIndex].undoController, + onSubmitted: widget.children[derivedIndex].configuration.onSubmitted, + onTap: widget.children[derivedIndex].configuration.onTap, + onTapOutside: widget.children[derivedIndex].configuration.onTapOutside, + padding: widget.children[derivedIndex].configuration.padding, + readOnly: widget.children[derivedIndex].configuration.readOnly, + restorationId: widget.children[derivedIndex].configuration.restorationId, + scribbleEnabled: widget.children[derivedIndex].configuration.scribbleEnabled, + scrollController: widget.children[derivedIndex].configuration.scrollController, + scrollPadding: widget.children[derivedIndex].configuration.scrollPadding, + scrollPhysics: widget.children[derivedIndex].configuration.scrollPhysics, + selectionControls: widget.children[derivedIndex].configuration.selectionControls, + selectionHeightStyle: widget.children[derivedIndex].configuration.selectionHeightStyle, + selectionWidthStyle: widget.children[derivedIndex].configuration.selectionWidthStyle, + showCursor: widget.children[derivedIndex].configuration.showCursor, + smartDashesType: widget.children[derivedIndex].configuration.smartDashesType, + smartQuotesType: widget.children[derivedIndex].configuration.smartQuotesType, + spellCheckConfiguration: widget.children[derivedIndex].configuration.spellCheckConfiguration, + strutStyle: widget.children[derivedIndex].configuration.strutStyle, + style: widget.children[derivedIndex].configuration.style, + textAlign: widget.children[derivedIndex].configuration.textAlign, + textAlignVertical: widget.children[derivedIndex].configuration.textAlignVertical, + textCapitalization: widget.children[derivedIndex].configuration.textCapitalization, + textColor: widget.children[derivedIndex].configuration.textColor, + textDirection: widget.children[derivedIndex].configuration.textDirection, + textInputAction: widget.children[derivedIndex].configuration.textInputAction, + textInputSize: widget.children[derivedIndex].configuration.textInputSize, + trailing: widget.children[derivedIndex].configuration.trailing, + transitionCurve: widget.children[derivedIndex].configuration.transitionCurve, + transitionDuration: widget.children[derivedIndex].configuration.transitionDuration, + undoController: widget.children[derivedIndex].configuration.undoController, validator: widget.children[derivedIndex].validator, );