Files
unionflow-mobile-apps/lib/shared/widgets/validated_text_field.dart
dahoud 7cd7c6fc9e feat(shared): legacy presentation/ + shared design system + widgets
- lib/presentation : pages legacy (explore/network, notifications) avec BLoC
- lib/shared/design_system : UnionFlow Design System v2 (tokens, components)
  + MD3 tokens + module_colors par feature
- lib/shared/widgets : widgets transversaux (core_card, core_shimmer,
  error_widget, loading_widget, powered_by_lions_dev, etc.)
- lib/shared/constants + utils
2026-04-15 20:27:23 +00:00

339 lines
9.5 KiB
Dart

/// Reusable validated text field with consistent styling
library validated_text_field;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../design_system/tokens/app_colors.dart';
/// Validated text field with consistent styling and behavior
class ValidatedTextField extends StatelessWidget {
final TextEditingController? controller;
final String? labelText;
final String? hintText;
final String? helperText;
final String? initialValue;
final String? Function(String?)? validator;
final void Function(String)? onChanged;
final void Function(String?)? onSaved;
final TextInputType? keyboardType;
final TextInputAction? textInputAction;
final bool obscureText;
final bool enabled;
final bool readOnly;
final int? maxLines;
final int? minLines;
final int? maxLength;
final Widget? prefixIcon;
final Widget? suffixIcon;
final List<TextInputFormatter>? inputFormatters;
final FocusNode? focusNode;
final void Function()? onEditingComplete;
final void Function(String)? onFieldSubmitted;
final AutovalidateMode? autovalidateMode;
final bool showCounter;
const ValidatedTextField({
super.key,
this.controller,
this.labelText,
this.hintText,
this.helperText,
this.initialValue,
this.validator,
this.onChanged,
this.onSaved,
this.keyboardType,
this.textInputAction,
this.obscureText = false,
this.enabled = true,
this.readOnly = false,
this.maxLines = 1,
this.minLines,
this.maxLength,
this.prefixIcon,
this.suffixIcon,
this.inputFormatters,
this.focusNode,
this.onEditingComplete,
this.onFieldSubmitted,
this.autovalidateMode,
this.showCounter = true,
});
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return TextFormField(
controller: controller,
initialValue: initialValue,
decoration: InputDecoration(
labelText: labelText,
hintText: hintText,
helperText: helperText,
prefixIcon: prefixIcon,
suffixIcon: suffixIcon,
border: const OutlineInputBorder(),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: isDark ? AppColors.borderDark : AppColors.border,
width: 1.0,
),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: AppColors.primary,
width: 2.0,
),
),
errorBorder: OutlineInputBorder(
borderSide: BorderSide(
color: AppColors.error,
width: 1.0,
),
),
focusedErrorBorder: OutlineInputBorder(
borderSide: BorderSide(
color: AppColors.error,
width: 2.0,
),
),
filled: !enabled,
fillColor: !enabled
? (isDark ? AppColors.surfaceVariantDark : AppColors.backgroundSubtle)
: null,
counterText: showCounter ? null : '',
),
validator: validator,
onChanged: onChanged,
onSaved: onSaved,
keyboardType: keyboardType,
textInputAction: textInputAction,
obscureText: obscureText,
enabled: enabled,
readOnly: readOnly,
maxLines: maxLines,
minLines: minLines,
maxLength: maxLength,
inputFormatters: inputFormatters,
focusNode: focusNode,
onEditingComplete: onEditingComplete,
onFieldSubmitted: onFieldSubmitted,
autovalidateMode: autovalidateMode,
);
}
}
/// Validated amount field with currency formatting
class ValidatedAmountField extends StatelessWidget {
final TextEditingController? controller;
final String? labelText;
final String? hintText;
final String? initialValue;
final String? Function(String?)? validator;
final void Function(String)? onChanged;
final void Function(String?)? onSaved;
final bool enabled;
final String currencySymbol;
final FocusNode? focusNode;
const ValidatedAmountField({
super.key,
this.controller,
this.labelText,
this.hintText,
this.initialValue,
this.validator,
this.onChanged,
this.onSaved,
this.enabled = true,
this.currencySymbol = 'FCFA',
this.focusNode,
});
@override
Widget build(BuildContext context) {
return ValidatedTextField(
controller: controller,
initialValue: initialValue,
labelText: labelText,
hintText: hintText,
helperText: 'Entrez un montant positif (max 2 décimales)',
validator: validator,
onChanged: onChanged,
onSaved: onSaved,
enabled: enabled,
focusNode: focusNode,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
textInputAction: TextInputAction.next,
suffixIcon: Padding(
padding: const EdgeInsets.all(12.0),
child: Text(
currencySymbol,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: AppColors.textTertiary,
),
),
),
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,2}')),
],
);
}
}
/// Validated dropdown field
class ValidatedDropdownField<T> extends StatelessWidget {
final T? value;
final List<DropdownMenuItem<T>> items;
final String? labelText;
final String? hintText;
final String? helperText;
final String? Function(T?)? validator;
final void Function(T?)? onChanged;
final void Function(T?)? onSaved;
final bool enabled;
const ValidatedDropdownField({
super.key,
this.value,
required this.items,
this.labelText,
this.hintText,
this.helperText,
this.validator,
this.onChanged,
this.onSaved,
this.enabled = true,
});
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return DropdownButtonFormField<T>(
value: value,
items: items,
decoration: InputDecoration(
labelText: labelText,
hintText: hintText,
helperText: helperText,
border: const OutlineInputBorder(),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: isDark ? AppColors.borderDark : AppColors.border,
width: 1.0,
),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: AppColors.primary,
width: 2.0,
),
),
errorBorder: OutlineInputBorder(
borderSide: BorderSide(
color: AppColors.error,
width: 1.0,
),
),
filled: !enabled,
fillColor: !enabled
? (isDark ? AppColors.surfaceVariantDark : AppColors.backgroundSubtle)
: null,
),
validator: validator,
onChanged: enabled ? onChanged : null,
onSaved: onSaved,
);
}
}
/// Validated date picker field
class ValidatedDateField extends StatelessWidget {
final DateTime? selectedDate;
final String? labelText;
final String? hintText;
final String? helperText;
final String? Function(DateTime?)? validator;
final void Function(DateTime)? onChanged;
final DateTime? firstDate;
final DateTime? lastDate;
final bool enabled;
const ValidatedDateField({
super.key,
this.selectedDate,
this.labelText,
this.hintText,
this.helperText,
this.validator,
this.onChanged,
this.firstDate,
this.lastDate,
this.enabled = true,
});
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return InkWell(
onTap: enabled
? () async {
final date = await showDatePicker(
context: context,
initialDate: selectedDate ?? DateTime.now(),
firstDate: firstDate ?? DateTime(2000),
lastDate: lastDate ?? DateTime(2100),
);
if (date != null && onChanged != null) {
onChanged!(date);
}
}
: null,
child: InputDecorator(
decoration: InputDecoration(
labelText: labelText,
hintText: hintText,
helperText: helperText,
suffixIcon: const Icon(Icons.calendar_today),
border: const OutlineInputBorder(),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: isDark ? AppColors.borderDark : AppColors.border,
width: 1.0,
),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: AppColors.primary,
width: 2.0,
),
),
errorBorder: OutlineInputBorder(
borderSide: BorderSide(
color: AppColors.error,
width: 1.0,
),
),
filled: !enabled,
fillColor: !enabled
? (isDark ? AppColors.surfaceVariantDark : AppColors.backgroundSubtle)
: null,
errorText: validator != null ? validator!(selectedDate) : null,
),
child: Text(
selectedDate != null
? '${selectedDate!.day}/${selectedDate!.month}/${selectedDate!.year}'
: hintText ?? 'Sélectionner une date',
style: TextStyle(
color: selectedDate != null
? (isDark ? AppColors.textPrimaryDark : AppColors.textPrimary)
: AppColors.textTertiary,
),
),
),
);
}
}