Skip to content

Instantly share code, notes, and snippets.

@followthemoney1
Created December 16, 2025 12:08
Show Gist options
  • Select an option

  • Save followthemoney1/43a685ca037b546fc7a5af64c68246e7 to your computer and use it in GitHub Desktop.

Select an option

Save followthemoney1/43a685ca037b546fc7a5af64c68246e7 to your computer and use it in GitHub Desktop.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:pod_payment/core/contants/size_constants.dart';
import 'package:pod_payment/core/extensions/context_extension.dart';
import 'package:pod_payment/shared/widgets/pod_svg.dart';
import 'package:pod_payment/core/contants/image_constants.dart';
typedef ValidationChecker = String? Function(String?);
class PodTextField extends StatefulWidget {
const PodTextField({
super.key,
required this.controller,
this.focusNode,
this.inputFormatters,
this.hintText,
this.autoFocus = false,
this.obscureText = false,
this.canShowHidePassword = false,
this.keyboardType = TextInputType.text,
this.isDisabled = false,
this.textInputAction = TextInputAction.done,
this.autoFillHints,
this.maxLength = 150,
this.onChanged,
this.onEditingComplete,
this.onSubmitted,
this.onFocusChanged,
this.readOnly = false,
this.containerPadding,
this.cursorColor,
this.borderRadius,
this.validator,
this.textStyle,
this.centerInput = false,
this.minLines,
this.maxLines,
this.autovalidateMode = AutovalidateMode.onUserInteraction,
this.backgroundColor,
this.prefix,
this.suffix,
this.mouseCursor,
this.focusedBorderSide,
this.unfocusedBorderSide = const BorderSide(color: Color(0xFFC6C5C4), width: 1),
this.hoverColor,
this.isCollapsed,
this.prefixIconConstraints,
this.suffixIconConstraints,
this.textAlignVertical,
//
});
final TextEditingController controller;
final FocusNode? focusNode;
final String? hintText;
final List<TextInputFormatter>? inputFormatters;
final bool obscureText;
final bool canShowHidePassword;
final TextInputType keyboardType;
final bool autoFocus;
final bool isDisabled;
final TextInputAction textInputAction;
final List<String>? autoFillHints;
final int? maxLength;
final Function(String)? onChanged;
final VoidCallback? onEditingComplete;
final Function(String)? onSubmitted;
final Function(bool value)? onFocusChanged;
final int? maxLines;
final int? minLines;
final bool readOnly;
final EdgeInsets? containerPadding;
final Color? cursorColor;
final BorderRadius? borderRadius;
final ValidationChecker? validator;
final TextStyle? textStyle;
final bool centerInput;
final AutovalidateMode autovalidateMode;
final Widget? prefix;
final Widget? suffix;
final Color? backgroundColor;
final MouseCursor? mouseCursor;
final BorderSide? focusedBorderSide;
final BorderSide? unfocusedBorderSide;
final Color? hoverColor;
final bool? isCollapsed;
final BoxConstraints? prefixIconConstraints;
final BoxConstraints? suffixIconConstraints;
final TextAlignVertical? textAlignVertical;
@override
State<PodTextField> createState() => _PodTextFieldState();
}
class _PodTextFieldState extends State<PodTextField> {
bool _obscureText = false;
bool _hasFocus = false;
@override
void initState() {
super.initState();
_obscureText = widget.obscureText;
widget.focusNode?.addListener(() {
setState(() {
_hasFocus = widget.focusNode?.hasFocus ?? false;
});
widget.onFocusChanged?.call(_hasFocus);
});
}
void _requestFocus() {
setState(() {
FocusScope.of(context).requestFocus(widget.focusNode);
});
}
@override
Widget build(BuildContext context) {
return Focus(
onFocusChange: (value) {
widget.onFocusChanged?.call(value);
setState(() {
_hasFocus = value;
});
},
child: _buildBody(),
);
}
Widget _buildBody() {
return TextFormField(
textAlign: widget.centerInput ? TextAlign.center : TextAlign.start,
maxLines: widget.maxLines ?? 1,
minLines: widget.minLines,
maxLength: widget.maxLength,
readOnly: widget.readOnly,
enabled: !widget.isDisabled,
inputFormatters: [
...?widget.inputFormatters,
// FilteringTextInputFormatter.deny(_confusablesPattern),
],
controller: widget.controller,
focusNode: widget.focusNode,
enableSuggestions: widget.autoFillHints != null,
autofocus: widget.autoFocus,
autocorrect: widget.autoFillHints != null,
autofillHints: widget.autoFillHints,
keyboardType: widget.keyboardType,
obscureText: _obscureText,
textInputAction: widget.textInputAction,
cursorColor: widget.cursorColor ?? context.colorScheme.primary,
expands: false,
style: widget.textStyle ?? context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurface),
onTap: _requestFocus,
validator: widget.validator,
onChanged: widget.onChanged,
textAlignVertical: widget.textAlignVertical,
mouseCursor: widget.mouseCursor,
autovalidateMode: widget.autovalidateMode,
onEditingComplete: widget.onEditingComplete,
onFieldSubmitted: widget.onSubmitted,
decoration: InputDecoration(
isCollapsed: widget.isCollapsed ?? false,
hoverColor: widget.hoverColor,
border: OutlineInputBorder(
borderRadius: widget.borderRadius ?? BorderRadius.circular(BorderRadiusConstants.small),
borderSide: const BorderSide(color: Colors.transparent),
),
focusedBorder: OutlineInputBorder(
borderRadius: widget.borderRadius ?? BorderRadius.circular(BorderRadiusConstants.small),
borderSide: widget.focusedBorderSide ?? BorderSide(color: context.colorScheme.primary),
),
enabledBorder: OutlineInputBorder(
borderRadius: widget.borderRadius ?? BorderRadius.circular(BorderRadiusConstants.small),
borderSide: widget.unfocusedBorderSide ?? BorderSide(color: context.colorScheme.surface.withAlpha(20)),
),
errorBorder: OutlineInputBorder(
borderRadius: widget.borderRadius ?? BorderRadius.circular(BorderRadiusConstants.small),
borderSide: BorderSide(color: context.colorScheme.error),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: widget.borderRadius ?? BorderRadius.circular(BorderRadiusConstants.small),
borderSide: BorderSide(color: context.colorScheme.error),
),
disabledBorder: OutlineInputBorder(
borderRadius: widget.borderRadius ?? BorderRadius.circular(BorderRadiusConstants.small),
borderSide: const BorderSide(color: Colors.transparent),
),
prefixIcon: _buildPrefix(),
prefixIconConstraints: widget.prefixIconConstraints,
suffixIcon: _buildSuffix(),
suffixIconConstraints: widget.suffixIconConstraints,
filled: true,
fillColor: widget.backgroundColor ?? const Color(0xFF000000),
errorStyle: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.error, fontSize: 12),
hintStyle:
widget.textStyle?.copyWith(color: context.colorScheme.onSurfaceVariant) ??
context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceVariant),
hintText: widget.hintText,
alignLabelWithHint: widget.maxLines != null,
counterText: '',
contentPadding:
widget.containerPadding ??
const EdgeInsets.symmetric(horizontal: PaddingConstants.large, vertical: PaddingConstants.large),
),
);
}
Widget? _buildPrefix() {
if (widget.prefix != null) {
return Padding(padding: const EdgeInsets.only(left: 4), child: widget.prefix);
}
return null;
}
Widget? _buildSuffix() {
if (widget.canShowHidePassword && widget.suffix != null) {
return Row(mainAxisSize: MainAxisSize.min, children: [widget.suffix!, _buildShowHideButton()]);
} else if (widget.canShowHidePassword) {
return _buildShowHideButton();
} else if (widget.suffix != null) {
return widget.suffix;
}
return null;
}
Widget _buildShowHideButton() {
return Padding(
padding: const EdgeInsets.all(PaddingConstants.medium),
child: InkWell(
onTap: () {
setState(() {
_obscureText = !_obscureText;
});
},
child: Icon(
_obscureText ? Icons.visibility : Icons.visibility_off,
color: _hasFocus ? context.colorScheme.primary : context.colorScheme.onSurfaceVariant,
),
),
);
}
}
class SearchBarField extends StatelessWidget {
const SearchBarField({
super.key,
this.controller,
this.onChanged,
this.onSubmitted,
this.hintText = 'Search',
this.autofocus = false,
this.textInputAction = TextInputAction.search,
this.focusNode,
this.backgroundColor = const Color(0xFFF5F3F2),
this.iconColor,
this.textColor,
this.hintColor,
});
final TextEditingController? controller;
final ValueChanged<String>? onChanged;
final ValueChanged<String>? onSubmitted;
final String hintText;
final bool autofocus;
final TextInputAction textInputAction;
final FocusNode? focusNode;
final Color backgroundColor;
final Color? iconColor;
final Color? textColor;
final Color? hintColor;
@override
Widget build(BuildContext context) {
// Use provided colors or fall back to default colors
final effectiveIconColor = iconColor ?? Colors.grey.shade600;
final effectiveTextColor = textColor ?? Colors.grey.shade700;
final effectiveHintColor = hintColor ?? Colors.grey.shade500;
return Semantics(
label: hintText,
textField: true,
child: Container(
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(BorderRadiusConstants.small),
),
// Height + horizontal padding mimic the roomy feel from the image
padding: const EdgeInsets.symmetric(horizontal: PaddingConstants.large),
height: SizeConstants.searchFieldHeight,
alignment: Alignment.centerLeft,
child: Row(
children: [
PodSVGIcon(size: SizeConstants.size20, pathNonBinary: ImageConstants.icSearch, color: effectiveIconColor),
const SizedBox(width: PaddingConstants.medium),
// Expanded TextField without its own fill/borders
Expanded(
child: TextField(
controller: controller,
focusNode: focusNode,
autofocus: autofocus,
textInputAction: textInputAction,
onChanged: onChanged,
onSubmitted: onSubmitted,
cursorColor: effectiveTextColor,
style: context.theme.textTheme.titleMedium?.copyWith(
color: effectiveTextColor,
fontWeight: FontWeight.w500,
),
decoration: InputDecoration(
isDense: true,
hintText: hintText,
hintStyle: context.theme.textTheme.titleMedium?.copyWith(
color: effectiveHintColor,
fontWeight: FontWeight.w500,
),
// No borders/fill so the container is the only background.
border: InputBorder.none,
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
disabledBorder: InputBorder.none,
contentPadding: EdgeInsets.zero,
),
),
),
],
),
),
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment