diff --git a/docs/pages/docs/text-field.mdx b/docs/pages/docs/text-field.mdx index 5b0f0a7b3..a873c13ab 100644 --- a/docs/pages/docs/text-field.mdx +++ b/docs/pages/docs/text-field.mdx @@ -2,7 +2,8 @@ import { Tabs } from 'nextra/components'; import { Widget } from '../../components/widget'; # Text Fields -A text field lets the user enter text, either with hardware keyboard or with an onscreen keyboard. +A text field lets the user enter text, either with hardware keyboard or with an onscreen keyboard. It can also be used +in a form. @@ -14,7 +15,8 @@ A text field lets the user enter text, either with hardware keyboard or with an enabled: enabled, label: 'Email', hint: 'john@doe.com', - ) + footer: Text('Enter your email associated with your Forui account.'), + ); ``` @@ -26,9 +28,9 @@ A text field lets the user enter text, either with hardware keyboard or with an ```dart FTextField( enabled: true, - label: 'Email', + label: Text('Email'), hint: 'john@doe.com', - footer: 'Enter your email associated with your Forui account.', + footer: Text('Enter your email associated with your Forui account.'), keyboardType: TextInputType.emailAddress, textCapitalization: TextCapitalization.none, ); @@ -38,9 +40,8 @@ FTextField( ```dart FTextField.email( - label: 'Email', hint: 'john@doe.com', - footer: 'Enter your email associated with your Forui account.', + footer: Text('Enter your email associated with your Forui account.'), ); ``` @@ -48,8 +49,7 @@ FTextField.email( ```dart FTextField.password( - label: 'Password', - footer: 'Your password must be at least 8 characters long.', + footer: Text('Your password must be at least 8 characters long.'), ); ``` @@ -57,9 +57,9 @@ FTextField.password( ```dart FTextField.multiline( - label: 'Description', + label: Text('Description'), hint: 'Enter a description...', - footer: 'Enter a description of the item.', + footer: Text('Enter a description of the item.'), ); ``` @@ -75,7 +75,6 @@ FTextField.multiline( ```dart FTextField.email( - label: 'Email', hint: 'john@doe.com', ); ``` @@ -92,7 +91,7 @@ FTextField.multiline( ```dart FTextField.email( - label: 'Email', + enabled: false hint: 'john@doe.com', ); ``` @@ -110,7 +109,6 @@ FTextField.multiline( ```dart FTextField.password( controller: TextEditingController(text: 'My password'), - label: 'Password', ); ``` @@ -125,9 +123,67 @@ FTextField.multiline( ```dart FTextField.multiline( - label: 'Leave a review', + label: Text('Leave a review'), + maxLines: 4, ); ``` + +### Form + + + + + + + ```dart + class LoginForm extends StatefulWidget { + const LoginForm({super.key}); + + @override + State createState() => _LoginFormState(); + } + + class _LoginFormState extends State { + final GlobalKey _formKey = GlobalKey(); + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) => Form( + key: _formKey, + child: Column( + children: [ + FTextField.email( + hint: 'janedoe@foruslabs.com', + help: const Text(''), + 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.', + ), + const SizedBox(height: 30), + FButton( + rawLabel: const Text('Login'), + onPress: () { + if (!_formKey.currentState!.validate()) { + // Handle errors here. + return; + } + }, + ), + ], + ), + ); + } + ``` + + diff --git a/forui/CHANGELOG.md b/forui/CHANGELOG.md index 68f6bb14c..2798daaec 100644 --- a/forui/CHANGELOG.md +++ b/forui/CHANGELOG.md @@ -1,12 +1,14 @@ -## 0.2.0 +## Next +### Changes * Add `Header.nested` widget. - -### Breaking changes - -* `FHeaderStyle` have been nested in `FHeaderStyles.rootStyle`. -* `FHeaderActionStyle.action` parameter has been renamed to `FRootHeaderStyle.actionStyle`. -* `FHeaderActionStyle.padding` parameter has been moved to `FRootHeaderStyle.actionSpacing`. +* Change `FTextField` to be usable in `Form`s. +* Change `FTextFieldStyle`'s default vertical content padding from `5` to `15`. +* **Breaking** Move `FHeaderStyle` to `FHeaderStyles.rootStyle`. +* **Breaking** Move `FHeaderActionStyle.padding` to `FRootHeaderStyle.actionSpacing`. +* Split exports in `forui.dart` into sub-libraries. +* **Breaking** Suffix style parameters with `Style`, i.e. `FRootHeaderStyle.action` has been renamed to `FRootHeaderStyle.actionStyle`. +* Fix missing `key` parameter in `FTextField` constructors. ## 0.1.0 diff --git a/forui/analysis_options.yaml b/forui/analysis_options.yaml index 81cdeebfb..61f85845a 100644 --- a/forui/analysis_options.yaml +++ b/forui/analysis_options.yaml @@ -5,4 +5,5 @@ analyzer: linter: rules: + - use_key_in_widget_constructors - require_trailing_commas diff --git a/forui/build.yaml b/forui/build.yaml index 824618c6d..479407f30 100644 --- a/forui/build.yaml +++ b/forui/build.yaml @@ -3,10 +3,6 @@ targets: $default: builders: - stevia_runner:steviaAssetGenerator: - generate_for: - - assets/** - mockito:mockBuilder: generate_for: - test/**.dart \ No newline at end of file diff --git a/forui/lib/assets.dart b/forui/lib/assets.dart new file mode 100644 index 000000000..b4c2ef18c --- /dev/null +++ b/forui/lib/assets.dart @@ -0,0 +1,6 @@ +/// The bundled assets in [forui_assets](https://github.com/forus-labs/forui/tree/main/forui_assets), exported for +/// convenience. +library forui.assets; + +export 'package:forui_assets/forui_assets.dart'; +export 'package:forui/src/svg_extension.nitrogen.dart'; diff --git a/forui/lib/forui.dart b/forui/lib/forui.dart index a9e3d7982..a168a7a17 100644 --- a/forui/lib/forui.dart +++ b/forui/lib/forui.dart @@ -1,32 +1,6 @@ /// A Flutter package for building beautiful user interfaces. library forui; -// Icons -export 'package:forui_assets/forui_assets.dart'; -export 'package:forui/src/svg_extension.nitrogen.dart'; - -// Theme -export 'src/theme/color_scheme.dart'; -export 'src/theme/style.dart'; -export 'src/theme/theme.dart'; -export 'src/theme/theme_data.dart'; - -export 'src/theme/typography.dart'; - -// Themes -export 'src/theme/themes.dart'; - -// Foundation -export 'src/foundation/tappable.dart' hide FTappable; - -// Widgets -export 'src/widgets/badge/badge.dart' hide FBadgeContent, Variant; -export 'src/widgets/button/button.dart' hide FButtonContent, Variant; -export 'src/widgets/card/card.dart' hide FCardContent; -export 'src/widgets/dialog/dialog.dart' hide FDialogContent, FHorizontalDialogContent, FVerticalDialogContent; -export 'src/widgets/header/header.dart'; -export 'src/widgets/tabs/tabs.dart'; -export 'src/widgets/text_field/text_field.dart'; -export 'src/widgets/scaffold.dart'; -export 'src/widgets/separator.dart'; -export 'src/widgets/switch.dart'; +export 'assets.dart'; +export 'theme.dart'; +export 'widgets.dart'; diff --git a/forui/lib/src/widgets/dialog/dialog_content.dart b/forui/lib/src/widgets/dialog/dialog_content.dart index b35ce352f..9d057ce1d 100644 --- a/forui/lib/src/widgets/dialog/dialog_content.dart +++ b/forui/lib/src/widgets/dialog/dialog_content.dart @@ -102,6 +102,7 @@ class FHorizontalDialogContent extends FDialogContent { required super.body, required super.rawBody, required super.actions, + super.key, }) : super(alignment: CrossAxisAlignment.start, titleTextAlign: TextAlign.start, bodyTextAlign: TextAlign.start); @override @@ -125,6 +126,7 @@ class FVerticalDialogContent extends FDialogContent { required super.body, required super.rawBody, required super.actions, + super.key, }) : super(alignment: CrossAxisAlignment.center, titleTextAlign: TextAlign.center, bodyTextAlign: TextAlign.center); @override diff --git a/forui/lib/src/widgets/text_field/text_field.dart b/forui/lib/src/widgets/text_field/text_field.dart index a118eefd5..140243b3e 100644 --- a/forui/lib/src/widgets/text_field/text_field.dart +++ b/forui/lib/src/widgets/text_field/text_field.dart @@ -10,74 +10,47 @@ import 'package:meta/meta.dart'; import 'package:forui/forui.dart'; part 'text_field_style.dart'; +part 'text_form_field.dart'; /// A text field. /// -/// It lets the user enter text, either with hardware keyboard or with an onscreen keyboard. +/// It lets the user enter text, either with hardware keyboard or with an onscreen keyboard. A [FTextField] is internally +/// a [FormField], therefore it can be used in a [Form]. /// /// 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 _defaultContextMenuBuilder( + static Widget _contextMenuBuilder( BuildContext context, - EditableTextState editableTextState, + EditableTextState state, ) => - AdaptiveTextSelectionToolbar.editableText(editableTextState: editableTextState); + AdaptiveTextSelectionToolbar.editableText(editableTextState: state); + + static Widget _errorBuilder(BuildContext context, String text) => Text(text); /// The text field's style. Defaults to [FThemeData.textFieldStyle]. final FTextFieldStyle? style; /// The label above a text field. - /// - /// ## Contract: - /// Throws [AssertionError] if: - /// * both [label] and [rawLabel] are not null - final String? label; - - /// The raw label above a text field. - /// - /// ## Contract: - /// Throws [AssertionError] if: - /// * both [label] and [rawLabel] are not null - final Widget? rawLabel; + final Widget? label; /// The text to display when the text field is empty. /// /// See [InputDecoration.hintText] for more information. final String? hint; - /// The maximum number of lines the [hint] can occupy. Defaults to the value of [TextField.maxLines] attribute. - /// - /// See [InputDecoration.hintMaxLines] for more information. - final int? hintMaxLines; - - /// The help text. - /// - /// See [InputDecoration.helperText] for more information. - final String? help; - /// The raw help text. - final Widget? rawHelp; - - /// The maximum number of lines the [help] can occupy. Defaults to the value of [TextField.maxLines] attribute. - /// - /// See [InputDecoration.helperMaxLines] for more information. - final int? helpMaxLines; - - /// The error text. /// - /// See [InputDecoration.errorText] for more information. - final String? error; + /// See [InputDecoration.helper] for more information. + final Widget? help; /// The raw error text. - final Widget? rawError; - - /// The maximum number of lines the [error] can occupy. Defaults to the value of [TextField.maxLines] attribute. /// - /// See [InputDecoration.errorMaxLines] for more information. - final int? errorMaxLines; + /// See [InputDecoration.error] for more information. + final Widget? error; /// The configuration for the magnifier of this text field. /// @@ -296,7 +269,7 @@ final class FTextField extends StatelessWidget { /// 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 [style]'s [FTextFieldStyle.error] when the limit is exceeded. + /// but the error counter and divider will switch to the [style]'s [FTextFieldStyle.errorStyle] when the limit is exceeded. final int? maxLength; /// Determines how the [maxLength] limit should be enforced. @@ -477,27 +450,47 @@ final class FTextField extends StatelessWidget { /// The suffix icon. /// /// See [InputDecoration.suffixIcon] for more information. - final Widget? suffixIcon; + final Widget? suffix; - /// Creates a [FTextField]. + /// An optional method to call with the final value when the form is saved via [FormState.save]. + final FormFieldSetter? onSave; + + /// 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. [_Field] transform the text + /// using [...] before using the returned widget to override [error]. + /// + /// Alternating between error and normal state can cause the height of the [_Field] 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 [help] + /// parameter to a space. + final FormFieldValidator? validator; + + /// An optional value to initialize the form field to, or null otherwise. + final String? initialValue; + + /// Used to enable/disable this form field auto validation and update its error text. + /// + /// Defaults to [AutovalidateMode.disabled]. /// - /// ## Contract: - /// Throws [AssertionError] if: - /// * both [label] and [rawLabel] are not null - /// * both [help] and [rawHelp] are not null - /// * both [error] and [rawError] are not null + /// 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. + final AutovalidateMode? autovalidateMode; + + /// A builder that transforms a [FormFieldState.errorText] into a widget. Defaults to a [Text] widget. + /// + /// The builder is called whenever [validator] returns an error text. It replaces [error] if it was provided. + final Widget Function(BuildContext, String) errorBuilder; + + /// Creates a [FTextField]. const FTextField({ this.style, this.label, - this.rawLabel, this.hint, - this.hintMaxLines, this.help, - this.rawHelp, - this.helpMaxLines, this.error, - this.rawError, - this.errorMaxLines, this.magnifierConfiguration, this.controller, this.focusNode, @@ -537,28 +530,26 @@ final class FTextField extends StatelessWidget { this.restorationId, this.scribbleEnabled = true, this.enableIMEPersonalizedLearning = true, - this.contextMenuBuilder = _defaultContextMenuBuilder, + this.contextMenuBuilder = _contextMenuBuilder, this.canRequestFocus = true, this.undoController, this.spellCheckConfiguration, - this.suffixIcon, - }) : assert(label == null || rawLabel == null, 'Cannot provide both a label and a rawLabel.'), - assert(help == null || rawHelp == null, 'Cannot provide both a help and a rawHelp.'), - assert(error == null || rawError == null, 'Cannot provide both an error and a rawError.'); + this.suffix, + this.onSave, + this.validator, + this.initialValue, + this.autovalidateMode, + this.errorBuilder = _errorBuilder, + super.key, + }); /// Creates a [FTextField] configured for emails. const FTextField.email({ this.style, - this.label, - this.rawLabel, - this.hint = 'Email', - this.hintMaxLines, + this.label = const Text('Email'), + this.hint, this.help, - this.rawHelp, - this.helpMaxLines, this.error, - this.rawError, - this.errorMaxLines, this.magnifierConfiguration, this.controller, this.focusNode, @@ -598,37 +589,29 @@ final class FTextField extends StatelessWidget { this.restorationId, this.scribbleEnabled = true, this.enableIMEPersonalizedLearning = true, - this.contextMenuBuilder = _defaultContextMenuBuilder, + this.contextMenuBuilder = _contextMenuBuilder, this.canRequestFocus = true, this.undoController, this.spellCheckConfiguration, - this.suffixIcon, - }) : assert(label == null || rawLabel == null, 'Cannot provide both a label and a rawLabel.'), - assert(help == null || rawHelp == null, 'Cannot provide both a help and a rawHelp.'), - assert(error == null || rawError == null, 'Cannot provide both an error and a rawError.'); + this.suffix, + this.onSave, + this.validator, + this.initialValue, + this.autovalidateMode, + this.errorBuilder = _errorBuilder, + super.key, + }); /// Creates a [FTextField] configured for passwords. /// /// [autofillHints] defaults to [AutofillHints.password]. It should be overridden with [AutofillHints.newPassword] /// when handling the creation of new passwords. - /// - /// ## Contract: - /// Throws [AssertionError] if: - /// * both [label] and [rawLabel] are not null - /// * both [help] and [rawHelp] are not null - /// * both [error] and [rawError] are not null const FTextField.password({ this.style, - this.label, - this.rawLabel, - this.hint = 'Password', - this.hintMaxLines, + this.label = const Text('Password'), + this.hint, this.help, - this.rawHelp, - this.helpMaxLines, this.error, - this.rawError, - this.errorMaxLines, this.magnifierConfiguration, this.controller, this.focusNode, @@ -668,36 +651,30 @@ final class FTextField extends StatelessWidget { this.restorationId, this.scribbleEnabled = true, this.enableIMEPersonalizedLearning = true, - this.contextMenuBuilder = _defaultContextMenuBuilder, + this.contextMenuBuilder = _contextMenuBuilder, this.canRequestFocus = true, this.undoController, this.spellCheckConfiguration, - this.suffixIcon, - }) : assert(label == null || rawLabel == null, 'Cannot provide both a label and a rawLabel.'), - assert(help == null || rawHelp == null, 'Cannot provide both a help and a rawHelp.'), - assert(error == null || rawError == null, 'Cannot provide both an error and a rawError.'); + this.suffix, + this.onSave, + this.validator, + this.initialValue, + this.autovalidateMode, + this.errorBuilder = _errorBuilder, + super.key, + }); /// Creates a [FTextField] configured for multiline inputs. /// - /// The text field's height can be configured by adjusting [minLines]. - /// - /// ## Contract: - /// Throws [AssertionError] if: - /// * both [label] and [rawLabel] are not null - /// * both [help] and [rawHelp] are not null - /// * both [error] and [rawError] are not null + /// The text field's height can be configured by adjusting [minLines]. By default, the text field will expand every + /// time a new line is added. To limit the maximum height of the text field and make it scrollable, consider setting + /// [maxLines]. const FTextField.multiline({ this.style, this.label, - this.rawLabel, this.hint, - this.hintMaxLines, this.help, - this.rawHelp, - this.helpMaxLines, this.error, - this.rawError, - this.errorMaxLines, this.magnifierConfiguration, this.controller, this.focusNode, @@ -737,34 +714,30 @@ final class FTextField extends StatelessWidget { this.restorationId, this.scribbleEnabled = true, this.enableIMEPersonalizedLearning = true, - this.contextMenuBuilder = _defaultContextMenuBuilder, + this.contextMenuBuilder = _contextMenuBuilder, this.canRequestFocus = true, this.undoController, this.spellCheckConfiguration, - this.suffixIcon, - }) : assert(label == null || rawLabel == null, 'Cannot provide both a label and a rawLabel.'), - assert(help == null || rawHelp == null, 'Cannot provide both a help and a rawHelp.'), - assert(error == null || rawError == null, 'Cannot provide both an error and a rawError.'); + this.suffix, + this.onSave, + this.validator, + this.initialValue, + this.autovalidateMode, + this.errorBuilder = _errorBuilder, + super.key, + }); @override Widget build(BuildContext context) { final theme = context.theme; - final typography = theme.typography; final style = this.style ?? theme.textFieldStyle; final stateStyle = switch (this) { - _ when !enabled => style.disabled, - _ when error != null || rawError != null => style.error, - _ => style.enabled, - }; - final materialLocalizations = Localizations.of(context, MaterialLocalizations); - - final label = switch ((this.label, rawLabel)) { - (final String label, _) => Text(label), - (_, final Widget label) => label, - _ => null, + _ when !enabled => style.disabledStyle, + _ when error != null => style.errorStyle, + _ => style.enabledStyle, }; - final textField = MergeSemantics( + final textFormField = MergeSemantics( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -773,7 +746,7 @@ final class FTextField extends StatelessWidget { padding: const EdgeInsets.only(top: 4, bottom: 7), child: DefaultTextStyle.merge( style: stateStyle.labelTextStyle, - child: label, + child: label!, ), ), Material( @@ -791,13 +764,19 @@ final class FTextField extends StatelessWidget { primaryColor: style.cursorColor, ), ), - child: _textField(context, typography, style, stateStyle), + child: _Field( + parent: this, + style: style, + stateStyle: stateStyle, + key: key, + ), ), ), ], ), ); + final materialLocalizations = Localizations.of(context, MaterialLocalizations); return materialLocalizations == null ? Localizations( locale: Localizations.maybeLocaleOf(context) ?? const Locale('en', 'US'), @@ -806,131 +785,9 @@ final class FTextField extends StatelessWidget { GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], - child: textField, + child: textFormField, ) - : textField; - } - - Widget _textField( - BuildContext context, - FTypography typography, - FTextFieldStyle style, - FTextFieldStateStyle current, - ) { - final rawError = this.rawError == null - ? this.rawError - : DefaultTextStyle.merge( - style: current.footerTextStyle, - child: this.rawError!, - ); - - final rawHelp = this.rawHelp == null - ? this.rawHelp - : DefaultTextStyle.merge( - style: current.footerTextStyle, - child: this.rawHelp!, - ); - - return TextField( - controller: controller, - focusNode: focusNode, - undoController: undoController, - cursorErrorColor: style.cursorColor, - decoration: InputDecoration( - suffixIcon: suffixIcon, - // 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)), - contentPadding: style.contentPadding.copyWith(left: 0), - hintText: hint, - hintStyle: current.hintTextStyle, - hintMaxLines: hintMaxLines, - helper: rawHelp, - helperText: help, - helperStyle: current.footerTextStyle, - helperMaxLines: helpMaxLines, - error: rawError, - errorText: error, - errorStyle: current.footerTextStyle, - errorMaxLines: errorMaxLines, - disabledBorder: OutlineInputBorder( - borderSide: BorderSide( - color: style.disabled.unfocused.color, - width: style.disabled.unfocused.width, - ), - borderRadius: style.disabled.unfocused.radius, - ), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide( - color: style.enabled.unfocused.color, - width: style.enabled.unfocused.width, - ), - borderRadius: style.enabled.unfocused.radius, - ), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: style.enabled.focused.color, - width: style.enabled.focused.width, - ), - borderRadius: current.focused.radius, - ), - errorBorder: OutlineInputBorder( - borderSide: BorderSide( - color: style.error.unfocused.color, - width: style.error.unfocused.width, - ), - borderRadius: style.error.unfocused.radius, - ), - focusedErrorBorder: OutlineInputBorder( - borderSide: BorderSide( - color: style.error.focused.color, - width: style.error.focused.width, - ), - borderRadius: style.error.focused.radius, - ), - ), - keyboardType: keyboardType, - textInputAction: textInputAction, - textCapitalization: textCapitalization, - style: current.contentTextStyle, - textAlign: textAlign, - textAlignVertical: textAlignVertical, - textDirection: textDirection, - readOnly: readOnly, - showCursor: showCursor, - autofocus: autofocus, - statesController: statesController, - obscureText: obscureText, - autocorrect: autocorrect, - smartDashesType: smartDashesType, - smartQuotesType: smartQuotesType, - enableSuggestions: enableSuggestions, - maxLines: maxLines, - minLines: minLines, - expands: expands, - maxLength: maxLength, - maxLengthEnforcement: maxLengthEnforcement, - onChanged: onChange, - onEditingComplete: onEditingComplete, - onSubmitted: onSubmit, - onAppPrivateCommand: onAppPrivateCommand, - inputFormatters: inputFormatters, - enabled: enabled, - ignorePointers: ignorePointers, - keyboardAppearance: style.keyboardAppearance, - scrollPadding: style.scrollPadding, - dragStartBehavior: dragStartBehavior, - selectionControls: selectionControls, - scrollController: scrollController, - scrollPhysics: scrollPhysics, - autofillHints: autofillHints, - restorationId: restorationId, - scribbleEnabled: scribbleEnabled, - enableIMEPersonalizedLearning: enableIMEPersonalizedLearning, - contextMenuBuilder: contextMenuBuilder, - canRequestFocus: canRequestFocus, - spellCheckConfiguration: spellCheckConfiguration, - magnifierConfiguration: magnifierConfiguration, - ); + : textFormField; } @override @@ -938,13 +795,7 @@ final class FTextField extends StatelessWidget { super.debugFillProperties(properties); properties ..add(DiagnosticsProperty('style', style)) - ..add(StringProperty('label', label)) ..add(StringProperty('hint', hint)) - ..add(IntProperty('hintMaxLines', hintMaxLines)) - ..add(StringProperty('help', help)) - ..add(IntProperty('helpMaxLines', helpMaxLines)) - ..add(StringProperty('error', error)) - ..add(IntProperty('errorMaxLines', errorMaxLines)) ..add(DiagnosticsProperty('magnifierConfiguration', magnifierConfiguration)) ..add(DiagnosticsProperty('controller', controller)) ..add(DiagnosticsProperty('focusNode', focusNode)) @@ -994,6 +845,11 @@ final class FTextField extends StatelessWidget { ..add(FlagProperty('canRequestFocus', value: canRequestFocus, ifTrue: 'canRequestFocus')) ..add(DiagnosticsProperty('undoController', undoController)) ..add(DiagnosticsProperty('spellCheckConfiguration', spellCheckConfiguration)) - ..add(DiagnosticsProperty('suffixIcon', suffixIcon)); + ..add(DiagnosticsProperty('suffixIcon', suffix)) + ..add(ObjectFlagProperty.has('onSave', onSave)) + ..add(ObjectFlagProperty.has('validator', validator)) + ..add(StringProperty('initialValue', initialValue)) + ..add(EnumProperty('autovalidateMode', autovalidateMode)) + ..add(ObjectFlagProperty.has('errorBuilder', errorBuilder)); } } 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 b21d71921..95e12e461 100644 --- a/forui/lib/src/widgets/text_field/text_field_style.dart +++ b/forui/lib/src/widgets/text_field/text_field_style.dart @@ -14,7 +14,7 @@ final class FTextFieldStyle with Diagnosticable { /// The padding surrounding this text field's content. /// - /// Defaults to `const EdgeInsets.symmetric(horizontal: 15, vertical: 5)`. + /// Defaults to `const EdgeInsets.symmetric(horizontal: 15, vertical: 15)`. final EdgeInsets contentPadding; /// Configures padding to edges surrounding a [Scrollable] when this text field scrolls into view. @@ -28,22 +28,22 @@ final class FTextFieldStyle with Diagnosticable { final EdgeInsets scrollPadding; /// The style when this text field is enabled. - final FTextFieldStateStyle enabled; + final FTextFieldStateStyle enabledStyle; /// The style when this text field is enabled. - final FTextFieldStateStyle disabled; + final FTextFieldStateStyle disabledStyle; /// The style when this text field has an error. - final FTextFieldStateStyle error; + final FTextFieldStateStyle errorStyle; /// Creates a [FTextFieldStyle]. FTextFieldStyle({ required this.keyboardAppearance, - required this.enabled, - required this.disabled, - required this.error, + required this.enabledStyle, + required this.disabledStyle, + required this.errorStyle, this.cursorColor = CupertinoColors.activeBlue, - this.contentPadding = const EdgeInsets.symmetric(horizontal: 15, vertical: 5), + this.contentPadding = const EdgeInsets.symmetric(horizontal: 15, vertical: 15), this.scrollPadding = const EdgeInsets.all(20), }); @@ -54,9 +54,9 @@ final class FTextFieldStyle with Diagnosticable { required FStyle style, }) : keyboardAppearance = colorScheme.brightness, cursorColor = CupertinoColors.activeBlue, - contentPadding = const EdgeInsets.symmetric(horizontal: 15, vertical: 5), + contentPadding = const EdgeInsets.symmetric(horizontal: 15, vertical: 15), scrollPadding = const EdgeInsets.all(20.0), - enabled = FTextFieldStateStyle.inherit( + enabledStyle = FTextFieldStateStyle.inherit( labelColor: colorScheme.primary, contentColor: colorScheme.primary, hintColor: colorScheme.mutedForeground, @@ -66,9 +66,9 @@ final class FTextFieldStyle with Diagnosticable { typography: typography, style: style, ), - disabled = FTextFieldStateStyle.inherit( - labelColor: colorScheme.primary, - contentColor: colorScheme.primary, + disabledStyle = FTextFieldStateStyle.inherit( + labelColor: colorScheme.primary.withOpacity(0.7), + contentColor: colorScheme.primary.withOpacity(0.7), hintColor: colorScheme.border.withOpacity(0.7), footerColor: colorScheme.border.withOpacity(0.7), focusedBorderColor: colorScheme.border.withOpacity(0.7), @@ -76,7 +76,7 @@ final class FTextFieldStyle with Diagnosticable { typography: typography, style: style, ), - error = FTextFieldStateStyle.inherit( + errorStyle = FTextFieldStateStyle.inherit( labelColor: colorScheme.primary, contentColor: colorScheme.primary, hintColor: colorScheme.mutedForeground, @@ -91,17 +91,17 @@ final class FTextFieldStyle with Diagnosticable { /// /// ```dart /// final style = FTextFieldStyle( - /// enabled: ..., - /// disabled: ..., + /// enabledStyle: ..., + /// disabledStyle: ..., /// // Other arguments omitted for brevity /// ); /// /// final copy = style.copyWith( - /// disabled: ..., + /// disabledStyle: ..., /// ); /// - /// print(style.enabled == copy.enabled); // true - /// print(style.disabled == copy.disabled); // false + /// print(style.enabledStyle == copy.enabledStyle); // true + /// print(style.disabledStyle == copy.disabledStyle); // false /// ``` @useResult FTextFieldStyle copyWith({ @@ -109,18 +109,18 @@ final class FTextFieldStyle with Diagnosticable { Color? cursorColor, EdgeInsets? contentPadding, EdgeInsets? scrollPadding, - FTextFieldStateStyle? enabled, - FTextFieldStateStyle? disabled, - FTextFieldStateStyle? error, + FTextFieldStateStyle? enabledStyle, + FTextFieldStateStyle? disabledStyle, + FTextFieldStateStyle? errorStyle, }) => FTextFieldStyle( keyboardAppearance: keyboardAppearance ?? this.keyboardAppearance, cursorColor: cursorColor ?? this.cursorColor, contentPadding: contentPadding ?? this.contentPadding, scrollPadding: scrollPadding ?? this.scrollPadding, - enabled: enabled ?? this.enabled, - disabled: disabled ?? this.disabled, - error: error ?? this.error, + enabledStyle: enabledStyle ?? this.enabledStyle, + disabledStyle: disabledStyle ?? this.disabledStyle, + errorStyle: errorStyle ?? this.errorStyle, ); @override @@ -131,9 +131,9 @@ final class FTextFieldStyle with Diagnosticable { ..add(ColorProperty('cursorColor', cursorColor, defaultValue: CupertinoColors.activeBlue)) ..add(DiagnosticsProperty('contentPadding', contentPadding)) ..add(DiagnosticsProperty('scrollPadding', scrollPadding)) - ..add(DiagnosticsProperty('enabledBorder', enabled)) - ..add(DiagnosticsProperty('disabledBorder', disabled)) - ..add(DiagnosticsProperty('errorBorder', error)); + ..add(DiagnosticsProperty('enabledStyle', enabledStyle)) + ..add(DiagnosticsProperty('disabledStyle', disabledStyle)) + ..add(DiagnosticsProperty('errorStyle', errorStyle)); } @override @@ -145,9 +145,9 @@ final class FTextFieldStyle with Diagnosticable { cursorColor == other.cursorColor && contentPadding == other.contentPadding && scrollPadding == other.scrollPadding && - enabled == other.enabled && - disabled == other.disabled && - error == other.error; + enabledStyle == other.enabledStyle && + disabledStyle == other.disabledStyle && + errorStyle == other.errorStyle; @override int get hashCode => @@ -155,9 +155,9 @@ final class FTextFieldStyle with Diagnosticable { cursorColor.hashCode ^ contentPadding.hashCode ^ scrollPadding.hashCode ^ - enabled.hashCode ^ - disabled.hashCode ^ - error.hashCode; + enabledStyle.hashCode ^ + disabledStyle.hashCode ^ + errorStyle.hashCode; } /// A [FTextField] state's style. @@ -175,10 +175,10 @@ final class FTextFieldStateStyle with Diagnosticable { final TextStyle footerTextStyle; /// The border's color when focused. - final FTextFieldBorderStyle focused; + final FTextFieldBorderStyle focusedStyle; /// The border's style when unfocused. - final FTextFieldBorderStyle unfocused; + final FTextFieldBorderStyle unfocusedStyle; /// Creates a [FTextFieldStateStyle]. FTextFieldStateStyle({ @@ -186,8 +186,8 @@ final class FTextFieldStateStyle with Diagnosticable { required this.contentTextStyle, required this.hintTextStyle, required this.footerTextStyle, - required this.focused, - required this.unfocused, + required this.focusedStyle, + required this.unfocusedStyle, }); /// Creates a [FTextFieldStateStyle] that inherits its properties. @@ -216,8 +216,8 @@ final class FTextFieldStateStyle with Diagnosticable { fontFamily: typography.defaultFontFamily, color: footerColor, ), - focused = FTextFieldBorderStyle.inherit(color: focusedBorderColor, style: style), - unfocused = FTextFieldBorderStyle.inherit(color: unfocusedBorderColor, style: style); + focusedStyle = FTextFieldBorderStyle.inherit(color: focusedBorderColor, style: style), + unfocusedStyle = FTextFieldBorderStyle.inherit(color: unfocusedBorderColor, style: style); /// Returns a copy of this [FTextFieldStateStyle] with the given properties replaced. /// @@ -241,16 +241,16 @@ final class FTextFieldStateStyle with Diagnosticable { TextStyle? contentTextStyle, TextStyle? hintTextStyle, TextStyle? footerTextStyle, - FTextFieldBorderStyle? focused, - FTextFieldBorderStyle? unfocused, + FTextFieldBorderStyle? focusedStyle, + FTextFieldBorderStyle? unfocusedStyle, }) => FTextFieldStateStyle( labelTextStyle: labelTextStyle ?? this.labelTextStyle, contentTextStyle: contentTextStyle ?? this.contentTextStyle, hintTextStyle: hintTextStyle ?? this.hintTextStyle, footerTextStyle: footerTextStyle ?? this.footerTextStyle, - focused: focused ?? this.focused, - unfocused: unfocused ?? this.unfocused, + focusedStyle: focusedStyle ?? this.focusedStyle, + unfocusedStyle: unfocusedStyle ?? this.unfocusedStyle, ); @override @@ -261,8 +261,8 @@ final class FTextFieldStateStyle with Diagnosticable { ..add(DiagnosticsProperty('contentTextStyle', contentTextStyle)) ..add(DiagnosticsProperty('hintTextStyle', hintTextStyle)) ..add(DiagnosticsProperty('footerTextStyle', footerTextStyle)) - ..add(DiagnosticsProperty('focused', focused)) - ..add(DiagnosticsProperty('unfocused', unfocused)); + ..add(DiagnosticsProperty('focusedStyle', focusedStyle)) + ..add(DiagnosticsProperty('unfocusedStyle', unfocusedStyle)); } @override @@ -273,16 +273,16 @@ final class FTextFieldStateStyle with Diagnosticable { labelTextStyle == other.labelTextStyle && contentTextStyle == other.contentTextStyle && hintTextStyle == other.hintTextStyle && - focused == other.focused && - unfocused == other.unfocused; + focusedStyle == other.focusedStyle && + unfocusedStyle == other.unfocusedStyle; @override int get hashCode => labelTextStyle.hashCode ^ contentTextStyle.hashCode ^ hintTextStyle.hashCode ^ - focused.hashCode ^ - unfocused.hashCode; + focusedStyle.hashCode ^ + unfocusedStyle.hashCode; } /// A [FTextField] border's style. diff --git a/forui/lib/src/widgets/text_field/text_form_field.dart b/forui/lib/src/widgets/text_field/text_form_field.dart new file mode 100644 index 000000000..ffe232b77 --- /dev/null +++ b/forui/lib/src/widgets/text_field/text_form_field.dart @@ -0,0 +1,244 @@ +part of 'text_field.dart'; + +class _Field extends FormField { + final FTextField parent; + + _Field({ + required FTextField parent, + required FTextFieldStyle style, + required FTextFieldStateStyle stateStyle, + required Key? key, + }) : this._( + parent: parent, + style: style, + stateStyle: stateStyle, + decoration: 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)), + contentPadding: style.contentPadding.copyWith(left: 0), + hintText: parent.hint, + hintStyle: stateStyle.hintTextStyle, + helper: parent.help == null + ? null + : DefaultTextStyle.merge(style: stateStyle.footerTextStyle, child: parent.help!), + helperStyle: stateStyle.footerTextStyle, + error: parent.error == null + ? null + : DefaultTextStyle.merge(style: stateStyle.footerTextStyle, child: parent.error!), + errorStyle: stateStyle.footerTextStyle, + disabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: style.disabledStyle.unfocusedStyle.color, + width: style.disabledStyle.unfocusedStyle.width, + ), + borderRadius: style.disabledStyle.unfocusedStyle.radius, + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: style.enabledStyle.unfocusedStyle.color, + width: style.enabledStyle.unfocusedStyle.width, + ), + borderRadius: style.enabledStyle.unfocusedStyle.radius, + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: style.enabledStyle.focusedStyle.color, + width: style.enabledStyle.focusedStyle.width, + ), + borderRadius: stateStyle.focusedStyle.radius, + ), + errorBorder: OutlineInputBorder( + borderSide: BorderSide( + color: style.errorStyle.unfocusedStyle.color, + width: style.errorStyle.unfocusedStyle.width, + ), + borderRadius: style.errorStyle.unfocusedStyle.radius, + ), + focusedErrorBorder: OutlineInputBorder( + borderSide: BorderSide( + color: style.errorStyle.focusedStyle.color, + width: style.errorStyle.focusedStyle.width, + ), + borderRadius: style.errorStyle.focusedStyle.radius, + ), + ), + key: key, + ); + + _Field._({ + required this.parent, + required FTextFieldStyle style, + required FTextFieldStateStyle stateStyle, + required InputDecoration decoration, + super.key, + }) : super( + onSaved: parent.onSave, + validator: parent.validator, + initialValue: parent.initialValue, + enabled: parent.enabled, + autovalidateMode: parent.autovalidateMode, + restorationId: parent.restorationId, + builder: (field) { + final state = field as _State; + return UnmanagedRestorationScope( + bucket: state.bucket, + child: TextField( + controller: state._effectiveController, + decoration: decoration.copyWith( + error: state.errorText == null ? null : parent.errorBuilder(state.context, state.errorText!), + ), + focusNode: parent.focusNode, + undoController: parent.undoController, + cursorErrorColor: style.cursorColor, + keyboardType: parent.keyboardType, + textInputAction: parent.textInputAction, + textCapitalization: parent.textCapitalization, + style: stateStyle.contentTextStyle, + textAlign: parent.textAlign, + textAlignVertical: parent.textAlignVertical, + textDirection: parent.textDirection, + readOnly: parent.readOnly, + showCursor: parent.showCursor, + autofocus: parent.autofocus, + statesController: parent.statesController, + obscureText: parent.obscureText, + autocorrect: parent.autocorrect, + smartDashesType: parent.smartDashesType, + smartQuotesType: parent.smartQuotesType, + enableSuggestions: parent.enableSuggestions, + maxLines: parent.maxLines, + minLines: parent.minLines, + expands: parent.expands, + maxLength: parent.maxLength, + maxLengthEnforcement: parent.maxLengthEnforcement, + onChanged: (value) { + field.didChange(value); + parent.onChange?.call(value); + }, + onEditingComplete: parent.onEditingComplete, + onSubmitted: parent.onSubmit, + onAppPrivateCommand: parent.onAppPrivateCommand, + inputFormatters: parent.inputFormatters, + enabled: parent.enabled, + ignorePointers: parent.ignorePointers, + keyboardAppearance: style.keyboardAppearance, + scrollPadding: style.scrollPadding, + dragStartBehavior: parent.dragStartBehavior, + selectionControls: parent.selectionControls, + scrollController: parent.scrollController, + scrollPhysics: parent.scrollPhysics, + autofillHints: parent.autofillHints, + restorationId: parent.restorationId, + scribbleEnabled: parent.scribbleEnabled, + enableIMEPersonalizedLearning: parent.enableIMEPersonalizedLearning, + contextMenuBuilder: parent.contextMenuBuilder, + canRequestFocus: parent.canRequestFocus, + spellCheckConfiguration: parent.spellCheckConfiguration, + magnifierConfiguration: parent.magnifierConfiguration, + ), + ); + }, + ); + + @override + FormFieldState createState() => _State(); +} + +// This class is based on Material's _TextFormFieldState implementation. +class _State extends FormFieldState { + RestorableTextEditingController? _controller; + + @override + void initState() { + super.initState(); + + if (widget.parent.controller case final controller?) { + controller.addListener(_handleControllerChanged); + } else { + _registerController(RestorableTextEditingController(text: widget.initialValue)); + } + } + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + super.restoreState(oldBucket, initialRestore); + if (_controller case final controller?) { + registerForRestoration(controller, 'controller'); + } + + // Make sure to update the internal [FormFieldState] value to sync up with text editing controller value. + setValue(_effectiveController.text); + } + + void _registerController(RestorableTextEditingController controller) { + assert(_controller == null, '_controller is already initialized.'); + _controller = controller; + if (!restorePending) { + registerForRestoration(controller, 'controller'); + } + } + + @override + void didUpdateWidget(_Field old) { + super.didUpdateWidget(old); + if (widget.parent.controller == old.parent.controller) { + return; + } + + widget.parent.controller?.addListener(_handleControllerChanged); + old.parent.controller?.removeListener(_handleControllerChanged); + + switch ((widget.parent.controller, old.parent.controller)) { + case (final current?, _): + setValue(current.text); + if (_controller != null) { + unregisterFromRestoration(_controller!); + _controller?.dispose(); + _controller = null; + } + + case (null, final old?): + _registerController(RestorableTextEditingController.fromValue(old.value)); + } + } + + @override + void dispose() { + widget.parent.controller?.removeListener(_handleControllerChanged); + _controller?.dispose(); + super.dispose(); + } + + @override + void didChange(String? value) { + super.didChange(value); + if (_effectiveController.text != value) { + _effectiveController.text = value ?? ''; + } + } + + @override + void reset() { + // Set the controller value before calling super.reset() to let _handleControllerChanged suppress the change. + _effectiveController.text = widget.initialValue ?? ''; + super.reset(); + widget.parent.onChange?.call(_effectiveController.text); + } + + 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); + } + } + + @override + _Field get widget => super.widget as _Field; + + TextEditingController get _effectiveController => widget.parent.controller ?? _controller!.value; +} diff --git a/forui/lib/theme.dart b/forui/lib/theme.dart new file mode 100644 index 000000000..b0799fc43 --- /dev/null +++ b/forui/lib/theme.dart @@ -0,0 +1,10 @@ +/// Classes and functions for configuring the Forui widgets' theme. A theme configures the colors and typographic +/// choices of Forui widgets. +library forui.theme; + +export 'src/theme/color_scheme.dart'; +export 'src/theme/style.dart'; +export 'src/theme/theme.dart'; +export 'src/theme/theme_data.dart'; +export 'src/theme/themes.dart'; +export 'src/theme/typography.dart'; diff --git a/forui/lib/widgets.dart b/forui/lib/widgets.dart new file mode 100644 index 000000000..7e2a630ef --- /dev/null +++ b/forui/lib/widgets.dart @@ -0,0 +1,13 @@ +/// The Forui widgets and their corresponding styles. +library forui.widgets; + +export 'src/widgets/badge/badge.dart' hide FBadgeContent, Variant; +export 'src/widgets/button/button.dart' hide FButtonContent, Variant; +export 'src/widgets/card/card.dart' hide FCardContent; +export 'src/widgets/dialog/dialog.dart' hide FDialogContent, FHorizontalDialogContent, FVerticalDialogContent; +export 'src/widgets/header/header.dart'; +export 'src/widgets/tabs/tabs.dart'; +export 'src/widgets/text_field/text_field.dart'; +export 'src/widgets/scaffold.dart'; +export 'src/widgets/separator.dart'; +export 'src/widgets/switch.dart'; diff --git a/forui/test/golden/text_field/default-zinc-dark-focused-no-text.png b/forui/test/golden/text_field/default-zinc-dark-focused-no-text.png index 2ac12d5ec..187bdd0dd 100644 Binary files a/forui/test/golden/text_field/default-zinc-dark-focused-no-text.png and b/forui/test/golden/text_field/default-zinc-dark-focused-no-text.png differ diff --git a/forui/test/golden/text_field/default-zinc-dark-focused-raw-text.png b/forui/test/golden/text_field/default-zinc-dark-focused-raw-text.png deleted file mode 100644 index 5dcd6ba28..000000000 Binary files a/forui/test/golden/text_field/default-zinc-dark-focused-raw-text.png and /dev/null differ diff --git a/forui/test/golden/text_field/default-zinc-dark-focused-text.png b/forui/test/golden/text_field/default-zinc-dark-focused-text.png index d8a675b6f..e1e23653e 100644 Binary files a/forui/test/golden/text_field/default-zinc-dark-focused-text.png and b/forui/test/golden/text_field/default-zinc-dark-focused-text.png differ diff --git a/forui/test/golden/text_field/default-zinc-dark-unfocused-no-text.png b/forui/test/golden/text_field/default-zinc-dark-unfocused-no-text.png index b786cc36c..d952843fd 100644 Binary files a/forui/test/golden/text_field/default-zinc-dark-unfocused-no-text.png and b/forui/test/golden/text_field/default-zinc-dark-unfocused-no-text.png differ diff --git a/forui/test/golden/text_field/default-zinc-dark-unfocused-raw-text.png b/forui/test/golden/text_field/default-zinc-dark-unfocused-raw-text.png deleted file mode 100644 index c6a26c934..000000000 Binary files a/forui/test/golden/text_field/default-zinc-dark-unfocused-raw-text.png and /dev/null differ diff --git a/forui/test/golden/text_field/default-zinc-dark-unfocused-text.png b/forui/test/golden/text_field/default-zinc-dark-unfocused-text.png index 676f7ac82..3c5610f57 100644 Binary files a/forui/test/golden/text_field/default-zinc-dark-unfocused-text.png and b/forui/test/golden/text_field/default-zinc-dark-unfocused-text.png differ diff --git a/forui/test/golden/text_field/default-zinc-light-focused-no-text.png b/forui/test/golden/text_field/default-zinc-light-focused-no-text.png index 94487f96d..dc20b5878 100644 Binary files a/forui/test/golden/text_field/default-zinc-light-focused-no-text.png and b/forui/test/golden/text_field/default-zinc-light-focused-no-text.png differ diff --git a/forui/test/golden/text_field/default-zinc-light-focused-raw-text.png b/forui/test/golden/text_field/default-zinc-light-focused-raw-text.png deleted file mode 100644 index 4ec24fee7..000000000 Binary files a/forui/test/golden/text_field/default-zinc-light-focused-raw-text.png and /dev/null differ diff --git a/forui/test/golden/text_field/default-zinc-light-focused-text.png b/forui/test/golden/text_field/default-zinc-light-focused-text.png index 8702a2549..53d6e6956 100644 Binary files a/forui/test/golden/text_field/default-zinc-light-focused-text.png and b/forui/test/golden/text_field/default-zinc-light-focused-text.png differ diff --git a/forui/test/golden/text_field/default-zinc-light-unfocused-no-text.png b/forui/test/golden/text_field/default-zinc-light-unfocused-no-text.png index bfbbb5088..d01bb0c05 100644 Binary files a/forui/test/golden/text_field/default-zinc-light-unfocused-no-text.png and b/forui/test/golden/text_field/default-zinc-light-unfocused-no-text.png differ diff --git a/forui/test/golden/text_field/default-zinc-light-unfocused-raw-text.png b/forui/test/golden/text_field/default-zinc-light-unfocused-raw-text.png deleted file mode 100644 index f352346ad..000000000 Binary files a/forui/test/golden/text_field/default-zinc-light-unfocused-raw-text.png and /dev/null differ diff --git a/forui/test/golden/text_field/default-zinc-light-unfocused-text.png b/forui/test/golden/text_field/default-zinc-light-unfocused-text.png index e2d96e606..055e84d17 100644 Binary files a/forui/test/golden/text_field/default-zinc-light-unfocused-text.png and b/forui/test/golden/text_field/default-zinc-light-unfocused-text.png differ diff --git a/forui/test/golden/text_field/email-zinc-dark-focused-no-text.png b/forui/test/golden/text_field/email-zinc-dark-focused-no-text.png index 1de20344d..a8766fa44 100644 Binary files a/forui/test/golden/text_field/email-zinc-dark-focused-no-text.png and b/forui/test/golden/text_field/email-zinc-dark-focused-no-text.png differ diff --git a/forui/test/golden/text_field/email-zinc-dark-focused-text.png b/forui/test/golden/text_field/email-zinc-dark-focused-text.png index eab4d0a32..fe940ee26 100644 Binary files a/forui/test/golden/text_field/email-zinc-dark-focused-text.png and b/forui/test/golden/text_field/email-zinc-dark-focused-text.png differ diff --git a/forui/test/golden/text_field/email-zinc-dark-unfocused-no-text.png b/forui/test/golden/text_field/email-zinc-dark-unfocused-no-text.png index 6944859cf..bc90b17fb 100644 Binary files a/forui/test/golden/text_field/email-zinc-dark-unfocused-no-text.png and b/forui/test/golden/text_field/email-zinc-dark-unfocused-no-text.png differ diff --git a/forui/test/golden/text_field/email-zinc-dark-unfocused-text.png b/forui/test/golden/text_field/email-zinc-dark-unfocused-text.png index 440ce1b89..e534fcd91 100644 Binary files a/forui/test/golden/text_field/email-zinc-dark-unfocused-text.png and b/forui/test/golden/text_field/email-zinc-dark-unfocused-text.png differ diff --git a/forui/test/golden/text_field/email-zinc-light-focused-no-text.png b/forui/test/golden/text_field/email-zinc-light-focused-no-text.png index c9c21e4b6..a5326f8c4 100644 Binary files a/forui/test/golden/text_field/email-zinc-light-focused-no-text.png and b/forui/test/golden/text_field/email-zinc-light-focused-no-text.png differ diff --git a/forui/test/golden/text_field/email-zinc-light-focused-text.png b/forui/test/golden/text_field/email-zinc-light-focused-text.png index 4260ac49d..2d7206399 100644 Binary files a/forui/test/golden/text_field/email-zinc-light-focused-text.png and b/forui/test/golden/text_field/email-zinc-light-focused-text.png differ diff --git a/forui/test/golden/text_field/email-zinc-light-unfocused-no-text.png b/forui/test/golden/text_field/email-zinc-light-unfocused-no-text.png index 428b51ae5..158e091a3 100644 Binary files a/forui/test/golden/text_field/email-zinc-light-unfocused-no-text.png and b/forui/test/golden/text_field/email-zinc-light-unfocused-no-text.png differ diff --git a/forui/test/golden/text_field/email-zinc-light-unfocused-text.png b/forui/test/golden/text_field/email-zinc-light-unfocused-text.png index fd0e3a50f..cf22a7ed7 100644 Binary files a/forui/test/golden/text_field/email-zinc-light-unfocused-text.png and b/forui/test/golden/text_field/email-zinc-light-unfocused-text.png differ diff --git a/forui/test/golden/text_field/error-zinc-dark-focused-no-text.png b/forui/test/golden/text_field/error-zinc-dark-focused-no-text.png index e0872467b..13a5c8c2f 100644 Binary files a/forui/test/golden/text_field/error-zinc-dark-focused-no-text.png and b/forui/test/golden/text_field/error-zinc-dark-focused-no-text.png differ diff --git a/forui/test/golden/text_field/error-zinc-dark-focused-raw-text.png b/forui/test/golden/text_field/error-zinc-dark-focused-raw-text.png deleted file mode 100644 index 86f2f090b..000000000 Binary files a/forui/test/golden/text_field/error-zinc-dark-focused-raw-text.png and /dev/null differ diff --git a/forui/test/golden/text_field/error-zinc-dark-focused-text.png b/forui/test/golden/text_field/error-zinc-dark-focused-text.png index b9306d66f..89e13da48 100644 Binary files a/forui/test/golden/text_field/error-zinc-dark-focused-text.png and b/forui/test/golden/text_field/error-zinc-dark-focused-text.png differ diff --git a/forui/test/golden/text_field/error-zinc-dark-unfocused-no-text.png b/forui/test/golden/text_field/error-zinc-dark-unfocused-no-text.png index 5e677fe03..08b964b39 100644 Binary files a/forui/test/golden/text_field/error-zinc-dark-unfocused-no-text.png and b/forui/test/golden/text_field/error-zinc-dark-unfocused-no-text.png differ diff --git a/forui/test/golden/text_field/error-zinc-dark-unfocused-raw-text.png b/forui/test/golden/text_field/error-zinc-dark-unfocused-raw-text.png deleted file mode 100644 index 5aa877a3b..000000000 Binary files a/forui/test/golden/text_field/error-zinc-dark-unfocused-raw-text.png and /dev/null differ diff --git a/forui/test/golden/text_field/error-zinc-dark-unfocused-text.png b/forui/test/golden/text_field/error-zinc-dark-unfocused-text.png index 37256ed17..6d30d592e 100644 Binary files a/forui/test/golden/text_field/error-zinc-dark-unfocused-text.png and b/forui/test/golden/text_field/error-zinc-dark-unfocused-text.png differ diff --git a/forui/test/golden/text_field/error-zinc-light-focused-no-text.png b/forui/test/golden/text_field/error-zinc-light-focused-no-text.png index 291af3254..a22488944 100644 Binary files a/forui/test/golden/text_field/error-zinc-light-focused-no-text.png and b/forui/test/golden/text_field/error-zinc-light-focused-no-text.png differ diff --git a/forui/test/golden/text_field/error-zinc-light-focused-raw-text.png b/forui/test/golden/text_field/error-zinc-light-focused-raw-text.png deleted file mode 100644 index 1aff3e426..000000000 Binary files a/forui/test/golden/text_field/error-zinc-light-focused-raw-text.png and /dev/null differ diff --git a/forui/test/golden/text_field/error-zinc-light-focused-text.png b/forui/test/golden/text_field/error-zinc-light-focused-text.png index 1acdc7b55..55d0d672c 100644 Binary files a/forui/test/golden/text_field/error-zinc-light-focused-text.png and b/forui/test/golden/text_field/error-zinc-light-focused-text.png differ diff --git a/forui/test/golden/text_field/error-zinc-light-unfocused-no-text.png b/forui/test/golden/text_field/error-zinc-light-unfocused-no-text.png index 9a398d3ae..80587f9de 100644 Binary files a/forui/test/golden/text_field/error-zinc-light-unfocused-no-text.png and b/forui/test/golden/text_field/error-zinc-light-unfocused-no-text.png differ diff --git a/forui/test/golden/text_field/error-zinc-light-unfocused-raw-text.png b/forui/test/golden/text_field/error-zinc-light-unfocused-raw-text.png deleted file mode 100644 index aa6567c2f..000000000 Binary files a/forui/test/golden/text_field/error-zinc-light-unfocused-raw-text.png and /dev/null differ diff --git a/forui/test/golden/text_field/error-zinc-light-unfocused-text.png b/forui/test/golden/text_field/error-zinc-light-unfocused-text.png index 261380801..77ad0e4bc 100644 Binary files a/forui/test/golden/text_field/error-zinc-light-unfocused-text.png and b/forui/test/golden/text_field/error-zinc-light-unfocused-text.png differ diff --git a/forui/test/golden/text_field/multiline-zinc-dark-focused-no-text.png b/forui/test/golden/text_field/multiline-zinc-dark-focused-no-text.png index e7bd1720f..b0066e461 100644 Binary files a/forui/test/golden/text_field/multiline-zinc-dark-focused-no-text.png and b/forui/test/golden/text_field/multiline-zinc-dark-focused-no-text.png differ diff --git a/forui/test/golden/text_field/multiline-zinc-dark-focused-text.png b/forui/test/golden/text_field/multiline-zinc-dark-focused-text.png index 923d6cfda..79d11255d 100644 Binary files a/forui/test/golden/text_field/multiline-zinc-dark-focused-text.png and b/forui/test/golden/text_field/multiline-zinc-dark-focused-text.png differ diff --git a/forui/test/golden/text_field/multiline-zinc-dark-unfocused-no-text.png b/forui/test/golden/text_field/multiline-zinc-dark-unfocused-no-text.png index b4132b382..b20beb27c 100644 Binary files a/forui/test/golden/text_field/multiline-zinc-dark-unfocused-no-text.png and b/forui/test/golden/text_field/multiline-zinc-dark-unfocused-no-text.png differ diff --git a/forui/test/golden/text_field/multiline-zinc-dark-unfocused-text.png b/forui/test/golden/text_field/multiline-zinc-dark-unfocused-text.png index 095e4bb4b..5f3675dc9 100644 Binary files a/forui/test/golden/text_field/multiline-zinc-dark-unfocused-text.png and b/forui/test/golden/text_field/multiline-zinc-dark-unfocused-text.png differ diff --git a/forui/test/golden/text_field/multiline-zinc-light-focused-no-text.png b/forui/test/golden/text_field/multiline-zinc-light-focused-no-text.png index 8d0eddbf7..325e25fa3 100644 Binary files a/forui/test/golden/text_field/multiline-zinc-light-focused-no-text.png and b/forui/test/golden/text_field/multiline-zinc-light-focused-no-text.png differ diff --git a/forui/test/golden/text_field/multiline-zinc-light-focused-text.png b/forui/test/golden/text_field/multiline-zinc-light-focused-text.png index 713a92806..633c0a281 100644 Binary files a/forui/test/golden/text_field/multiline-zinc-light-focused-text.png and b/forui/test/golden/text_field/multiline-zinc-light-focused-text.png differ diff --git a/forui/test/golden/text_field/multiline-zinc-light-unfocused-no-text.png b/forui/test/golden/text_field/multiline-zinc-light-unfocused-no-text.png index 619950776..af7249c78 100644 Binary files a/forui/test/golden/text_field/multiline-zinc-light-unfocused-no-text.png and b/forui/test/golden/text_field/multiline-zinc-light-unfocused-no-text.png differ diff --git a/forui/test/golden/text_field/multiline-zinc-light-unfocused-text.png b/forui/test/golden/text_field/multiline-zinc-light-unfocused-text.png index 4762316b7..07346bef0 100644 Binary files a/forui/test/golden/text_field/multiline-zinc-light-unfocused-text.png and b/forui/test/golden/text_field/multiline-zinc-light-unfocused-text.png differ diff --git a/forui/test/golden/text_field/password-zinc-dark-focused-no-text.png b/forui/test/golden/text_field/password-zinc-dark-focused-no-text.png index ab655427f..a57679ba5 100644 Binary files a/forui/test/golden/text_field/password-zinc-dark-focused-no-text.png and b/forui/test/golden/text_field/password-zinc-dark-focused-no-text.png differ diff --git a/forui/test/golden/text_field/password-zinc-dark-focused-text.png b/forui/test/golden/text_field/password-zinc-dark-focused-text.png index bb921559a..c20569ebd 100644 Binary files a/forui/test/golden/text_field/password-zinc-dark-focused-text.png and b/forui/test/golden/text_field/password-zinc-dark-focused-text.png differ diff --git a/forui/test/golden/text_field/password-zinc-dark-unfocused-no-text.png b/forui/test/golden/text_field/password-zinc-dark-unfocused-no-text.png index 3c99da536..edab691ec 100644 Binary files a/forui/test/golden/text_field/password-zinc-dark-unfocused-no-text.png and b/forui/test/golden/text_field/password-zinc-dark-unfocused-no-text.png differ diff --git a/forui/test/golden/text_field/password-zinc-dark-unfocused-text.png b/forui/test/golden/text_field/password-zinc-dark-unfocused-text.png index 0b2fbc0ca..145614a3f 100644 Binary files a/forui/test/golden/text_field/password-zinc-dark-unfocused-text.png and b/forui/test/golden/text_field/password-zinc-dark-unfocused-text.png differ diff --git a/forui/test/golden/text_field/password-zinc-light-focused-no-text.png b/forui/test/golden/text_field/password-zinc-light-focused-no-text.png index cfd05c29b..948c48fb2 100644 Binary files a/forui/test/golden/text_field/password-zinc-light-focused-no-text.png and b/forui/test/golden/text_field/password-zinc-light-focused-no-text.png differ diff --git a/forui/test/golden/text_field/password-zinc-light-focused-text.png b/forui/test/golden/text_field/password-zinc-light-focused-text.png index 88057fffc..c1a8c6093 100644 Binary files a/forui/test/golden/text_field/password-zinc-light-focused-text.png and b/forui/test/golden/text_field/password-zinc-light-focused-text.png differ diff --git a/forui/test/golden/text_field/password-zinc-light-unfocused-no-text.png b/forui/test/golden/text_field/password-zinc-light-unfocused-no-text.png index e2a8f0c34..b12453454 100644 Binary files a/forui/test/golden/text_field/password-zinc-light-unfocused-no-text.png and b/forui/test/golden/text_field/password-zinc-light-unfocused-no-text.png differ diff --git a/forui/test/golden/text_field/password-zinc-light-unfocused-text.png b/forui/test/golden/text_field/password-zinc-light-unfocused-text.png index f42adf7b2..37b7b43a8 100644 Binary files a/forui/test/golden/text_field/password-zinc-light-unfocused-text.png and b/forui/test/golden/text_field/password-zinc-light-unfocused-text.png differ diff --git a/forui/test/src/widgets/text_field/text_field_golden_test.dart b/forui/test/src/widgets/text_field/text_field_golden_test.dart index 5986e5858..850b64248 100644 --- a/forui/test/src/widgets/text_field/text_field_golden_test.dart +++ b/forui/test/src/widgets/text_field/text_field_golden_test.dart @@ -18,60 +18,6 @@ void main() { group('FTextField', () { for (final (theme, theme_, _) in TestScaffold.themes) { for (final (focused, focused_) in [('focused', true), ('unfocused', false)]) { - testWidgets('default - $theme - $focused - raw text', (tester) async { - await tester.pumpWidget( - MaterialApp( - home: TestScaffold( - data: theme_, - child: Padding( - padding: const EdgeInsets.all(20), - child: FTextField( - controller: TextEditingController(text: 'short text'), - autofocus: focused_, - rawLabel: const Text('My Label'), - hint: 'hint', - rawHelp: const Text('Some help text.'), - ), - ), - ), - ), - ); - - await tester.pumpAndSettle(); - - await expectLater( - find.byType(TestScaffold), - matchesGoldenFile('text_field/default-$theme-$focused-raw-text.png'), - ); - }); - - testWidgets('error - $theme - $focused - raw text', (tester) async { - await tester.pumpWidget( - MaterialApp( - home: TestScaffold( - data: theme_, - child: Padding( - padding: const EdgeInsets.all(20), - child: FTextField( - controller: TextEditingController(text: 'short text'), - autofocus: focused_, - rawLabel: const Text('My Label'), - hint: 'hint', - rawError: const Text('An error has occurred.'), - ), - ), - ), - ), - ); - - await tester.pumpAndSettle(); - - await expectLater( - find.byType(TestScaffold), - matchesGoldenFile('text_field/error-$theme-$focused-raw-text.png'), - ); - }); - for (final (text, text_) in [('text', 'short text'), ('no-text', null)]) { testWidgets('default - $theme - $focused - $text', (tester) async { final controller = text_ == null ? null : TextEditingController(text: text_); @@ -84,9 +30,9 @@ void main() { child: FTextField( controller: controller, autofocus: focused_, - label: 'My Label', + label: const Text('My Label'), hint: 'hint', - help: 'Some help text.', + help: const Text('Some help text.'), ), ), ), @@ -112,9 +58,9 @@ void main() { child: FTextField( controller: controller, autofocus: focused_, - label: 'My Label', + label: const Text('My Label'), hint: 'hint', - error: 'An error has occurred.', + error: const Text('An error has occurred.'), ), ), ), @@ -140,7 +86,6 @@ void main() { child: FTextField.email( controller: controller, autofocus: focused_, - label: 'Email', hint: 'janedoe@foruslabs.com', ), ), @@ -167,7 +112,6 @@ void main() { child: FTextField.password( controller: controller, autofocus: focused_, - label: 'Password', hint: 'password', ), ), @@ -196,7 +140,7 @@ void main() { child: FTextField.multiline( controller: controller, autofocus: focused_, - label: 'My Label', + label: const Text('My Label'), hint: 'hint', ), ), diff --git a/forui/test/src/widgets/text_field/text_field_test.dart b/forui/test/src/widgets/text_field/text_field_test.dart index 6f41015fc..d29c77530 100644 --- a/forui/test/src/widgets/text_field/text_field_test.dart +++ b/forui/test/src/widgets/text_field/text_field_test.dart @@ -63,34 +63,5 @@ void main() { expect(tester.takeException(), null); }); - - for (final constructor in [ - (string, raw) => FTextField(label: string, rawLabel: raw), - (string, raw) => FTextField(help: string, rawHelp: raw), - (string, raw) => FTextField(error: string, rawError: raw), - (string, raw) => FTextField.email(label: string, rawLabel: raw), - (string, raw) => FTextField.email(help: string, rawHelp: raw), - (string, raw) => FTextField.email(error: string, rawError: raw), - (string, raw) => FTextField.password(label: string, rawLabel: raw), - (string, raw) => FTextField.password(help: string, rawHelp: raw), - (string, raw) => FTextField.password(error: string, rawError: raw), - (string, raw) => FTextField.multiline(label: string, rawLabel: raw), - (string, raw) => FTextField.multiline(help: string, rawHelp: raw), - (string, raw) => FTextField.multiline(error: string, rawError: raw), - ]) { - for (final (string, raw) in [ - (null, null), - ('', null), - (null, const SizedBox()), - ]) { - testWidgets('constructor does not throw error', (tester) async { - expect(() => constructor(string, raw), returnsNormally); - }); - } - - testWidgets('constructor title throws error', (tester) async { - expect(() => constructor('', const SizedBox()), throwsAssertionError); - }); - } }); } diff --git a/samples/README.md b/samples/README.md index 996da9fee..2ec4b8a60 100644 --- a/samples/README.md +++ b/samples/README.md @@ -1,16 +1,4 @@ -# web +# Samples -Samples for the Forui website - -## Getting Started - -This project is a starting point for a Flutter application. - -A few resources to get you started if this is your first Flutter project: - -- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) - -For help getting started with Flutter development, view the -[online documentation](https://docs.flutter.dev/), which offers tutorials, -samples, guidance on mobile development, and a full API reference. +This project contains the samples used in [forui.dev](https://forui.dev). It is not expected to be consumed directly by +users. diff --git a/samples/analysis_options.yaml b/samples/analysis_options.yaml index 0b4dd70f0..bcd4fecce 100644 --- a/samples/analysis_options.yaml +++ b/samples/analysis_options.yaml @@ -3,4 +3,8 @@ analyzer: errors: diagnostic_describe_all_properties: ignore public_member_api_docs: ignore - unused_result: ignore \ No newline at end of file + unused_result: ignore + +linter: + rules: + - require_trailing_commas \ No newline at end of file diff --git a/samples/lib/main.dart b/samples/lib/main.dart index acb2aac51..4c682b298 100644 --- a/samples/lib/main.dart +++ b/samples/lib/main.dart @@ -86,6 +86,10 @@ class _AppRouter extends $_AppRouter { path: '/text-field/multiline', page: MultilineTextFieldRoute.page, ), + AutoRoute( + path: '/text-field/form', + page: FormTextFieldRoute.page, + ), AutoRoute( path: '/scaffold/default', page: ScaffoldRoute.page, @@ -97,6 +101,6 @@ class _AppRouter extends $_AppRouter { AutoRoute( path: '/switch/default', page: SwitchRoute.page, - ) + ), ]; } diff --git a/samples/lib/widgets/scaffold.dart b/samples/lib/widgets/scaffold.dart index 15b496541..76a1ae041 100644 --- a/samples/lib/widgets/scaffold.dart +++ b/samples/lib/widgets/scaffold.dart @@ -39,12 +39,12 @@ class ScaffoldPage extends SampleScaffold { child: Column( children: [ const FTextField( - label: 'Name', + label: Text('Name'), hint: 'John Renalo', ), const SizedBox(height: 10), const FTextField( - label: 'Email', + label: Text('Email'), hint: 'john@doe.com', ), Padding( diff --git a/samples/lib/widgets/tabs.dart b/samples/lib/widgets/tabs.dart index 1d3ff1c3d..b3102eff7 100644 --- a/samples/lib/widgets/tabs.dart +++ b/samples/lib/widgets/tabs.dart @@ -29,12 +29,12 @@ class TabsPage extends SampleScaffold { child: Column( children: [ const FTextField( - label: 'Name', + label: Text('Name'), hint: 'John Renalo', ), const SizedBox(height: 10), const FTextField( - label: 'Email', + label: Text('Email'), hint: 'john@doe.com', ), Padding( @@ -58,9 +58,9 @@ class TabsPage extends SampleScaffold { padding: const EdgeInsets.only(top: 10), child: Column( children: [ - const FTextField(label: 'Current password'), + const FTextField(label: Text('Current password')), const SizedBox(height: 10), - const FTextField(label: 'New password'), + const FTextField(label: Text('New password')), Padding( padding: const EdgeInsets.only(top: 24, bottom: 16), child: FButton( diff --git a/samples/lib/widgets/text_field.dart b/samples/lib/widgets/text_field.dart index 340ff0e33..26d6a4065 100644 --- a/samples/lib/widgets/text_field.dart +++ b/samples/lib/widgets/text_field.dart @@ -22,7 +22,7 @@ class TextFieldPage extends SampleScaffold { padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 30), child: FTextField( enabled: enabled, - label: 'Email', + label: const Text('Email'), hint: 'john@doe.com', ), ), @@ -44,7 +44,6 @@ class PasswordTextFieldPage extends SampleScaffold { padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 30), child: FTextField.password( controller: TextEditingController(text: 'My password'), - label: 'Password', ), ), ], @@ -64,9 +63,64 @@ class MultilineTextFieldPage extends SampleScaffold { Padding( padding: EdgeInsets.symmetric(horizontal: 20, vertical: 30), child: FTextField.multiline( - label: 'Leave a review', + label: Text('Leave a review'), + maxLines: 4, ), ), ], ); } + +@RoutePage() +class FormTextFieldPage extends SampleScaffold { + FormTextFieldPage({ + @queryParam super.theme, + }); + + @override + Widget child(BuildContext context) => const Padding( + padding: EdgeInsets.all(15.0), + child: LoginForm(), + ); +} + +class LoginForm extends StatefulWidget { + const LoginForm({super.key}); + + @override + State createState() => _LoginFormState(); +} + +class _LoginFormState extends State { + final GlobalKey _formKey = GlobalKey(); + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) => Form( + key: _formKey, + child: Column( + children: [ + FTextField.email( + hint: 'janedoe@foruslabs.com', + help: const Text(''), + 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.', + ), + const SizedBox(height: 30), + FButton( + rawLabel: const Text('Login'), + onPress: () => _formKey.currentState!.validate(), + ), + ], + ), + ); +}