diff --git a/docs/pages/docs/checkbox.mdx b/docs/pages/docs/checkbox.mdx index e9f719bec..7081675f0 100644 --- a/docs/pages/docs/checkbox.mdx +++ b/docs/pages/docs/checkbox.mdx @@ -8,7 +8,7 @@ On touch devices, it is recommended to use a [switch](/docs/switch) instead in m - + ```dart @@ -25,6 +25,8 @@ On touch devices, it is recommended to use a [switch](/docs/switch) instead in m FCheckbox( enabled: true, initialValue: true, + autofocus: true, + onChanged: (value) {}, ); ``` @@ -33,25 +35,25 @@ FCheckbox( ### Disabled - - - - - ```dart - FCheckbox( - enabled: false, - ); - ``` - + + + + + ```dart + FCheckbox( + enabled: false, + ); + ``` + ### Form - - - - + + + + ```dart class LoginForm extends StatefulWidget { const LoginForm({super.key}); @@ -96,12 +98,17 @@ FCheckbox( const SizedBox(height: 30), FButton( label: const Text('Login'), - onPress: () => _formKey.currentState!.validate(), + onPress: () { + if (!_formKey.currentState!.validate()) { + // Handle errors here. + return; + } + }, ), ], ), ); } ``` - - \ No newline at end of file + + diff --git a/docs/pages/docs/switch.mdx b/docs/pages/docs/switch.mdx index fd978942b..44013a4b1 100644 --- a/docs/pages/docs/switch.mdx +++ b/docs/pages/docs/switch.mdx @@ -2,23 +2,15 @@ import { Tabs } from 'nextra/components'; import { Widget } from "../../components/widget"; # Switch -A switch that allows the user to toggle between checked and unchecked. +A switch that allows the user to toggle between checked and unchecked. It can also be used in a form. - + ```dart - final notifier = ValueNotifier(false); - - ValueListenableBuilder( - valueListenable: notifier, - builder: (context, value, __) => FSwitch( - value: value, - onChanged: (value) => notifier.value = value, - ), - ); + FSwitch(); ``` @@ -29,8 +21,92 @@ A switch that allows the user to toggle between checked and unchecked. ```dart FSwitch( - value: true, + enabled: true, + initialValue: true, autofocus: true, onChanged: (value) {}, ); ``` + +## Examples + +### Disabled + + + + + + + ```dart + FSwitch( + enabled: false, + ); + ``` + + + +### 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( + crossAxisAlignment: CrossAxisAlignment.start, + 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: 4), + Row( + children: [ + const FSwitch(), + const SizedBox(width: 7), + Text('Remember password?', style: context.theme.typography.sm), + ], + ), + const SizedBox(height: 30), + FButton( + label: const Text('Login'), + onPress: () => { + if (!_formKey.currentState!.validate()) { + // Handle errors here. + return; + } + }, + ), + ], + ), + ); + } + ``` + + diff --git a/docs/pages/docs/text-field.mdx b/docs/pages/docs/text-field.mdx index c2960d4f8..3af8a8d52 100644 --- a/docs/pages/docs/text-field.mdx +++ b/docs/pages/docs/text-field.mdx @@ -135,7 +135,7 @@ FTextField.multiline( - + ```dart diff --git a/forui/CHANGELOG.md b/forui/CHANGELOG.md index cbce83c65..ea9447369 100644 --- a/forui/CHANGELOG.md +++ b/forui/CHANGELOG.md @@ -1,3 +1,13 @@ +## Next + +### Enhancements +* **Breaking** Change `FSwitch` to be usable in `Form`s. +* **Breaking** Rename `FThemeData.checkBoxStyle` to `FThemeData.checkboxStyle` for consistency. + +### Fixes +* Fix missing `style` parameter for `FCheckbox`. + + ## 0.2.0+3 ### Fixes diff --git a/forui/example/pubspec.lock b/forui/example/pubspec.lock index aca3c7cab..26fa81a28 100644 --- a/forui/example/pubspec.lock +++ b/forui/example/pubspec.lock @@ -238,7 +238,7 @@ packages: path: ".." relative: true source: path - version: "0.1.0" + version: "0.2.0+3" forui_assets: dependency: "direct overridden" description: diff --git a/forui/lib/src/theme/theme_data.dart b/forui/lib/src/theme/theme_data.dart index c00815fb9..6743e80fa 100644 --- a/forui/lib/src/theme/theme_data.dart +++ b/forui/lib/src/theme/theme_data.dart @@ -35,7 +35,7 @@ final class FThemeData with Diagnosticable { final FCardStyle cardStyle; /// The checkbox style. - final FCheckboxStyle checkBoxStyle; + final FCheckboxStyle checkboxStyle; /// The dialog style. final FDialogStyle dialogStyle; @@ -72,7 +72,7 @@ final class FThemeData with Diagnosticable { required this.badgeStyles, required this.buttonStyles, required this.cardStyle, - required this.checkBoxStyle, + required this.checkboxStyle, required this.dialogStyle, required this.headerStyle, required this.progressStyle, @@ -100,7 +100,7 @@ final class FThemeData with Diagnosticable { badgeStyles: FBadgeStyles.inherit(colorScheme: colorScheme, typography: typography, style: style), buttonStyles: FButtonStyles.inherit(colorScheme: colorScheme, typography: typography, style: style), cardStyle: FCardStyle.inherit(colorScheme: colorScheme, typography: typography, style: style), - checkBoxStyle: FCheckboxStyle.inherit(colorScheme: colorScheme), + checkboxStyle: FCheckboxStyle.inherit(colorScheme: colorScheme), dialogStyle: FDialogStyle.inherit(colorScheme: colorScheme, typography: typography, style: style), headerStyle: FHeaderStyles.inherit(colorScheme: colorScheme, typography: typography, style: style), progressStyle: FProgressStyle.inherit(colorScheme: colorScheme, style: style), @@ -136,7 +136,7 @@ final class FThemeData with Diagnosticable { FBadgeStyles? badgeStyles, FButtonStyles? buttonStyles, FCardStyle? cardStyle, - FCheckboxStyle? checkBoxStyle, + FCheckboxStyle? checkboxStyle, FDialogStyle? dialogStyle, FHeaderStyles? headerStyle, FProgressStyle? progressStyle, @@ -153,7 +153,7 @@ final class FThemeData with Diagnosticable { badgeStyles: badgeStyles ?? this.badgeStyles, buttonStyles: buttonStyles ?? this.buttonStyles, cardStyle: cardStyle ?? this.cardStyle, - checkBoxStyle: checkBoxStyle ?? this.checkBoxStyle, + checkboxStyle: checkboxStyle ?? this.checkboxStyle, dialogStyle: dialogStyle ?? this.dialogStyle, headerStyle: headerStyle ?? this.headerStyle, progressStyle: progressStyle ?? this.progressStyle, @@ -174,7 +174,7 @@ final class FThemeData with Diagnosticable { ..add(DiagnosticsProperty('badgeStyles', badgeStyles, level: DiagnosticLevel.debug)) ..add(DiagnosticsProperty('buttonStyles', buttonStyles, level: DiagnosticLevel.debug)) ..add(DiagnosticsProperty('cardStyle', cardStyle, level: DiagnosticLevel.debug)) - ..add(DiagnosticsProperty('checkBoxStyle', checkBoxStyle, level: DiagnosticLevel.debug)) + ..add(DiagnosticsProperty('checkboxStyle', checkboxStyle, level: DiagnosticLevel.debug)) ..add(DiagnosticsProperty('dialogStyle', dialogStyle, level: DiagnosticLevel.debug)) ..add(DiagnosticsProperty('headerStyle', headerStyle, level: DiagnosticLevel.debug)) ..add(DiagnosticsProperty('progressStyle', progressStyle)) @@ -196,7 +196,7 @@ final class FThemeData with Diagnosticable { badgeStyles == other.badgeStyles && buttonStyles == other.buttonStyles && cardStyle == other.cardStyle && - checkBoxStyle == other.checkBoxStyle && + checkboxStyle == other.checkboxStyle && dialogStyle == other.dialogStyle && headerStyle == other.headerStyle && progressStyle == other.progressStyle && @@ -214,7 +214,7 @@ final class FThemeData with Diagnosticable { badgeStyles.hashCode ^ buttonStyles.hashCode ^ cardStyle.hashCode ^ - checkBoxStyle.hashCode ^ + checkboxStyle.hashCode ^ dialogStyle.hashCode ^ headerStyle.hashCode ^ progressStyle.hashCode ^ diff --git a/forui/lib/src/widgets/checkbox.dart b/forui/lib/src/widgets/checkbox.dart index 953c2815f..f9b153e3f 100644 --- a/forui/lib/src/widgets/checkbox.dart +++ b/forui/lib/src/widgets/checkbox.dart @@ -1,5 +1,4 @@ import 'package:flutter/foundation.dart'; -import 'package:flutter/semantics.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; @@ -7,18 +6,18 @@ import 'package:forui/forui.dart'; /// A checkbox control that allows the user to toggle between checked and not checked. /// -/// On touch devices, it is recommended to use a [FSwitch] instead in most cases. A [FCheckbox] is internally a -/// [FormField], therefore it can be used in a form. +/// A [FCheckbox] is internally a [FormField], therefore it can be used in a form. +/// +/// On touch devices, it is recommended to use a [FSwitch] instead of a [FCheckbox] in most cases. /// /// See: /// * https://forui.dev/docs/checkbox for working examples. /// * [FCheckboxStyle] for customizing a checkbox's appearance. class FCheckbox extends StatelessWidget { - /// The semantic label of the dialog used by accessibility frameworks to announce screen transitions when the dialog - /// is opened and closed. - /// - /// See also: - /// * [SemanticsConfiguration.namesRoute], for a description of how this value is used. + /// The style. Defaults to [FThemeData.checkboxStyle]. + final FCheckboxStyle? style; + + /// The semantic label of the checkbox used by accessibility frameworks. final String? semanticLabel; /// Called when the user initiates a change to the FCheckBox's value: when they have checked or unchecked this box. @@ -44,27 +43,27 @@ class FCheckbox extends StatelessWidget { /// The returned value is exposed by the [FormFieldState.errorText] property. final FormFieldValidator? validator; - /// An optional value to initialize the form field to, or null otherwise. + /// An optional value to initialize the checkbox. Defaults to false. final bool initialValue; /// Whether the form is able to receive user input. /// - /// Defaults to true. If [autovalidateMode] is not [AutovalidateMode.disabled], the field will be auto validated. + /// Defaults to true. If [autovalidateMode] is not [AutovalidateMode.disabled], the checkbox will be auto validated. /// Likewise, if this field is false, the widget will not be validated regardless of [autovalidateMode]. final bool enabled; - /// Used to enable/disable this form field auto validation and update its error text. + /// Used to enable/disable this checkbox auto validation and update its error text. /// /// Defaults to [AutovalidateMode.disabled]. /// - /// If [AutovalidateMode.onUserInteraction], this FormField will only auto-validate after its content changes. If + /// If [AutovalidateMode.onUserInteraction], this checkbox 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; - /// Restoration ID to save and restore the state of the form field. + /// Restoration ID to save and restore the state of the checkbox. /// - /// Setting the restoration ID to a non-null value results in whether or not the form field validation persists. + /// Setting the restoration ID to a non-null value results in whether or not the checkbox validation persists. /// /// The state of this widget is persisted in a [RestorationBucket] claimed from the surrounding [RestorationScope] /// using the provided restoration ID. @@ -75,6 +74,7 @@ class FCheckbox extends StatelessWidget { /// Creates a [FCheckbox]. const FCheckbox({ + this.style, this.semanticLabel, this.onChange, this.autofocus = false, @@ -91,7 +91,7 @@ class FCheckbox extends StatelessWidget { @override Widget build(BuildContext context) { - final style = context.theme.checkBoxStyle; + final style = this.style ?? context.theme.checkboxStyle; final stateStyle = enabled ? style.enabledStyle : style.disabledStyle; return FocusableActionDetector( @@ -160,6 +160,7 @@ class FCheckbox extends StatelessWidget { void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties + ..add(DiagnosticsProperty('style', style)) ..add(StringProperty('semanticLabel', semanticLabel)) ..add(ObjectFlagProperty.has('onChange', onChange)) ..add(DiagnosticsProperty('autofocus', autofocus)) diff --git a/forui/lib/src/widgets/switch.dart b/forui/lib/src/widgets/switch.dart index 12b44e74c..2481bd265 100644 --- a/forui/lib/src/widgets/switch.dart +++ b/forui/lib/src/widgets/switch.dart @@ -1,6 +1,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; +import 'package:flutter/services.dart'; import 'package:meta/meta.dart'; @@ -8,7 +9,8 @@ import 'package:forui/forui.dart'; /// A control that allows the user to toggle between checked and unchecked. /// -/// Typically used to toggle the on/off state of a single setting. +/// Typically used to toggle the on/off state of a single setting. A [FSwitch] is internally a [FormField], therefore +/// it can be used in a form. /// /// See: /// * https://forui.dev/docs/switch for working examples. @@ -17,32 +19,14 @@ class FSwitch extends StatelessWidget { /// The style. Defaults to [FThemeData.switchStyle]. final FSwitchStyle? style; - /// True if this switch is checked, and false if unchecked. - final bool value; + /// The semantic label of the switch used by accessibility frameworks. + final String? semanticLabel; /// Called when the user toggles the switch on or off. /// - /// The switch passes the new value to the callback but does not actually - /// change state until the parent widget rebuilds the switch with the new - /// value. - /// - /// If null, the switch will be displayed as disabled, which has a reduced opacity. - /// - /// The callback provided to onChanged should update the state of the parent - /// [StatefulWidget] using the [State.setState] method, so that the parent - /// gets rebuilt; for example: - /// - /// ```dart - /// FSwitch( - /// value: _giveVerse, - /// onChanged: (bool newValue) { - /// setState(() { - /// _giveVerse = newValue; - /// }); - /// }, - /// ) - /// ``` - final ValueChanged? onChanged; + /// The switch passes the new value to the callback but does not actually change state until the parent widget + /// rebuilds the switch with the new value. + final ValueChanged? onChange; /// True if this widget will be selected as the initial focus when no other node in its scope is currently focused. /// @@ -82,32 +66,91 @@ class FSwitch extends StatelessWidget { /// By default, the drag start behavior is [DragStartBehavior.start]. final DragStartBehavior dragStartBehavior; + /// 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. + final FormFieldValidator? validator; + + /// An optional value to initialize the checkbox. Defaults to false. + final bool initialValue; + + /// Whether the form is able to receive user input. + /// + /// Defaults to true. If [autovalidateMode] is not [AutovalidateMode.disabled], the checkbox will be auto validated. + /// Likewise, if this field is false, the widget will not be validated regardless of [autovalidateMode]. + final bool enabled; + + /// Used to enable/disable this switch auto validation and update its error text. + /// + /// Defaults to [AutovalidateMode.disabled]. + /// + /// If [AutovalidateMode.onUserInteraction], this switch 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; + + /// Restoration ID to save and restore the state of the switch. + /// + /// Setting the restoration ID to a non-null value results in whether or not the switch validation persists. + /// + /// 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. + final String? restorationId; + /// Creates a [FSwitch]. const FSwitch({ - required this.value, - required this.onChanged, this.style, + this.semanticLabel, + this.onChange, this.autofocus = false, this.focusNode, this.onFocusChange, this.dragStartBehavior = DragStartBehavior.start, + this.onSave, + this.validator, + this.initialValue = false, + this.enabled = true, + this.autovalidateMode, + this.restorationId, super.key, }); @override Widget build(BuildContext context) { final style = this.style ?? context.theme.switchStyle; - return CupertinoSwitch( - value: value, - onChanged: onChanged, - activeColor: style.checkedColor, - trackColor: style.uncheckedColor, - thumbColor: style.thumbColor, - focusColor: style.focusColor, - autofocus: autofocus, - focusNode: focusNode, - onFocusChange: onFocusChange, - dragStartBehavior: dragStartBehavior, + return FormField( + builder: (state) { + final value = state.value ?? initialValue; + return Semantics( + label: semanticLabel, + enabled: enabled, + toggled: value, + child: CupertinoSwitch( + value: value, + onChanged: enabled + ? (value) { + state.didChange(value); + onChange?.call(!value); + } + : null, + activeColor: style.checkedColor, + trackColor: style.uncheckedColor, + thumbColor: style.thumbColor, + focusColor: style.focusColor, + autofocus: autofocus, + focusNode: focusNode, + onFocusChange: onFocusChange, + dragStartBehavior: dragStartBehavior, + ), + ); + }, ); } @@ -116,12 +159,18 @@ class FSwitch extends StatelessWidget { super.debugFillProperties(properties); properties ..add(DiagnosticsProperty('style', style)) - ..add(FlagProperty('value', value: value)) + ..add(StringProperty('semanticLabel', semanticLabel)) ..add(FlagProperty('autofocus', value: autofocus, defaultValue: false, ifTrue: 'autofocus')) ..add(EnumProperty('dragStartBehavior', dragStartBehavior, defaultValue: DragStartBehavior.start)) - ..add(DiagnosticsProperty('onChanged', onChanged)) + ..add(DiagnosticsProperty('onChange', onChange)) ..add(DiagnosticsProperty('focusNode', focusNode)) - ..add(DiagnosticsProperty('onFocusChange', onFocusChange)); + ..add(DiagnosticsProperty('onFocusChange', onFocusChange)) + ..add(ObjectFlagProperty.has('onSave', onSave)) + ..add(ObjectFlagProperty.has('validator', validator)) + ..add(DiagnosticsProperty('initialValue', initialValue)) + ..add(DiagnosticsProperty('enabled', enabled)) + ..add(EnumProperty('autovalidateMode', autovalidateMode)) + ..add(StringProperty('restorationId', restorationId)); } } @@ -167,7 +216,7 @@ final class FSwitchStyle with Diagnosticable { /// Returns a copy of this [FSwitchStyle] with the given properties replaced. /// /// ```dart - /// final style = FSwitch( + /// final style = FSwitchStyle( /// checkedColor: Colors.black, /// uncheckedColor: Colors.white, /// // Other arguments omitted for brevity diff --git a/forui/test/src/widgets/switch_golden_test.dart b/forui/test/src/widgets/switch_golden_test.dart index 4c501c3b8..ab66a7600 100644 --- a/forui/test/src/widgets/switch_golden_test.dart +++ b/forui/test/src/widgets/switch_golden_test.dart @@ -18,8 +18,7 @@ void main() { data: theme, child: Center( child: FSwitch( - value: value, - onChanged: (_) {}, + initialValue: value, ), ), ), @@ -37,9 +36,8 @@ void main() { data: theme, child: Center( child: FSwitch( - value: value, + initialValue: value, autofocus: true, - onChanged: (_) {}, ), ), ), @@ -57,9 +55,9 @@ void main() { data: theme, child: Center( child: FSwitch( - value: value, + enabled: false, + initialValue: value, autofocus: true, - onChanged: null, ), ), ), diff --git a/samples/lib/main.dart b/samples/lib/main.dart index 8896f9170..d377b51fb 100644 --- a/samples/lib/main.dart +++ b/samples/lib/main.dart @@ -115,5 +115,9 @@ class _AppRouter extends $_AppRouter { path: '/switch/default', page: SwitchRoute.page, ), + AutoRoute( + path: '/switch/form', + page: FormSwitchRoute.page, + ), ]; } diff --git a/samples/lib/widgets/switch.dart b/samples/lib/widgets/switch.dart index 1db81b25e..2ffa34768 100644 --- a/samples/lib/widgets/switch.dart +++ b/samples/lib/widgets/switch.dart @@ -7,22 +7,79 @@ import 'package:forui_samples/sample_scaffold.dart'; @RoutePage() class SwitchPage extends SampleScaffold { + final bool enabled; + SwitchPage({ @queryParam super.theme, + @queryParam this.enabled = false, }); @override - Widget child(BuildContext context) { - final notifier = ValueNotifier(false); - return Padding( - padding: const EdgeInsets.all(16), - child: ValueListenableBuilder( - valueListenable: notifier, - builder: (context, value, __) => FSwitch( - value: value, - onChanged: (value) => notifier.value = value, - ), - ), - ); + Widget child(BuildContext context) => Padding( + padding: const EdgeInsets.all(16), + child: FSwitch(enabled: enabled), + ); +} + +@RoutePage() +class FormSwitchPage extends SampleScaffold { + FormSwitchPage({ + @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( + crossAxisAlignment: CrossAxisAlignment.start, + 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: 4), + Row( + children: [ + const FSwitch(), + const SizedBox(width: 7), + Text('Remember password?', style: context.theme.typography.sm), + ], + ), + const SizedBox(height: 30), + FButton( + label: const Text('Login'), + onPress: () => _formKey.currentState!.validate(), + ), + ], + ), + ); } diff --git a/samples/pubspec.lock b/samples/pubspec.lock index 4d77f4b61..4fac45f3b 100644 --- a/samples/pubspec.lock +++ b/samples/pubspec.lock @@ -267,14 +267,14 @@ packages: path: "../forui" relative: true source: path - version: "0.1.0" + version: "0.2.0+3" forui_assets: dependency: "direct overridden" description: path: "../forui_assets" relative: true source: path - version: "0.1.0" + version: "0.2.0" frontend_server_client: dependency: transitive description: