petitspas/frontend/lib/widgets/email_text_field.dart
Julien Martin bf05b1d7d7 feat(inscription): email accusé réception, photos enfants, formulaires identité
- Backend: mail après inscription parent avec n° dossier, UPLOAD_PHOTOS_DIR, réponse API
- Frontend: imageBytes + payload photo, utils email/code postal/téléphone, champs dédiés
- Formulaires admin, login (focus), personal_info, child_card, custom_app_text_field

Made-with: Cursor
2026-03-31 00:04:38 +02:00

220 lines
6.2 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../utils/email_utils.dart';
import 'custom_app_text_field.dart';
/// [TextFormField] e-mail : à la perte de focus → minuscules + validation immédiate.
class EmailTextFormField extends StatefulWidget {
const EmailTextFormField({
super.key,
required this.controller,
this.decoration,
this.label,
this.hint,
this.allowEmpty = false,
this.readOnly = false,
this.focusNode,
this.autovalidateMode,
this.validator,
});
final TextEditingController controller;
final InputDecoration? decoration;
final String? label;
final String? hint;
final bool allowEmpty;
final bool readOnly;
final FocusNode? focusNode;
final AutovalidateMode? autovalidateMode;
final String? Function(String?)? validator;
@override
State<EmailTextFormField> createState() => _EmailTextFormFieldState();
}
class _EmailTextFormFieldState extends State<EmailTextFormField> {
final GlobalKey<FormFieldState<String>> _fieldKey =
GlobalKey<FormFieldState<String>>();
late final FocusNode _focusNode;
late final bool _ownsFocusNode;
@override
void initState() {
super.initState();
_ownsFocusNode = widget.focusNode == null;
_focusNode = widget.focusNode ?? FocusNode();
_focusNode.addListener(_onFocusChange);
}
void _onFocusChange() {
if (_focusNode.hasFocus || widget.readOnly) {
return;
}
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted || _focusNode.hasFocus) {
return;
}
final c = widget.controller;
final normalized = normalizeEmailText(c.text);
if (normalized != c.text) {
c.value = TextEditingValue(
text: normalized,
selection: TextSelection.collapsed(offset: normalized.length),
);
}
_fieldKey.currentState?.validate();
});
}
@override
void dispose() {
_focusNode.removeListener(_onFocusChange);
if (_ownsFocusNode) {
_focusNode.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
final deco = widget.decoration ??
InputDecoration(
labelText: widget.label,
hintText: widget.hint,
border: const OutlineInputBorder(),
);
return TextFormField(
key: _fieldKey,
controller: widget.controller,
focusNode: _focusNode,
readOnly: widget.readOnly,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
autocorrect: false,
enableSuggestions: false,
autofillHints: const [AutofillHints.email],
inputFormatters: const <TextInputFormatter>[EmailMaxLengthFormatter()],
autovalidateMode: widget.autovalidateMode,
decoration: deco,
validator: widget.readOnly
? null
: (widget.validator ??
(value) => validateEmail(value, allowEmpty: widget.allowEmpty)),
);
}
}
/// Même logique que [EmailTextFormField] avec le style [CustomAppTextField].
class EmailCustomTextField extends StatefulWidget {
const EmailCustomTextField({
super.key,
required this.controller,
required this.labelText,
this.hintText = '',
this.focusNode,
this.fieldWidth = double.infinity,
this.fieldHeight = 53.0,
this.labelFontSize = 22.0,
this.inputFontSize = 20.0,
this.enabled = true,
this.readOnly = false,
this.allowEmpty = false,
this.style = CustomAppTextFieldStyle.beige,
this.validator,
this.textInputAction,
this.onFieldSubmitted,
});
final TextEditingController controller;
final String labelText;
final String hintText;
final FocusNode? focusNode;
final double fieldWidth;
final double fieldHeight;
final double labelFontSize;
final double inputFontSize;
final bool enabled;
final bool readOnly;
final bool allowEmpty;
final CustomAppTextFieldStyle style;
final String? Function(String?)? validator;
final TextInputAction? textInputAction;
final ValueChanged<String>? onFieldSubmitted;
@override
State<EmailCustomTextField> createState() => _EmailCustomTextFieldState();
}
class _EmailCustomTextFieldState extends State<EmailCustomTextField> {
final GlobalKey<FormFieldState<String>> _fieldKey =
GlobalKey<FormFieldState<String>>();
late final FocusNode _focusNode;
late final bool _ownsFocusNode;
@override
void initState() {
super.initState();
_ownsFocusNode = widget.focusNode == null;
_focusNode = widget.focusNode ?? FocusNode();
_focusNode.addListener(_onFocusChange);
}
void _onFocusChange() {
if (_focusNode.hasFocus || widget.readOnly || !widget.enabled) {
return;
}
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted || _focusNode.hasFocus) {
return;
}
final c = widget.controller;
final normalized = normalizeEmailText(c.text);
if (normalized != c.text) {
c.value = TextEditingValue(
text: normalized,
selection: TextSelection.collapsed(offset: normalized.length),
);
}
_fieldKey.currentState?.validate();
});
}
@override
void dispose() {
_focusNode.removeListener(_onFocusChange);
if (_ownsFocusNode) {
_focusNode.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return CustomAppTextField(
formFieldKey: _fieldKey,
controller: widget.controller,
focusNode: _focusNode,
labelText: widget.labelText,
hintText: widget.hintText,
style: widget.style,
fieldWidth: widget.fieldWidth,
fieldHeight: widget.fieldHeight,
labelFontSize: widget.labelFontSize,
inputFontSize: widget.inputFontSize,
keyboardType: TextInputType.emailAddress,
autocorrect: false,
enableSuggestions: false,
enabled: widget.enabled,
readOnly: widget.readOnly,
autofillHints: const [AutofillHints.email],
textInputAction: widget.textInputAction ?? TextInputAction.next,
onFieldSubmitted: widget.onFieldSubmitted,
inputFormatters: const [EmailMaxLengthFormatter()],
validator: widget.validator ??
((v) => validateEmail(v, allowEmpty: widget.allowEmpty)),
isRequired: false,
);
}
}