Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion lib/src/components/date_picker.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1162,7 +1162,8 @@ class _ShadDatePickerState extends State<ShadDatePicker> {
);
},
child: ShadButton.raw(
decoration: widget.buttonDecoration,
decoration:
widget.buttonDecoration ?? theme.datePickerTheme.buttonDecoration,
variant: widget.buttonVariant ??
theme.datePickerTheme.buttonVariant ??
ShadButtonVariant.outline,
Expand Down
54 changes: 54 additions & 0 deletions lib/src/components/form/field.dart
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ class ShadFormBuilderField<T> extends FormField<T> {
/// form.
class ShadFormBuilderFieldState<F extends ShadFormBuilderField<T>, T>
extends FormFieldState<T> {
String? _customErrorText;
FocusNode? _focusNode;
ShadFormState? _parentForm;

Expand All @@ -156,6 +157,28 @@ class ShadFormBuilderFieldState<F extends ShadFormBuilderField<T>, T>
/// Whether the field is enabled, factoring in parent form state.
bool get enabled => widget.enabled && (_parentForm?.enabled ?? true);

@override

/// Returns the current error text,
/// As it might be validation error or programmatically set.
String? get errorText => super.errorText ?? _customErrorText;

@override

/// Returns `true` if the field has an error or has a custom error text.
bool get hasError => super.hasError || errorText != null;

@override

/// Returns `true` if the field is valid and has no custom error text.
bool get isValid => super.isValid && _customErrorText == null;

/// Returns `true` if the field's value is valid and ignores custom error.
bool get valueIsValid => super.isValid;

/// Returns `true` if the field has an error and ignores custom error.
bool get valueHasError => super.hasError;

@override
void initState() {
super.initState();
Expand Down Expand Up @@ -197,9 +220,40 @@ class ShadFormBuilderFieldState<F extends ShadFormBuilderField<T>, T>
void reset() {
super.reset();
didChange(initialValue);
if (_customErrorText != null) {
setState(() => _customErrorText = null);
}
widget.onReset?.call();
}

/// Validate field
///
/// Clear custom error if [clearCustomError] is `true`.
/// By default `true`
///
@override
bool validate({
bool clearCustomError = true,
}) {
if (clearCustomError && _customErrorText != null) {
setState(() => _customErrorText = null);
}
final isValid = super.validate() && !hasError;
Copy link
Preview

Copilot AI Aug 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validation logic creates a circular dependency. The hasError getter includes both validation errors and custom errors, so this line will always return false when _customErrorText is set, even if the field value itself is valid. Consider using super.validate() && _customErrorText == null instead.

Suggested change
final isValid = super.validate() && !hasError;
final isValid = super.validate() && _customErrorText == null;

Copilot uses AI. Check for mistakes.

return isValid;
}

/// Invalidate field with a [errorText]
///
void invalidate(String errorText, {bool revalidate = true}) {
setState(() => _customErrorText = errorText);

if (revalidate) {
validate(
clearCustomError: false,
);
}
}

@override
void dispose() {
if (widget.id != null) _parentForm?.unregisterField(widget.id!, this);
Expand Down
Binary file added test/src/components/goldens/input_error.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
39 changes: 39 additions & 0 deletions test/src/components/input_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:shadcn_ui/src/app.dart';
import 'package:shadcn_ui/src/components/input.dart';
import 'package:shadcn_ui/src/components/form/fields/input.dart';
import 'package:shadcn_ui/src/components/form/form.dart';
import 'package:shadcn_ui/src/components/form/field.dart';

import '../../extra/pump_async_widget.dart';

Expand All @@ -24,5 +27,41 @@ void main() {
matchesGoldenFile('goldens/input.png'),
);
});

testWidgets('ShadInputFormField programmatic error matches goldens',
(tester) async {
// Use a GlobalKey to access the field state and call invalidate
final key =
GlobalKey<ShadFormBuilderFieldState<ShadInputFormField, String>>();

await tester.pumpAsyncWidget(
createTestWidget(
ShadForm(
child: Padding(
padding: const EdgeInsets.all(16),
child: ShadInputFormField(
key: key,
label: const Text('Email'),
placeholder: const Text('Email'),
),
),
),
),
);

// Programmatically set an error on the field
key.currentState!.invalidate('This field is required');
await tester.pumpAndSettle();
// Sanity-check state to catch regressions beyond the golden image.
expect(key.currentState!.hasError, isTrue);
expect(key.currentState!.errorText, 'This field is required');
expect(key.currentState!.isValid, isFalse);
// Base value has no validator in this test, so valueIsValid should be true.
expect(key.currentState!.valueIsValid, isTrue);
expect(
find.byType(ShadInputFormField),
matchesGoldenFile('goldens/input_error.png'),
);
});
});
}