diff --git a/docs/pages/docs/text-field.mdx b/docs/pages/docs/text-field.mdx index a413b4637..61e25b12f 100644 --- a/docs/pages/docs/text-field.mdx +++ b/docs/pages/docs/text-field.mdx @@ -168,16 +168,22 @@ FTextField.multiline( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - FTextField.email( - hint: 'janedoe@foruslabs.com', - help: const Text(''), - validator: (value) => (value?.contains('@') ?? false) ? null : 'Please enter a valid email.', + SizedBox( + height: 110, + child: FTextField.email( + hint: 'janedoe@foruslabs.com', + autovalidateMode: AutovalidateMode.onUserInteraction, + validator: (value) => (value?.contains('@') ?? false) ? null : 'Please enter a valid email.', + ), ), const SizedBox(height: 4), - FTextField.password( - hint: '', - help: const Text(''), - validator: (value) => 8 <= (value?.length ?? 0) ? null : 'Password must be at least 8 characters long.', + SizedBox( + height: 110, + child: FTextField.password( + hint: '', + autovalidateMode: AutovalidateMode.onUserInteraction, + validator: (value) => 8 <= (value?.length ?? 0) ? null : 'Password must be at least 8 characters long.', + ), ), const SizedBox(height: 30), FButton( diff --git a/forui/CHANGELOG.md b/forui/CHANGELOG.md index dda40f364..8d9b3cce7 100644 --- a/forui/CHANGELOG.md +++ b/forui/CHANGELOG.md @@ -7,8 +7,14 @@ * Change `FTextFieldStyle` to inherit from `FFormFieldStyle`. +* Change `FTextField` to display error under description instead of replacing it. + * **Breaking:** Remove `FTextField.error`. +* **Breaking:** Change `FTextField.help` to `FTextField.description`. + +* **Breaking:** Change how `FTextFieldStyle` stores various state-dependent styles. + ### Fixes * Fix `FTextField` not changing error text color when an error occurs. diff --git a/forui/lib/src/widgets/text_field/text_field.dart b/forui/lib/src/widgets/text_field/text_field.dart index b2c32a2d9..8829ea4fd 100644 --- a/forui/lib/src/widgets/text_field/text_field.dart +++ b/forui/lib/src/widgets/text_field/text_field.dart @@ -21,7 +21,6 @@ part 'text_form_field.dart'; /// See: /// * https://forui.dev/docs/text-field for working examples. /// * [FTextFieldStyle] for customizing a text field's appearance. -/// * [_Field] for a text field that integrates with a [Form]. /// * [TextField] for more details about working with a text field. final class FTextField extends StatelessWidget { static Widget _contextMenuBuilder( @@ -457,10 +456,9 @@ final class FTextField extends StatelessWidget { /// The returned value is exposed by the [FormFieldState.errorText] property. It transforms the text using /// [errorBuilder]. /// - /// Alternating between error and normal state can cause the height of the [_Field] to change if no other + /// Alternating between error and normal state can cause the height of the [FTextField] 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 [_Field] in a fixed height parent like [SizedBox], or set the [description] - /// parameter to a space. + /// error is displayed, wrap the [FTextField] in a fixed height parent like [SizedBox]. final FormFieldValidator? validator; /// An optional value to initialize the form field to, or null otherwise. diff --git a/forui/lib/src/widgets/text_field/text_field_state_style.dart b/forui/lib/src/widgets/text_field/text_field_state_style.dart index cecf12788..e55ebbcfd 100644 --- a/forui/lib/src/widgets/text_field/text_field_state_style.dart +++ b/forui/lib/src/widgets/text_field/text_field_state_style.dart @@ -149,6 +149,11 @@ final class FTextFieldErrorStyle extends FTextFieldStateStyle { /// The error's [TextStyle]. final TextStyle errorTextStyle; + /// The duration of the error fade-in animation. + /// + /// Defaults to 100 milliseconds. + final Duration animationDuration; + /// Creates a [FTextFieldErrorStyle]. FTextFieldErrorStyle({ required this.errorTextStyle, @@ -158,6 +163,7 @@ final class FTextFieldErrorStyle extends FTextFieldStateStyle { required super.descriptionTextStyle, required super.focusedStyle, required super.unfocusedStyle, + this.animationDuration = const Duration(milliseconds: 100), }); /// Creates a [FTextFieldErrorStyle] that inherits its properties. @@ -170,6 +176,7 @@ final class FTextFieldErrorStyle extends FTextFieldStateStyle { required super.typography, required super.style, }) : errorTextStyle = formFieldErrorStyle.errorTextStyle, + animationDuration = const Duration(milliseconds: 100), super.inherit(formFieldStateStyle: formFieldErrorStyle); /// Returns a copy of this [FTextFieldStateStyle] with the given properties replaced. @@ -197,6 +204,7 @@ final class FTextFieldErrorStyle extends FTextFieldStateStyle { TextStyle? descriptionTextStyle, FTextFieldBorderStyle? focusedStyle, FTextFieldBorderStyle? unfocusedStyle, + Duration? animationDuration, }) => FTextFieldErrorStyle( errorTextStyle: errorTextStyle ?? this.errorTextStyle, @@ -206,12 +214,15 @@ final class FTextFieldErrorStyle extends FTextFieldStateStyle { descriptionTextStyle: descriptionTextStyle ?? this.descriptionTextStyle, focusedStyle: focusedStyle ?? this.focusedStyle, unfocusedStyle: unfocusedStyle ?? this.unfocusedStyle, + animationDuration: animationDuration ?? this.animationDuration, ); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); - properties.add(DiagnosticsProperty('errorTextStyle', errorTextStyle)); + properties + ..add(DiagnosticsProperty('errorTextStyle', errorTextStyle)) + ..add(DiagnosticsProperty('animationDuration', animationDuration)); } @override @@ -225,7 +236,8 @@ final class FTextFieldErrorStyle extends FTextFieldStateStyle { descriptionTextStyle == other.descriptionTextStyle && focusedStyle == other.focusedStyle && unfocusedStyle == other.unfocusedStyle && - errorTextStyle == other.errorTextStyle; + errorTextStyle == other.errorTextStyle && + animationDuration == other.animationDuration; @override int get hashCode => @@ -235,7 +247,8 @@ final class FTextFieldErrorStyle extends FTextFieldStateStyle { descriptionTextStyle.hashCode ^ focusedStyle.hashCode ^ unfocusedStyle.hashCode ^ - errorTextStyle.hashCode; + errorTextStyle.hashCode ^ + animationDuration.hashCode; } /// A [FTextField] border's style. diff --git a/forui/lib/src/widgets/text_field/text_field_style.dart b/forui/lib/src/widgets/text_field/text_field_style.dart index 2502aca63..9dfe2d0e5 100644 --- a/forui/lib/src/widgets/text_field/text_field_style.dart +++ b/forui/lib/src/widgets/text_field/text_field_style.dart @@ -27,6 +27,8 @@ final class FTextFieldStyle with Diagnosticable { /// after the scroll. final EdgeInsets scrollPadding; + + /// The style when this text field is enabled. final FTextFieldNormalStyle enabledStyle; diff --git a/forui/lib/src/widgets/text_field/text_form_field.dart b/forui/lib/src/widgets/text_field/text_form_field.dart index 076136b3b..9fc75a307 100644 --- a/forui/lib/src/widgets/text_field/text_form_field.dart +++ b/forui/lib/src/widgets/text_field/text_form_field.dart @@ -6,8 +6,7 @@ class _Field extends FormField { FTextField parent, FTextFieldStyle style, FTextFieldStateStyle stateStyle, - ) => - InputDecoration( + ) => InputDecoration( suffixIcon: parent.suffix, // See https://stackoverflow.com/questions/70771410/flutter-how-can-i-remove-the-content-padding-for-error-in-textformfield prefix: Padding(padding: EdgeInsets.only(left: style.contentPadding.left)), @@ -18,13 +17,6 @@ class _Field extends FormField { ? null : DefaultTextStyle.merge(style: stateStyle.descriptionTextStyle, child: parent.description!), helperStyle: stateStyle.descriptionTextStyle, - error: state.errorText == null - ? null - : DefaultTextStyle.merge( - style: stateStyle.descriptionTextStyle, - child: parent.errorBuilder(state.context, state.errorText!), - ), - errorStyle: stateStyle.descriptionTextStyle, disabledBorder: OutlineInputBorder( borderSide: BorderSide( color: style.disabledStyle.unfocusedStyle.color, @@ -148,6 +140,19 @@ class _Field extends FormField { spellCheckConfiguration: parent.spellCheckConfiguration, magnifierConfiguration: parent.magnifierConfiguration, ), + AnimatedSwitcher( + duration: style.errorStyle.animationDuration, + child: switch (state.errorText) { + null => const SizedBox(), + final error => Padding( + padding: const EdgeInsets.only(top: 7, bottom: 4), + child: DefaultTextStyle.merge( + style: style.errorStyle.errorTextStyle, + child: Text(error), + ), + ), + }, + ), ], ), ); diff --git a/samples/lib/widgets/text_field.dart b/samples/lib/widgets/text_field.dart index 045060efd..e2b3190c6 100644 --- a/samples/lib/widgets/text_field.dart +++ b/samples/lib/widgets/text_field.dart @@ -106,16 +106,22 @@ class _LoginFormState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - FTextField.email( - hint: 'janedoe@foruslabs.com', - description: const Text(''), - validator: (value) => (value?.contains('@') ?? false) ? null : 'Please enter a valid email.', + SizedBox( + height: 110, + child: FTextField.email( + hint: 'janedoe@foruslabs.com', + autovalidateMode: AutovalidateMode.onUserInteraction, + validator: (value) => (value?.contains('@') ?? false) ? null : 'Please enter a valid email.', + ), ), const SizedBox(height: 4), - FTextField.password( - hint: '', - description: const Text(''), - validator: (value) => 8 <= (value?.length ?? 0) ? null : 'Password must be at least 8 characters long.', + SizedBox( + height: 110, + child: FTextField.password( + hint: '', + autovalidateMode: AutovalidateMode.onUserInteraction, + validator: (value) => 8 <= (value?.length ?? 0) ? null : 'Password must be at least 8 characters long.', + ), ), const SizedBox(height: 30), FButton(