Skip to content
Merged
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
86 changes: 58 additions & 28 deletions packages/dievas/lib/src/components/text_area/dievas_text_area.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class DievasTextArea extends StatelessWidget {
this.hint,
this.helperText,
this.errorText,
this.warningText,
this.minLines = 3,
this.maxLines = 6,
this.enabled = true,
Expand Down Expand Up @@ -60,6 +61,11 @@ class DievasTextArea extends StatelessWidget {
/// Error message. Overrides [helperText] and switches the border to error colour.
final String? errorText;

/// Non-blocking validation feedback. Overrides [helperText] and switches the
/// border to the warning colour. Takes precedence over the normal state but
/// yields to [errorText] when both are set.
final String? warningText;

/// Minimum visible lines. Defaults to 3.
final int minLines;

Expand All @@ -80,12 +86,22 @@ class DievasTextArea extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = DievasTheme.componentsOf(context).textInput;
final hasError = errorText != null && errorText!.isNotEmpty;
final hasError = switch (errorText) {
final error? => error.isNotEmpty,
_ => false,
};
final hasWarning = switch (warningText) {
final warning? when !hasError => warning.isNotEmpty,
_ => false,
};

// TextArea always uses md tokens

final borderColour = hasError ? theme.borderColourError : theme.borderColour;
final focusedBorderColour = hasError ? theme.borderColourError : theme.borderColourFocused;
final (borderColour, focusedBorderColour) = switch ((hasError, hasWarning)) {
(true, _) => (theme.borderColourError, theme.borderColourError),
(false, true) => (theme.borderColourWarning, theme.borderColourWarning),
_ => (theme.borderColour, theme.borderColourFocused),
};

final border = OutlineInputBorder(
borderRadius: theme.borderRadius,
Expand All @@ -105,9 +121,11 @@ class DievasTextArea extends StatelessWidget {
final content = _DievasTextFieldContent(
theme: theme,
hasError: hasError,
hasWarning: hasWarning,
label: label,
helperText: helperText,
errorText: errorText,
warningText: warningText,
field: TextField(
controller: controller,
focusNode: focusNode,
Expand Down Expand Up @@ -143,52 +161,64 @@ class DievasTextArea extends StatelessWidget {
),
);

if (!enabled) {
return Opacity(opacity: theme.disabledOpacity, child: content);
}

return content;
return switch (enabled) {
true => content,
_ => Opacity(opacity: theme.disabledOpacity, child: content),
};
}
}

class _DievasTextFieldContent extends StatelessWidget {
const _DievasTextFieldContent({
required this.theme,
required this.hasError,
required this.hasWarning,
required this.field,
this.label,
this.helperText,
this.errorText,
this.warningText,
});

final DievasTextInputThemeData theme;
final bool hasError;
final bool hasWarning;
final Widget field;

final String? label;
final String? helperText;
final String? errorText;
final String? warningText;

@override
Widget build(BuildContext context) => Column(
mainAxisSize: .min,
crossAxisAlignment: .start,
children: [
if (label case final text?) ...[
DefaultTextStyle(style: theme.labelStyle, child: Text(text)),
SizedBox(height: theme.labelSpacing),
],
field,
if (errorText case final errorText? when hasError) ...[
SizedBox(height: theme.helperSpacing),
DefaultTextStyle(
style: theme.errorStyle.copyWith(color: theme.borderColourError),
child: Text(errorText),
),
] else if (helperText case final helperText?) ...[
SizedBox(height: theme.helperSpacing),
DefaultTextStyle(style: theme.helperStyle, child: Text(helperText)),
Widget build(BuildContext context) {
final feedback = switch ((errorText, hasError, warningText, hasWarning, helperText)) {
(final error?, true, _, _, _) => (
message: error,
style: theme.errorStyle.copyWith(color: theme.borderColourError),
),
(_, _, final warning?, true, _) => (
message: warning,
style: theme.errorStyle.copyWith(color: theme.borderColourWarning),
),
(_, _, _, _, final helper?) => (message: helper, style: theme.helperStyle),
_ => null,
};

return Column(
mainAxisSize: .min,
crossAxisAlignment: .start,
children: [
if (label case final text?) ...[
DefaultTextStyle(style: theme.labelStyle, child: Text(text)),
SizedBox(height: theme.labelSpacing),
],
field,
if (feedback case (message: final message, style: final style)?) ...[
SizedBox(height: theme.helperSpacing),
DefaultTextStyle(style: style, child: Text(message)),
],
],
],
);
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class DievasTextInput extends StatefulWidget {
this.hint,
this.helperText,
this.errorText,
this.warningText,
this.leadingIcon,
this.trailingIcon,
this.size = DievasTextInputSize.md,
Expand Down Expand Up @@ -74,6 +75,11 @@ class DievasTextInput extends StatefulWidget {
/// error colour.
final String? errorText;

/// Non-blocking validation feedback. Overrides [helperText] and switches the
/// border to the warning colour. Takes precedence over the normal state but
/// yields to [errorText] when both are set.
final String? warningText;

/// Leading widget inside the input container (icon, prefix, etc.).
final Widget? leadingIcon;

Expand Down Expand Up @@ -104,7 +110,14 @@ class _DievasTextInputState extends State<DievasTextInput> {
@override
Widget build(BuildContext context) {
final theme = DievasTheme.componentsOf(context).textInput;
final hasError = widget.errorText != null && widget.errorText!.isNotEmpty;
final hasError = switch (widget.errorText) {
final error? => error.isNotEmpty,
_ => false,
};
final hasWarning = switch (widget.warningText) {
final warning? when !hasError => warning.isNotEmpty,
_ => false,
};

final height = switch (widget.size) {
.sm => theme.height.sm,
Expand All @@ -130,8 +143,11 @@ class _DievasTextInputState extends State<DievasTextInput> {
.lg => theme.placeholderStyle.lg,
};

final borderColour = hasError ? theme.borderColourError : theme.borderColour;
final focusedBorderColour = hasError ? theme.borderColourError : theme.borderColourFocused;
final (borderColour, focusedBorderColour) = switch ((hasError, hasWarning)) {
(true, _) => (theme.borderColourError, theme.borderColourError),
(false, true) => (theme.borderColourWarning, theme.borderColourWarning),
_ => (theme.borderColour, theme.borderColourFocused),
};

final border = OutlineInputBorder(
borderRadius: theme.borderRadius,
Expand All @@ -151,9 +167,11 @@ class _DievasTextInputState extends State<DievasTextInput> {
final content = _DievasTextInputFieldContent(
theme: theme,
hasError: hasError,
hasWarning: hasWarning,
label: widget.label,
helperText: widget.helperText,
errorText: widget.errorText,
warningText: widget.warningText,
field: SizedBox(
height: height,
child: TextField(
Expand Down Expand Up @@ -221,52 +239,64 @@ class _DievasTextInputState extends State<DievasTextInput> {
),
);

if (!widget.enabled) {
return Opacity(opacity: theme.disabledOpacity, child: content);
}

return content;
return switch (widget.enabled) {
true => content,
_ => Opacity(opacity: theme.disabledOpacity, child: content),
};
}
}

class _DievasTextInputFieldContent extends StatelessWidget {
const _DievasTextInputFieldContent({
required this.theme,
required this.hasError,
required this.hasWarning,
required this.field,
this.label,
this.helperText,
this.errorText,
this.warningText,
});

final DievasTextInputThemeData theme;
final bool hasError;
final bool hasWarning;
final Widget field;

final String? label;
final String? helperText;
final String? errorText;
final String? warningText;

@override
Widget build(BuildContext context) => Column(
mainAxisSize: .min,
crossAxisAlignment: .start,
children: [
if (label case final labelText?) ...[
DefaultTextStyle(style: theme.labelStyle, child: Text(labelText)),
SizedBox(height: theme.labelSpacing),
],
field,
if (errorText case final errorText? when hasError) ...[
SizedBox(height: theme.helperSpacing),
DefaultTextStyle(
style: theme.errorStyle.copyWith(color: theme.borderColourError),
child: Text(errorText),
),
] else if (helperText case final helperText?) ...[
SizedBox(height: theme.helperSpacing),
DefaultTextStyle(style: theme.helperStyle, child: Text(helperText)),
Widget build(BuildContext context) {
final feedback = switch ((errorText, hasError, warningText, hasWarning, helperText)) {
(final error?, true, _, _, _) => (
message: error,
style: theme.errorStyle.copyWith(color: theme.borderColourError),
),
(_, _, final warning?, true, _) => (
message: warning,
style: theme.errorStyle.copyWith(color: theme.borderColourWarning),
),
(_, _, _, _, final helper?) => (message: helper, style: theme.helperStyle),
_ => null,
};

return Column(
mainAxisSize: .min,
crossAxisAlignment: .start,
children: [
if (label case final labelText?) ...[
DefaultTextStyle(style: theme.labelStyle, child: Text(labelText)),
SizedBox(height: theme.labelSpacing),
],
field,
if (feedback case (message: final message, style: final style)?) ...[
SizedBox(height: theme.helperSpacing),
DefaultTextStyle(style: style, child: Text(message)),
],
],
],
);
);
}
}
17 changes: 16 additions & 1 deletion packages/dievas/lib/src/theme/color/sets/input_colours.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ final class InputColours extends Equatable {
required this.inputBorder,
required this.inputBorderFocus,
required this.inputBorderError,
required this.inputBorderWarning,
required this.inputText,
required this.inputPlaceholder,
});
Expand All @@ -24,6 +25,9 @@ final class InputColours extends Equatable {
/// Input border in error state.
final Color inputBorderError;

/// Input border in warning state.
final Color inputBorderWarning;

/// Input text colour (typed value).
final Color inputText;

Expand All @@ -35,13 +39,15 @@ final class InputColours extends Equatable {
Color? inputBorder,
Color? inputBorderFocus,
Color? inputBorderError,
Color? inputBorderWarning,
Color? inputText,
Color? inputPlaceholder,
}) => InputColours(
inputBg: inputBg ?? this.inputBg,
inputBorder: inputBorder ?? this.inputBorder,
inputBorderFocus: inputBorderFocus ?? this.inputBorderFocus,
inputBorderError: inputBorderError ?? this.inputBorderError,
inputBorderWarning: inputBorderWarning ?? this.inputBorderWarning,
inputText: inputText ?? this.inputText,
inputPlaceholder: inputPlaceholder ?? this.inputPlaceholder,
);
Expand All @@ -51,10 +57,19 @@ final class InputColours extends Equatable {
inputBorder: Color.lerp(a.inputBorder, b.inputBorder, t)!,
inputBorderFocus: Color.lerp(a.inputBorderFocus, b.inputBorderFocus, t)!,
inputBorderError: Color.lerp(a.inputBorderError, b.inputBorderError, t)!,
inputBorderWarning: Color.lerp(a.inputBorderWarning, b.inputBorderWarning, t)!,
inputText: Color.lerp(a.inputText, b.inputText, t)!,
inputPlaceholder: Color.lerp(a.inputPlaceholder, b.inputPlaceholder, t)!,
);

@override
List<Object?> get props => [inputBg, inputBorder, inputBorderFocus, inputBorderError, inputText, inputPlaceholder];
List<Object?> get props => [
inputBg,
inputBorder,
inputBorderFocus,
inputBorderError,
inputBorderWarning,
inputText,
inputPlaceholder,
];
}
Loading