From 5c1ec1d40625c9165b967067cfe542b3dc964e50 Mon Sep 17 00:00:00 2001 From: mikecoomber <58986130+mikecoomber@users.noreply.github.com> Date: Mon, 24 Jun 2024 10:25:54 +0100 Subject: [PATCH] refactor: Password input now uses text input (#109) * update password input * fixed tests --- .../components/password_input_example.dart | 28 ++- example/test/password_input_test.dart | 24 +- .../components/password_input_widgetbook.dart | 10 +- .../components/password/password_input.dart | 234 +++++------------- lib/src/components/text_input/text_input.dart | 26 +- 5 files changed, 123 insertions(+), 199 deletions(-) diff --git a/example/lib/pages/components/password_input_example.dart b/example/lib/pages/components/password_input_example.dart index 2c6d76ba..24dcea43 100644 --- a/example/lib/pages/components/password_input_example.dart +++ b/example/lib/pages/components/password_input_example.dart @@ -38,11 +38,8 @@ class _PasswordInputExampleState extends State { ZetaPasswordInput( size: ZetaWidgetSize.medium, rounded: false, - footerText: 'Error state is triggered if the password contains digits', - footerIcon: ZetaIcons.info_round, hintText: 'Password', controller: _passwordController, - onChanged: (value) => _formKey.currentState?.validate(), validator: (value) { if (value != null) { final regExp = RegExp(r'\d'); @@ -51,6 +48,12 @@ class _PasswordInputExampleState extends State { return null; }, ), + ZetaButton.primary( + label: 'Validate', + onPressed: () { + _formKey.currentState?.validate(); + }, + ), SizedBox(height: ZetaSpacing.xl_6), ...passwordInputExampleRow(ZetaWidgetSize.large), Divider(height: ZetaSpacing.xl_10), @@ -67,17 +70,26 @@ class _PasswordInputExampleState extends State { List passwordInputExampleRow(ZetaWidgetSize size, {bool rounded = true}) { return [ - ZetaPasswordInput(size: size, hintText: 'Password', rounded: rounded), + ZetaPasswordInput( + size: size, + hintText: 'Password', + rounded: rounded, + placeholder: 'Password', + ), SizedBox(height: 20), - ZetaPasswordInput(rounded: rounded, size: size, hintText: 'Password', enabled: false), + ZetaPasswordInput( + rounded: rounded, + size: size, + placeholder: 'Password', + disabled: true, + ), SizedBox(height: 20), ZetaPasswordInput( size: size, label: 'Label', - hintText: 'Password', - footerText: 'Default hint text', + placeholder: 'Password', + hintText: 'Default hint text', rounded: rounded, - footerIcon: ZetaIcons.info_round, ), ]; } diff --git a/example/test/password_input_test.dart b/example/test/password_input_test.dart index 765ede1a..e6335a79 100644 --- a/example/test/password_input_test.dart +++ b/example/test/password_input_test.dart @@ -20,12 +20,12 @@ void main() { widget: ZetaPasswordInput(), ), ); - final obscureIconOff = find.byIcon(ZetaIcons.visibility_off_sharp); + final obscureIconOff = find.byIcon(ZetaIcons.visibility_off_round); expect(obscureIconOff, findsOneWidget); await tester.tap(obscureIconOff); await tester.pump(); - final obscureIconOn = find.byIcon(ZetaIcons.visibility_sharp); + final obscureIconOn = find.byIcon(ZetaIcons.visibility_round); expect(obscureIconOn, findsOneWidget); }); @@ -36,19 +36,25 @@ void main() { return null; } + final controller = TextEditingController(); + controller.text = 'password123'; + final formKey = GlobalKey(); + await tester.pumpWidget( TestWidget( - widget: ZetaPasswordInput( - controller: TextEditingController(), - validator: testValidator, + widget: Form( + key: formKey, + child: ZetaPasswordInput( + controller: controller, + validator: testValidator, + ), ), ), ); - - final passwordField = find.byType(TextFormField); - await tester.enterText(passwordField, 'password12'); + formKey.currentState?.validate(); await tester.pump(); - expect(find.text('Error'), findsOneWidget); + // There will be two matches for the error text as the form field itself contains a hidden one. + expect(find.text('Error'), findsExactly(2)); }); } diff --git a/example/widgetbook/pages/components/password_input_widgetbook.dart b/example/widgetbook/pages/components/password_input_widgetbook.dart index f6714267..3c932313 100644 --- a/example/widgetbook/pages/components/password_input_widgetbook.dart +++ b/example/widgetbook/pages/components/password_input_widgetbook.dart @@ -33,7 +33,7 @@ class _PasswordState extends State<_Password> { ConstrainedBox( constraints: BoxConstraints(maxWidth: 328), child: ZetaPasswordInput( - enabled: context.knobs.boolean(label: 'Enabled', initialValue: true), + disabled: disabledKnob(context), obscureText: context.knobs.boolean(label: 'Obscure text', initialValue: true), size: context.knobs.list( label: 'Size', @@ -41,12 +41,10 @@ class _PasswordState extends State<_Password> { labelBuilder: enumLabelBuilder, ), rounded: rounded, - footerIcon: - iconKnob(context, initial: ZetaIcons.star_half_round, name: 'Footer icon', rounded: rounded), - footerText: context.knobs.string(label: 'Footer Text'), - hintText: context.knobs.string(label: 'Hint text'), + hintText: context.knobs.string(label: 'Hint Text'), + placeholder: context.knobs.string(label: 'Placeholder'), label: context.knobs.string(label: 'Label'), - onChanged: (_) => _formKey.currentState?.validate(), + onChange: (_) => _formKey.currentState?.validate(), validator: (_) => enableValidation ? validationString : null, controller: _passwordController, ), diff --git a/lib/src/components/password/password_input.dart b/lib/src/components/password/password_input.dart index fd32edcc..775c94f1 100644 --- a/lib/src/components/password/password_input.dart +++ b/lib/src/components/password/password_input.dart @@ -2,64 +2,56 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import '../../../zeta_flutter.dart'; - -///Extension for password visibility state handling -extension on ValueNotifier { - void toggle() => value = !value; -} +import '../../interfaces/form_field.dart'; ///Zeta Password Input -class ZetaPasswordInput extends StatefulWidget { +class ZetaPasswordInput extends ZetaFormField { ///Constructs [ZetaPasswordInput] const ZetaPasswordInput({ this.size = ZetaWidgetSize.large, this.validator, - this.onChanged, + super.onChange, + this.onSubmit, this.obscureText = true, - this.enabled = true, + @Deprecated('Use disabled instead. ' 'This property has been renamed as of 0.11.2') bool enabled = true, + super.disabled = false, this.controller, this.hintText, + this.errorText, this.label, - this.footerText, - this.footerIcon, - this.rounded = false, + this.placeholder, + super.rounded, super.key, + super.initialValue, + super.requirementLevel = ZetaFormFieldRequirement.none, }); - /// Controls the text being edited. + /// {@macro text-input-controller} final TextEditingController? controller; - /// Whether the text is obscured. Useful for passwords. Defaults to true. + /// {@macro text-input-obscure-text} final bool obscureText; - /// Text that suggests what sort of input the field accepts. - final String? hintText; - - /// Whether the input field is enabled or disabled. Defaults to true. - final bool enabled; + /// {@macro text-input-placeholder} + final String? placeholder; - /// Optional label text to display above the input field. + /// {@macro text-input-label} final String? label; - /// Optional footer text to display below the input field. - final String? footerText; - - /// Optional icon to display beside the footer text. - final IconData? footerIcon; + /// {@macro text-input-hint-text} + final String? hintText; - /// {@macro zeta-component-rounded} - final bool rounded; + /// {@macro text-input-error-text} + final String? errorText; - /// Defines the size of the input field. Can be [ZetaWidgetSize.small], [ZetaWidgetSize.medium], or [ZetaWidgetSize.large]. - /// Defaults to [ZetaWidgetSize.large]. + /// {@macro text-input-size} final ZetaWidgetSize size; - /// An optional method that validates an input. Returns an error string to - /// display if the input is invalid, or null otherwise. + /// {@macro text-input-validator} final String? Function(String?)? validator; - /// Called when the user initiates a change to the [ZetaPasswordInput] - final void Function(String)? onChanged; + /// {@macro text-input-on-submit} + final void Function(String? val)? onSubmit; @override State createState() => _ZetaPasswordInputState(); @@ -73,172 +65,64 @@ class ZetaPasswordInput extends StatefulWidget { ) ..add(DiagnosticsProperty('obscureText', obscureText)) ..add(StringProperty('hintText', hintText)) - ..add(DiagnosticsProperty('enabled', enabled)) ..add(StringProperty('label', label)) - ..add(StringProperty('footerText', footerText)) - ..add(DiagnosticsProperty('footerIcon', footerIcon)) + ..add(StringProperty('footerText', hintText)) ..add( ObjectFlagProperty.has( 'validator', validator, ), ) - ..add( - ObjectFlagProperty.has( - 'onChanged', - onChanged, - ), - ) ..add(EnumProperty('size', size)) - ..add(DiagnosticsProperty('rounded', rounded)); + ..add(DiagnosticsProperty('rounded', rounded)) + ..add(StringProperty('placeholder', placeholder)) + ..add(ObjectFlagProperty.has('onSubmit', onSubmit)) + ..add(StringProperty('errorText', errorText)); } } class _ZetaPasswordInputState extends State { - late final ValueNotifier _obscureTextNotifier; - String? _errorMessage; + late bool _obscureText; @override void initState() { super.initState(); - _obscureTextNotifier = ValueNotifier(widget.obscureText); - widget.controller?.addListener(_validate); + _obscureText = widget.obscureText; } - void _validate() { - if (widget.validator != null && widget.controller != null) { - setState(() => _errorMessage = widget.validator!(widget.controller!.text)); + IconData get _icon { + if (context.rounded) { + return !_obscureText ? ZetaIcons.visibility_round : ZetaIcons.visibility_off_round; + } else { + return !_obscureText ? ZetaIcons.visibility_sharp : ZetaIcons.visibility_off_sharp; } } - @override - void dispose() { - _obscureTextNotifier.dispose(); - widget.controller?.removeListener(_validate); - super.dispose(); - } - @override Widget build(BuildContext context) { - final theme = Zeta.of(context); - final defaultBorderColor = _errorMessage != null ? theme.colors.error.border : theme.colors.borderDefault; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (widget.label != null) - Padding( - padding: const EdgeInsets.symmetric(vertical: ZetaSpacing.minimum), - child: Text(widget.label!, style: ZetaTextStyles.bodyMedium), - ), - SizedBox( - height: _inputHeight, - child: ValueListenableBuilder( - valueListenable: _obscureTextNotifier, - builder: (context, obscureValue, child) { - return TextFormField( - controller: widget.controller, - obscureText: obscureValue, - onChanged: widget.onChanged, - style: _textStyle, - decoration: InputDecoration( - contentPadding: const EdgeInsets.symmetric(horizontal: ZetaSpacing.small), - filled: true, - fillColor: _getBackgroundColor(theme.colors), - enabledBorder: _getBorder(defaultBorderColor), - focusedBorder: _getBorder( - _errorMessage != null ? theme.colors.error.border : theme.colors.primary.border, - width: ZetaSpacingBase.x0_5, - ), - disabledBorder: _getBorder(theme.colors.borderDefault), - border: _getBorder(defaultBorderColor), - enabled: widget.enabled, - hintText: widget.hintText, - hintStyle: _textStyle, - suffixIcon: ValueListenableBuilder( - valueListenable: _obscureTextNotifier, - builder: (context, value, child) { - return IconButton( - padding: const EdgeInsets.symmetric(vertical: ZetaSpacing.minimum), - icon: Icon( - value ? ZetaIcons.visibility_off_sharp : ZetaIcons.visibility_sharp, - size: widget.size == ZetaWidgetSize.small ? ZetaSpacing.large : ZetaSpacing.xl_1, - ), - color: widget.enabled ? theme.colors.iconDefault : theme.colors.iconDisabled, - onPressed: () => _obscureTextNotifier.toggle(), - ); - }, - ), - ), - ); - }, - ), - ), - if (widget.footerText != null || widget.footerIcon != null || _errorMessage != null) - Padding( - padding: const EdgeInsets.only(top: ZetaSpacing.minimum), - child: Row( - children: [ - if (_errorMessage != null) ...[ - Icon( - ZetaIcons.error_round, - size: ZetaSpacing.large, - color: theme.colors.error.border, - ), - const SizedBox(width: ZetaSpacing.minimum), - Text( - _errorMessage!, - style: ZetaTextStyles.bodySmall.apply(color: theme.colors.error.border), - ), - ], - if (_errorMessage == null && widget.footerIcon != null) ...[ - Icon( - widget.footerIcon, - size: ZetaSpacing.large, - color: widget.enabled ? theme.colors.iconDefault : theme.colors.iconDisabled, - ), - const SizedBox(width: ZetaSpacing.minimum), - ], - if (_errorMessage == null && widget.footerText != null) ...[ - Text( - widget.footerText!, - style: ZetaTextStyles.bodySmall.apply( - color: widget.enabled ? theme.colors.textSubtle : theme.colors.textDefault, - ), - ), - ], - ], - ), - ), - ], - ); - } - - OutlineInputBorder _getBorder(Color color, {double width = 1}) { - return OutlineInputBorder( - borderSide: BorderSide(color: color, width: width), - borderRadius: widget.rounded ? ZetaRadius.minimal : ZetaRadius.none, + final rounded = context.rounded; + + return ZetaTextInput( + size: widget.size, + rounded: rounded, + controller: widget.controller, + validator: widget.validator, + hintText: widget.hintText, + placeholder: widget.placeholder, + label: widget.label, + onChange: widget.onChange, + errorText: widget.errorText, + onSubmit: widget.onSubmit, + disabled: widget.disabled, + obscureText: _obscureText, + suffix: IconButton( + icon: Icon(_icon), + onPressed: () { + setState(() { + _obscureText = !_obscureText; + }); + }, + ), ); } - - Color _getBackgroundColor(ZetaColors zetaColors) { - if (!widget.enabled) return zetaColors.surfaceDisabled; - if (_errorMessage != null) return zetaColors.error.surface; - return zetaColors.surfacePrimary; - } - - double get _inputHeight { - switch (widget.size) { - case ZetaWidgetSize.small: - return ZetaSpacing.xl_4; - case ZetaWidgetSize.medium: - return ZetaSpacing.xl_6; - case ZetaWidgetSize.large: - return ZetaSpacing.xl_8; - } - } - - TextStyle get _textStyle => - (widget.size == ZetaWidgetSize.small ? ZetaTextStyles.bodyMedium : ZetaTextStyles.bodyLarge) - .copyWith(height: 1.2); } diff --git a/lib/src/components/text_input/text_input.dart b/lib/src/components/text_input/text_input.dart index 3dedd1f3..875ba52a 100644 --- a/lib/src/components/text_input/text_input.dart +++ b/lib/src/components/text_input/text_input.dart @@ -36,30 +36,45 @@ class ZetaTextInput extends ZetaFormField { this.suffixText, this.suffixTextStyle, this.onSubmit, + this.obscureText = false, }) : assert(initialValue == null || controller == null, 'Only one of initial value and controller can be accepted.'), assert(prefix == null || prefixText == null, 'Only one of prefix or prefixText can be accepted.'), assert(suffix == null || suffixText == null, 'Only one of suffix or suffixText can be accepted.'); + /// {@template text-input-label} /// The label displayed above the input. + /// {@endtemplate} final String? label; + /// {@template text-input-on-submit} /// Called when the input is submitted. + /// {@endtemplate} final void Function(String? val)? onSubmit; + /// {@template text-input-hint-text} /// The hint text displayed below the input. + /// {@endtemplate} final String? hintText; + /// {@template text-input-placeholder} /// The placeholder text displayed in the input. + /// {@endtemplate} final String? placeholder; + /// {@template text-input-error-text} /// The error text shown beneath the input. Replaces [hintText]. + /// {@endtemplate} final String? errorText; + /// {@template text-input-controller} /// The controller given to the input. Cannot be given in addition to [initialValue]. + /// {@endtemplate} final TextEditingController? controller; + /// {@template text-input-validator} /// The validator passed to the input. Should return the error message to be displayed below the input. /// Should return null if there is no error. + /// {@endtemplate} final String? Function(String?)? validator; /// The widget displayed at the end of the input. Cannot be given in addition to [suffixText]. @@ -80,12 +95,19 @@ class ZetaTextInput extends ZetaFormField { /// The style applied to [prefixText]. final TextStyle? prefixTextStyle; + ///{@template text-input-size} /// The size of the input. + /// {@endtemplate} final ZetaWidgetSize size; /// The input formatters given to the text input. final List? inputFormatters; + /// {@template text-input-obscure-text} + /// Obscures the text within the input. + /// {@endtemplate} + final bool obscureText; + @override State createState() => ZetaTextInputState(); @override @@ -109,7 +131,8 @@ class ZetaTextInput extends ZetaFormField { ..add(DiagnosticsProperty('disabled', disabled)) ..add(IterableProperty('inputFormatters', inputFormatters)) ..add(EnumProperty('requirementLevel', requirementLevel)) - ..add(ObjectFlagProperty.has('onSubmit', onSubmit)); + ..add(ObjectFlagProperty.has('onSubmit', onSubmit)) + ..add(DiagnosticsProperty('obscureText', obscureText)); } } @@ -298,6 +321,7 @@ class ZetaTextInputState extends State implements ZetaFormFieldSt onChanged: widget.onChange, style: _baseTextStyle, cursorErrorColor: _colors.error, + obscureText: widget.obscureText, decoration: InputDecoration( isDense: true, contentPadding: _contentPadding,