petitspas/frontend/lib/widgets/custom_app_text_field.dart
Julien Martin a9ce4a6756 fix(am): ordre Tab formulaire pro et chevrons accessibles
- 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
2026-04-13 14:09:21 +02:00

200 lines
7.0 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 lest 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 derreur du
// validateur saffiche 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,
),
),
),
],
),
),
],
);
}
}