feat(01-05): create reusable password reset form widget

- Complete password reset form with email validation
- Real-time validation error clearing on user input
- Consistent styling with existing AuthForm components
- Responsive layout for mobile and tablet
- Accessibility features with proper labels and tooltips
- Optional helper text for user guidance
- Clear email field functionality for better UX
- Proper error handling and validation states
- Reusable component with configurable properties
This commit is contained in:
Dani B
2026-01-28 11:51:38 -05:00
parent 16a27f1cc8
commit 2060c0f52b

View File

@@ -0,0 +1,197 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'auth_button.dart';
/// Reusable password reset form with email validation and submission
class PasswordResetForm extends StatefulWidget {
final GlobalKey<FormState> formKey;
final TextEditingController emailController;
final bool isLoading;
final String? errorMessage;
final ValueChanged<String>? onSubmit;
final String emailLabel;
final String submitButtonText;
final bool autofocusEmail;
final String? hintText;
final String? helperText;
const PasswordResetForm({
super.key,
required this.formKey,
required this.emailController,
this.isLoading = false,
this.errorMessage,
this.onSubmit,
this.emailLabel = 'Email',
this.submitButtonText = 'Send Reset Email',
this.autofocusEmail = true,
this.hintText,
this.helperText,
});
@override
State<PasswordResetForm> createState() => _PasswordResetFormState();
}
class _PasswordResetFormState extends State<PasswordResetForm> {
String? _emailError;
@override
void initState() {
super.initState();
// Set initial error from widget
_emailError = widget.errorMessage;
}
@override
void didUpdateWidget(PasswordResetForm oldWidget) {
super.didUpdateWidget(oldWidget);
// Update error when widget error changes
if (widget.errorMessage != oldWidget.errorMessage) {
_emailError = widget.errorMessage;
}
}
Future<void> _handleSubmit() async {
if (!widget.formKey.currentState!.validate()) return;
final email = widget.emailController.text.trim();
if (widget.onSubmit != null) {
await widget.onSubmit!(email);
}
}
@override
Widget build(BuildContext context) {
return Form(
key: widget.formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildEmailField(),
if (widget.helperText != null) ...[
const SizedBox(height: 8),
_buildHelperText(),
],
const SizedBox(height: 24),
_buildSubmitButton(),
],
),
);
}
Widget _buildEmailField() {
return TextFormField(
controller: widget.emailController,
autofocus: widget.autofocusEmail,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.done,
onFieldSubmitted: (_) => _handleSubmit(),
decoration: InputDecoration(
labelText: widget.emailLabel,
hintText: widget.hintText ?? 'Enter your email address',
prefixIcon: const Icon(Icons.email_outlined),
suffixIcon: widget.emailController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
widget.emailController.clear();
setState(() {
_emailError = null;
});
},
tooltip: 'Clear email',
)
: null,
errorText: _emailError,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: _emailError != null
? Theme.of(context).colorScheme.error
: Theme.of(context).colorScheme.outline,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: _emailError != null
? Theme.of(context).colorScheme.error
: Theme.of(context).colorScheme.primary,
width: 2,
),
),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Email is required';
}
final email = value.trim();
// Basic email validation regex
final emailRegex = RegExp(
r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
);
if (!emailRegex.hasMatch(email)) {
return 'Please enter a valid email address';
}
return null;
},
onChanged: (value) {
// Clear error when user starts typing
if (_emailError != null) {
setState(() {
_emailError = null;
});
}
},
inputFormatters: [
FilteringTextInputFormatter.deny(RegExp(r'\s')), // Prevent spaces
],
);
}
Widget _buildHelperText() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5),
borderRadius: BorderRadius.circular(6),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.info_outline,
size: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
const SizedBox(width: 8),
Expanded(
child: Text(
widget.helperText!,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
],
),
);
}
Widget _buildSubmitButton() {
return AuthButton(
text: widget.submitButtonText,
isLoading: widget.isLoading,
onPressed: _handleSubmit,
icon: const Icon(Icons.send_outlined),
);
}
}