Skip to content

Commit a380fc0

Browse files
committed
feat: field-error-messages
1 parent 82d4aaa commit a380fc0

File tree

11 files changed

+270
-4
lines changed

11 files changed

+270
-4
lines changed

src/lib/form/bignumber-field/index.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ function BigNumberField({
4747
groupProps,
4848
descriptionProps,
4949
errorMessageProps,
50+
validationResult,
5051
} = useBigNumberField({ id, isDisabled, placeholder, isReadOnly, ...props });
5152

5253
return (
@@ -68,6 +69,8 @@ function BigNumberField({
6869
<>
6970
<Input
7071
{...inputProps}
72+
id="BigNumberField"
73+
aria-errormessage="BigNumberFieldError"
7174
name={name}
7275
className={cn(
7376
"hover-short-transition bg-klerosUIComponentsWhiteBackground size-full",
@@ -83,6 +86,7 @@ function BigNumberField({
8386
"border-klerosUIComponentsError": variant === "error",
8487
"border-klerosUIComponentsSuccess": variant === "success",
8588
},
89+
"invalid:border-klerosUIComponentsError",
8690
inputProps?.className,
8791
)}
8892
/>
@@ -181,6 +185,18 @@ function BigNumberField({
181185
{message}
182186
</div>
183187
)}
188+
{props.showFieldError && validationResult.isInvalid && (
189+
<span
190+
id="BigNumberFieldError"
191+
aria-label={validationResult.validationError}
192+
className={cn(
193+
"text-klerosUIComponentsError mt-1 text-sm break-words",
194+
props.fieldErrorClassName,
195+
)}
196+
>
197+
{validationResult.validationError}
198+
</span>
199+
)}
184200
</div>
185201
);
186202
}

src/lib/form/bignumber-field/useBigNumberField.tsx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,15 @@ export interface BigNumberFieldProps {
6363
formatOptions?: FormatOptions;
6464
/** Additional props for the input element. */
6565
inputProps?: React.InputHTMLAttributes<HTMLInputElement>;
66+
/** A function that returns an error message if a given value is invalid.
67+
* Return a string to denote invalid.*/
68+
validate?: (value: BigNumber | null) => true | null | undefined | string;
69+
/** Flag to enable field errors, alternative to `message`
70+
* This will show the validation errors from browser, or custom error in case `validate` is setup on Field.
71+
*/
72+
showFieldError?: boolean;
73+
/** ClassName for field error message. */
74+
fieldErrorClassName?: string;
6675
}
6776

6877
// Default format configuration
@@ -124,6 +133,12 @@ export function useBigNumberField(props: BigNumberFieldProps) {
124133
// State to track if the input is currently formatted
125134
const [isFormatted, setIsFormatted] = useState<boolean>(false);
126135

136+
// State to track input's validation
137+
const [validationResult, setValidationResult] = useState<{
138+
isInvalid: boolean;
139+
validationError?: string;
140+
}>({ isInvalid: false });
141+
127142
// State for the numeric value
128143
const [numberValue, setNumberValue] = useState<BigNumber | null>(() => {
129144
try {
@@ -231,6 +246,7 @@ export function useBigNumberField(props: BigNumberFieldProps) {
231246
setInputValue(newValue.toString());
232247
setIsFormatted(false);
233248
onChange?.(newValue);
249+
setValidationResult(getValidationResult(newValue));
234250
};
235251

236252
const decrement = () => {
@@ -252,6 +268,7 @@ export function useBigNumberField(props: BigNumberFieldProps) {
252268
setInputValue(newValue.toString());
253269
setIsFormatted(false);
254270
onChange?.(newValue);
271+
setValidationResult(getValidationResult(newValue));
255272
};
256273

257274
// Helper function to escape special characters in regex
@@ -415,6 +432,8 @@ export function useBigNumberField(props: BigNumberFieldProps) {
415432
} else if (inputValue !== "" && inputValue !== "-") {
416433
setInputValue("");
417434
}
435+
436+
setValidationResult(getValidationResult());
418437
};
419438

420439
// Handle keyboard events
@@ -552,6 +571,7 @@ export function useBigNumberField(props: BigNumberFieldProps) {
552571
readOnly: isReadOnly,
553572
required: props.isRequired,
554573
placeholder: props.placeholder,
574+
"aria-invalid": validationResult?.isInvalid,
555575
...getAriaAttributes(),
556576
...props.inputProps,
557577
});
@@ -561,6 +581,32 @@ export function useBigNumberField(props: BigNumberFieldProps) {
561581
htmlFor: id,
562582
});
563583

584+
// Field Error Render props
585+
const getValidationResult = (value?: BigNumber) => {
586+
const fieldErrorProps = {
587+
isInvalid: false,
588+
validationError: "",
589+
};
590+
591+
if (
592+
props.isRequired &&
593+
(value ? value.toString() : inputValue).trim() === ""
594+
) {
595+
fieldErrorProps.isInvalid = true;
596+
fieldErrorProps.validationError = "Please fill out this field.";
597+
}
598+
599+
const validate = props.validate;
600+
if (validate) {
601+
const result = validate(value ?? numberValue);
602+
if (typeof result === `string`) {
603+
fieldErrorProps.isInvalid = true;
604+
fieldErrorProps.validationError = result;
605+
}
606+
}
607+
return fieldErrorProps;
608+
};
609+
564610
// Increment button props
565611
const getIncrementButtonProps = () => ({
566612
type: "button" as const,
@@ -603,6 +649,7 @@ export function useBigNumberField(props: BigNumberFieldProps) {
603649
groupProps: getGroupProps(),
604650
descriptionProps: getDescriptionProps(),
605651
errorMessageProps: getErrorMessageProps(),
652+
validationResult,
606653
inputValue,
607654
numberValue,
608655
canIncrement: canIncrement(),

src/lib/form/number-field.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import {
1515
type NumberFieldProps as AriaNumberFieldProps,
1616
Label,
1717
Text,
18+
type FieldErrorProps,
19+
FieldError,
1820
} from "react-aria-components";
1921
import { cn } from "../../utils";
2022
import clsx from "clsx";
@@ -29,6 +31,14 @@ interface NumberFieldProps extends AriaNumberFieldProps {
2931
*/
3032
inputProps?: InputProps;
3133
label?: string;
34+
/** Flag to enable field errors, alternative to `message`
35+
* This will show the validation errors from browser, or custom error in case `validate` is setup on Field.
36+
*/
37+
showFieldError?: boolean;
38+
/** Props for FieldError in case `showFieldError` is true.
39+
* [See FieldErrorProps](https://react-spectrum.adobe.com/react-aria/NumberField.html#fielderror)
40+
*/
41+
fieldErrorProps?: FieldErrorProps;
3242
}
3343

3444
/** A number field allows a user to enter a number, and increment or decrement the value using stepper buttons. */
@@ -41,6 +51,8 @@ function NumberField({
4151
label,
4252
isDisabled,
4353
inputProps,
54+
showFieldError,
55+
fieldErrorProps,
4456
...props
4557
}: Readonly<NumberFieldProps>) {
4658
return (
@@ -170,6 +182,18 @@ function NumberField({
170182
{message}
171183
</Text>
172184
)}
185+
186+
{showFieldError && (
187+
<FieldError
188+
{...fieldErrorProps}
189+
className={cn(
190+
"text-klerosUIComponentsError mt-1 text-sm break-words",
191+
fieldErrorProps?.className,
192+
)}
193+
>
194+
{fieldErrorProps?.children}
195+
</FieldError>
196+
)}
173197
</AriaNumberField>
174198
);
175199
}

src/lib/form/searchbar.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import React from "react";
22
import SearchIcon from "../../assets/svgs/form/search.svg";
33
import {
4+
FieldError,
5+
type FieldErrorProps,
46
Group,
57
Input,
68
type InputProps,
@@ -18,13 +20,23 @@ interface SearchbarProps extends SearchFieldProps {
1820
* [See InputProps](https://react-spectrum.adobe.com/react-aria/NumberField.html#input-1)
1921
*/
2022
inputProps?: InputProps;
23+
/** Flag to enable field errors, alternative to `message`
24+
* This will show the validation errors from browser, or custom error in case `validate` is setup on Field.
25+
*/
26+
showFieldError?: boolean;
27+
/** Props for FieldError in case `showFieldError` is true.
28+
* [See FieldErrorProps](https://react-spectrum.adobe.com/react-aria/SearchField.html#fielderror)
29+
*/
30+
fieldErrorProps?: FieldErrorProps;
2131
}
2232
/** A search field allows a user to enter and clear a search query. */
2333
function Searchbar({
2434
label,
2535
placeholder,
2636
inputProps,
2737
className,
38+
showFieldError,
39+
fieldErrorProps,
2840
...props
2941
}: Readonly<SearchbarProps>) {
3042
return (
@@ -57,6 +69,17 @@ function Searchbar({
5769
)}
5870
/>
5971
</Group>
72+
{showFieldError && (
73+
<FieldError
74+
{...fieldErrorProps}
75+
className={cn(
76+
"text-klerosUIComponentsError mt-1 text-sm break-words",
77+
fieldErrorProps?.className,
78+
)}
79+
>
80+
{fieldErrorProps?.children}
81+
</FieldError>
82+
)}
6083
</SearchField>
6184
);
6285
}

src/lib/form/text-area.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import {
1212
type TextFieldProps,
1313
type TextAreaProps as AriaTextAreaProps,
1414
Text,
15+
type FieldErrorProps,
16+
FieldError,
1517
} from "react-aria-components";
1618
import { cn } from "../../utils";
1719

@@ -28,6 +30,14 @@ interface TextAreaProps extends TextFieldProps {
2830
resizeX?: boolean;
2931
/** Allow resizing along y-axis */
3032
resizeY?: boolean;
33+
/** Flag to enable field errors, alternative to `message`
34+
* This will show the validation errors from browser, or custom error in case `validate` is setup on Field.
35+
*/
36+
showFieldError?: boolean;
37+
/** Props for FieldError in case `showFieldError` is true.
38+
* [See FieldErrorProps](https://react-spectrum.adobe.com/react-aria/TextField.html#fielderror)
39+
*/
40+
fieldErrorProps?: FieldErrorProps;
3141
}
3242

3343
/** TextArea components supports multiline input and can be configured to resize. */
@@ -39,6 +49,8 @@ function TextArea({
3949
placeholder,
4050
resizeX = false,
4151
resizeY = false,
52+
showFieldError,
53+
fieldErrorProps,
4254
...props
4355
}: Readonly<TextAreaProps>) {
4456
return (
@@ -101,6 +113,17 @@ function TextArea({
101113
{message}
102114
</Text>
103115
)}
116+
{showFieldError && (
117+
<FieldError
118+
{...fieldErrorProps}
119+
className={cn(
120+
"text-klerosUIComponentsError mt-1 text-sm break-words",
121+
fieldErrorProps?.className,
122+
)}
123+
>
124+
{fieldErrorProps?.children}
125+
</FieldError>
126+
)}
104127
</TextField>
105128
);
106129
}

src/lib/form/text-field.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import {
1212
type TextFieldProps as AriaTextFieldProps,
1313
Label,
1414
Group,
15+
FieldError,
16+
type FieldErrorProps,
1517
} from "react-aria-components";
1618
import { cn } from "../../utils";
1719

@@ -25,6 +27,14 @@ interface TextFieldProps extends AriaTextFieldProps {
2527
*/
2628
inputProps?: InputProps;
2729
label?: string;
30+
/** Flag to enable field errors, alternative to `message`
31+
* This will show the validation errors from browser, or custom error in case `validate` is setup on Field.
32+
*/
33+
showFieldError?: boolean;
34+
/** Props for FieldError in case `showFieldError` is true.
35+
* [See FieldErrorProps](https://react-spectrum.adobe.com/react-aria/TextField.html#fielderror)
36+
*/
37+
fieldErrorProps?: FieldErrorProps;
2838
}
2939
/** A text field allows a user to enter a plain text value with a keyboard. */
3040
function TextField({
@@ -34,6 +44,8 @@ function TextField({
3444
className,
3545
placeholder,
3646
label,
47+
showFieldError,
48+
fieldErrorProps,
3749
...props
3850
}: Readonly<TextFieldProps>) {
3951
return (
@@ -109,6 +121,18 @@ function TextField({
109121
{message}
110122
</Text>
111123
)}
124+
125+
{showFieldError && (
126+
<FieldError
127+
{...fieldErrorProps}
128+
className={cn(
129+
"text-klerosUIComponentsError mt-1 text-sm break-words",
130+
fieldErrorProps?.className,
131+
)}
132+
>
133+
{fieldErrorProps?.children}
134+
</FieldError>
135+
)}
112136
</AriaTextField>
113137
);
114138
}

src/stories/bignumber-field.stories.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import React from "react";
12
import { Meta, StoryObj } from "@storybook/react";
23
import BigNumberField from "../lib/form/bignumber-field";
34
import Telegram from "../assets/svgs/telegram.svg";
45
import BigNumber from "bignumber.js";
56
import { IPreviewArgs } from "./utils";
7+
import { Button, Form } from "../lib";
68

79
const meta: Meta<typeof BigNumberField> = {
810
title: "Form/BigNumberField",
@@ -183,3 +185,33 @@ export const ReadOnly: Story = {
183185
defaultValue: "42",
184186
},
185187
};
188+
189+
/** Make a field required. Optionally you can choose to show the validation error and customize their style. */
190+
export const Required: Story = {
191+
args: {
192+
...Default.args,
193+
isRequired: true,
194+
},
195+
render: (args) => (
196+
<Form
197+
onSubmit={(e) => {
198+
e.preventDefault();
199+
}}
200+
>
201+
<BigNumberField
202+
{...args}
203+
placeholder="Enter '0'"
204+
showFieldError
205+
validate={(value) => (value?.eq(0) ? "Zero not allowed" : null)}
206+
/>
207+
<Button
208+
variant="primary"
209+
type="submit"
210+
aria-pressed="true"
211+
text="Click me!"
212+
small
213+
className="mt-4"
214+
/>
215+
</Form>
216+
),
217+
};

0 commit comments

Comments
 (0)