- Ordre de focus explicite (NumericFocusOrder) pour l’inscription AM étape 2 : chaîne des champs jusqu’aux chevrons, Pays → NIR, places → Suivant (10) puis Précédent (11). - FocusTraversalGroup avec OrderedTraversalPolicy sur le Scaffold pour respecter cet ordre (desktop / web). - Champ pays : TextInputAction.next, onFieldSubmitted et onEditingComplete vers le NIR ; CustomAppTextField expose onEditingComplete. - NirTextField : focusNode et focusTraversalOrder optionnels. Made-with: Cursor
200 lines
7.0 KiB
Dart
200 lines
7.0 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:flutter/services.dart';
|
||
import 'package:google_fonts/google_fonts.dart';
|
||
|
||
// Définition de l'enum pour les styles de couleur/fond
|
||
enum CustomAppTextFieldStyle {
|
||
beige,
|
||
lavande,
|
||
jaune,
|
||
}
|
||
|
||
class CustomAppTextField extends StatefulWidget {
|
||
final TextEditingController controller;
|
||
final FocusNode? focusNode;
|
||
final String labelText;
|
||
final String hintText;
|
||
final double fieldWidth;
|
||
final double fieldHeight;
|
||
final bool obscureText;
|
||
final TextInputType keyboardType;
|
||
final String? Function(String?)? validator;
|
||
final CustomAppTextFieldStyle style;
|
||
final bool isRequired;
|
||
final bool enabled;
|
||
final bool readOnly;
|
||
final VoidCallback? onTap;
|
||
final IconData? suffixIcon;
|
||
final double labelFontSize;
|
||
final double inputFontSize;
|
||
/// Espace vertical entre le libellé et la zone de saisie.
|
||
final double labelFieldSpacing;
|
||
final bool showLabel;
|
||
final Iterable<String>? autofillHints;
|
||
final TextInputAction? textInputAction;
|
||
final ValueChanged<String>? onFieldSubmitted;
|
||
/// Souvent appelé par la touche « suivant » du clavier (flèche), là où [onFieldSubmitted] ne l’est pas.
|
||
final VoidCallback? onEditingComplete;
|
||
final List<TextInputFormatter>? inputFormatters;
|
||
final bool autocorrect;
|
||
final bool enableSuggestions;
|
||
final GlobalKey<FormFieldState<String>>? formFieldKey;
|
||
|
||
const CustomAppTextField({
|
||
super.key,
|
||
required this.controller,
|
||
this.focusNode,
|
||
required this.labelText,
|
||
this.showLabel = true,
|
||
this.hintText = '',
|
||
this.fieldWidth = 300.0,
|
||
this.fieldHeight = 53.0,
|
||
this.obscureText = false,
|
||
this.keyboardType = TextInputType.text,
|
||
this.validator,
|
||
this.style = CustomAppTextFieldStyle.beige,
|
||
this.isRequired = false,
|
||
this.enabled = true,
|
||
this.readOnly = false,
|
||
this.onTap,
|
||
this.suffixIcon,
|
||
this.labelFontSize = 18.0,
|
||
this.inputFontSize = 18.0,
|
||
this.labelFieldSpacing = 6.0,
|
||
this.autofillHints,
|
||
this.textInputAction,
|
||
this.onFieldSubmitted,
|
||
this.onEditingComplete,
|
||
this.inputFormatters,
|
||
this.autocorrect = true,
|
||
this.enableSuggestions = true,
|
||
this.formFieldKey,
|
||
});
|
||
|
||
@override
|
||
State<CustomAppTextField> createState() => _CustomAppTextFieldState();
|
||
}
|
||
|
||
class _CustomAppTextFieldState extends State<CustomAppTextField> {
|
||
String getBackgroundImagePath() {
|
||
switch (widget.style) {
|
||
case CustomAppTextFieldStyle.lavande:
|
||
return 'assets/images/bg_lavender.png';
|
||
case CustomAppTextFieldStyle.jaune:
|
||
return 'assets/images/bg_yellow.png';
|
||
case CustomAppTextFieldStyle.beige:
|
||
default:
|
||
return 'assets/images/bg_beige.png';
|
||
}
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final double dynamicFieldHeight = widget.fieldHeight;
|
||
// Indication « non éditable » : libellé + hint en gris.
|
||
final Color labelColor =
|
||
widget.enabled ? Colors.black87 : Colors.grey;
|
||
final Color hintColor = widget.enabled
|
||
? Colors.black54.withOpacity(0.7)
|
||
: Colors.grey;
|
||
|
||
return Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
if (widget.showLabel) ...[
|
||
Text(
|
||
widget.labelText,
|
||
style: GoogleFonts.merienda(
|
||
fontSize: widget.labelFontSize,
|
||
color: labelColor,
|
||
fontWeight: FontWeight.w500,
|
||
),
|
||
),
|
||
SizedBox(height: widget.labelFieldSpacing),
|
||
],
|
||
// Pas de hauteur fixe sur le TextFormField : le message d’erreur du
|
||
// validateur s’affiche en dessous ; un SizedBox fixe le masquait.
|
||
SizedBox(
|
||
width: widget.fieldWidth,
|
||
child: Stack(
|
||
clipBehavior: Clip.none,
|
||
alignment: Alignment.centerLeft,
|
||
children: [
|
||
Positioned.fill(
|
||
child: Image.asset(
|
||
getBackgroundImagePath(),
|
||
fit: BoxFit.fill,
|
||
),
|
||
),
|
||
Padding(
|
||
padding:
|
||
const EdgeInsets.symmetric(horizontal: 18.0, vertical: 8.0),
|
||
child: ConstrainedBox(
|
||
constraints: BoxConstraints(
|
||
minHeight:
|
||
(dynamicFieldHeight - 16.0).clamp(24.0, double.infinity),
|
||
),
|
||
child: TextFormField(
|
||
key: widget.formFieldKey,
|
||
controller: widget.controller,
|
||
focusNode: widget.focusNode,
|
||
obscureText: widget.obscureText,
|
||
keyboardType: widget.keyboardType,
|
||
autocorrect: widget.autocorrect,
|
||
enableSuggestions: widget.enableSuggestions,
|
||
inputFormatters: widget.inputFormatters,
|
||
autofillHints: widget.autofillHints,
|
||
textInputAction: widget.textInputAction,
|
||
onFieldSubmitted: widget.onFieldSubmitted,
|
||
onEditingComplete: widget.onEditingComplete,
|
||
enabled: widget.enabled,
|
||
readOnly: widget.readOnly,
|
||
onTap: widget.onTap,
|
||
style: GoogleFonts.merienda(
|
||
fontSize: widget.inputFontSize,
|
||
color: Colors.black87),
|
||
validator: widget.validator ??
|
||
(value) {
|
||
if (!widget.enabled || widget.readOnly) return null;
|
||
if (widget.isRequired &&
|
||
(value == null || value.isEmpty)) {
|
||
return 'Ce champ est obligatoire';
|
||
}
|
||
return null;
|
||
},
|
||
decoration: InputDecoration(
|
||
hintText: widget.hintText,
|
||
hintStyle: GoogleFonts.merienda(
|
||
fontSize: widget.inputFontSize,
|
||
color: hintColor),
|
||
border: InputBorder.none,
|
||
contentPadding: EdgeInsets.zero,
|
||
isDense: true,
|
||
errorStyle: GoogleFonts.merienda(
|
||
fontSize: widget.inputFontSize * 0.75,
|
||
color: Colors.red.shade800,
|
||
height: 1.2,
|
||
),
|
||
errorMaxLines: 3,
|
||
suffixIcon: widget.suffixIcon != null
|
||
? Padding(
|
||
padding: const EdgeInsets.only(right: 0.0),
|
||
child: Icon(widget.suffixIcon,
|
||
color: Colors.black54,
|
||
size: widget.inputFontSize * 1.1),
|
||
)
|
||
: null,
|
||
),
|
||
textAlignVertical: TextAlignVertical.center,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|