@rovula/ui 0.1.7 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/bundle.css +281 -124
- package/dist/cjs/bundle.js +1545 -1545
- package/dist/cjs/bundle.js.map +1 -1
- package/dist/cjs/types/components/AlertDialog/AlertDialog.stories.d.ts +3 -0
- package/dist/cjs/types/components/Dialog/Dialog.d.ts +7 -1
- package/dist/cjs/types/components/Dialog/Dialog.stories.d.ts +3 -0
- package/dist/cjs/types/components/Dropdown/Dropdown.d.ts +2 -0
- package/dist/cjs/types/components/Dropdown/Dropdown.stories.d.ts +2 -0
- package/dist/cjs/types/components/Form/Field.d.ts +26 -0
- package/dist/cjs/types/components/Form/FieldMessage.d.ts +7 -0
- package/dist/cjs/types/components/Form/Form.d.ts +49 -11
- package/dist/cjs/types/components/Form/Form.stories.d.ts +23 -0
- package/dist/cjs/types/components/Form/ValidationHintList.d.ts +17 -0
- package/dist/cjs/types/components/Form/ValidationHintList.stories.d.ts +9 -0
- package/dist/cjs/types/components/Form/index.d.ts +10 -0
- package/dist/cjs/types/components/Form/useOptionBridge.d.ts +17 -0
- package/dist/cjs/types/components/OtpInput/OtpInput.d.ts +17 -0
- package/dist/cjs/types/components/OtpInput/OtpInput.stories.d.ts +15 -0
- package/dist/cjs/types/components/OtpInput/OtpInputGroup.d.ts +25 -0
- package/dist/cjs/types/components/OtpInput/index.d.ts +5 -0
- package/dist/cjs/types/components/TextInput/TextInput.styles.d.ts +3 -0
- package/dist/cjs/types/index.d.ts +5 -0
- package/dist/cjs/types/theme/ThemeColorCoverageRuntime.stories.d.ts +10 -0
- package/dist/cjs/types/utils/colors.d.ts +84 -0
- package/dist/components/ActionButton/ActionButton.stories.js +2 -2
- package/dist/components/ActionButton/ActionButton.styles.js +1 -1
- package/dist/components/AlertDialog/AlertDialog.js +6 -6
- package/dist/components/AlertDialog/AlertDialog.stories.js +3 -0
- package/dist/components/Avatar/Avatar.stories.js +1 -1
- package/dist/components/Avatar/Avatar.styles.js +1 -1
- package/dist/components/Avatar/AvatarBase.js +1 -1
- package/dist/components/Avatar/AvatarGroup.stories.js +1 -1
- package/dist/components/Button/Buttons.stories.js +2 -2
- package/dist/components/Calendar/Calendar.js +1 -1
- package/dist/components/Checkbox/Checkbox.js +1 -1
- package/dist/components/Checkbox/Checkbox.stories.js +17 -7
- package/dist/components/Collapsible/Collapsible.styles.js +1 -1
- package/dist/components/DataTable/DataTable.js +2 -2
- package/dist/components/Dialog/Dialog.js +12 -7
- package/dist/components/Dialog/Dialog.stories.js +90 -2
- package/dist/components/Dropdown/Dropdown.js +2 -2
- package/dist/components/DropdownMenu/DropdownMenu.js +3 -3
- package/dist/components/FocusedScrollView/FocusedScrollView.stories.js +6 -6
- package/dist/components/Form/Field.js +60 -0
- package/dist/components/Form/FieldMessage.js +24 -0
- package/dist/components/Form/Form.js +73 -41
- package/dist/components/Form/Form.stories.js +221 -0
- package/dist/components/Form/ValidationHintList.js +30 -0
- package/dist/components/Form/ValidationHintList.stories.js +50 -0
- package/dist/components/Form/index.js +5 -0
- package/dist/components/Form/useOptionBridge.js +27 -0
- package/dist/components/InputFilter/InputFilter.js +5 -4
- package/dist/components/InputFilter/InputFilter.stories.js +1 -1
- package/dist/components/InputFilter/InputFilter.styles.js +14 -1
- package/dist/components/Label/Label.styles.js +1 -1
- package/dist/components/Menu/Menu.js +2 -2
- package/dist/components/NumberInput/NumberInput.stories.js +1 -1
- package/dist/components/OtpInput/OtpInput.js +118 -0
- package/dist/components/OtpInput/OtpInput.stories.js +60 -0
- package/dist/components/OtpInput/OtpInputGroup.js +23 -0
- package/dist/components/OtpInput/index.js +3 -0
- package/dist/components/PasswordInput/PasswordInput.stories.js +1 -1
- package/dist/components/Popover/Popover.js +1 -1
- package/dist/components/RadioGroup/RadioGroup.js +1 -1
- package/dist/components/RadioGroup/RadioGroup.stories.js +2 -2
- package/dist/components/Search/Search.js +13 -1
- package/dist/components/Search/Search.stories.js +1 -1
- package/dist/components/Slider/Slider.js +1 -1
- package/dist/components/Slider/Slider.stories.js +5 -5
- package/dist/components/Switch/Switch.stories.js +2 -2
- package/dist/components/Table/Table.js +5 -5
- package/dist/components/Tabs/Tabs.js +12 -9
- package/dist/components/Tabs/Tabs.stories.js +1 -1
- package/dist/components/Text/Text.js +1 -1
- package/dist/components/Text/Text.stories.js +1 -1
- package/dist/components/TextArea/TextArea.stories.js +1 -1
- package/dist/components/TextArea/TextArea.styles.js +3 -3
- package/dist/components/TextInput/TextInput.js +3 -2
- package/dist/components/TextInput/TextInput.stories.js +3 -3
- package/dist/components/TextInput/TextInput.styles.js +41 -19
- package/dist/components/Toast/Toast.js +4 -2
- package/dist/components/Toast/Toast.stories.js +1 -1
- package/dist/components/Toast/Toast.styles.js +4 -4
- package/dist/components/Toast/Toaster.js +2 -2
- package/dist/components/Tree/Tree.stories.js +1 -1
- package/dist/components/Tree/TreeItem.js +1 -1
- package/dist/esm/bundle.css +281 -124
- package/dist/esm/bundle.js +1545 -1545
- package/dist/esm/bundle.js.map +1 -1
- package/dist/esm/types/components/AlertDialog/AlertDialog.stories.d.ts +3 -0
- package/dist/esm/types/components/Dialog/Dialog.d.ts +7 -1
- package/dist/esm/types/components/Dialog/Dialog.stories.d.ts +3 -0
- package/dist/esm/types/components/Dropdown/Dropdown.d.ts +2 -0
- package/dist/esm/types/components/Dropdown/Dropdown.stories.d.ts +2 -0
- package/dist/esm/types/components/Form/Field.d.ts +26 -0
- package/dist/esm/types/components/Form/FieldMessage.d.ts +7 -0
- package/dist/esm/types/components/Form/Form.d.ts +49 -11
- package/dist/esm/types/components/Form/Form.stories.d.ts +23 -0
- package/dist/esm/types/components/Form/ValidationHintList.d.ts +17 -0
- package/dist/esm/types/components/Form/ValidationHintList.stories.d.ts +9 -0
- package/dist/esm/types/components/Form/index.d.ts +10 -0
- package/dist/esm/types/components/Form/useOptionBridge.d.ts +17 -0
- package/dist/esm/types/components/OtpInput/OtpInput.d.ts +17 -0
- package/dist/esm/types/components/OtpInput/OtpInput.stories.d.ts +15 -0
- package/dist/esm/types/components/OtpInput/OtpInputGroup.d.ts +25 -0
- package/dist/esm/types/components/OtpInput/index.d.ts +5 -0
- package/dist/esm/types/components/TextInput/TextInput.styles.d.ts +3 -0
- package/dist/esm/types/index.d.ts +5 -0
- package/dist/esm/types/theme/ThemeColorCoverageRuntime.stories.d.ts +10 -0
- package/dist/esm/types/utils/colors.d.ts +84 -0
- package/dist/index.d.ts +245 -2
- package/dist/index.js +3 -0
- package/dist/src/theme/global.css +351 -149
- package/dist/theme/ThemeColorCoverageRuntime.stories.js +91 -0
- package/dist/utils/colors.js +92 -0
- package/package.json +4 -2
- package/src/components/ActionButton/ActionButton.stories.tsx +6 -6
- package/src/components/ActionButton/ActionButton.styles.ts +1 -1
- package/src/components/AlertDialog/AlertDialog.stories.tsx +22 -0
- package/src/components/AlertDialog/AlertDialog.tsx +6 -6
- package/src/components/Avatar/Avatar.stories.tsx +1 -1
- package/src/components/Avatar/Avatar.styles.ts +1 -1
- package/src/components/Avatar/AvatarBase.tsx +1 -1
- package/src/components/Avatar/AvatarGroup.stories.tsx +1 -1
- package/src/components/Button/Buttons.stories.tsx +10 -10
- package/src/components/Calendar/Calendar.tsx +3 -3
- package/src/components/Checkbox/Checkbox.stories.tsx +35 -12
- package/src/components/Checkbox/Checkbox.tsx +7 -5
- package/src/components/Collapsible/Collapsible.styles.ts +1 -1
- package/src/components/DataTable/DataTable.tsx +2 -2
- package/src/components/Dialog/Dialog.stories.tsx +173 -0
- package/src/components/Dialog/Dialog.tsx +32 -15
- package/src/components/Dropdown/Dropdown.styles.ts +1 -1
- package/src/components/Dropdown/Dropdown.tsx +16 -14
- package/src/components/DropdownMenu/DropdownMenu.tsx +3 -3
- package/src/components/FocusedScrollView/FocusedScrollView.stories.tsx +10 -10
- package/src/components/Form/Field.tsx +160 -0
- package/src/components/Form/FieldMessage.tsx +38 -0
- package/src/components/Form/Form.docs.mdx +67 -0
- package/src/components/Form/Form.stories.tsx +490 -0
- package/src/components/Form/Form.tsx +185 -87
- package/src/components/Form/README.md +284 -0
- package/src/components/Form/ValidationHintList.stories.tsx +118 -0
- package/src/components/Form/ValidationHintList.tsx +82 -0
- package/src/components/Form/index.ts +28 -0
- package/src/components/Form/useOptionBridge.ts +55 -0
- package/src/components/InputFilter/InputFilter.stories.tsx +1 -1
- package/src/components/InputFilter/InputFilter.styles.ts +14 -1
- package/src/components/InputFilter/InputFilter.tsx +33 -28
- package/src/components/Label/Label.styles.ts +2 -2
- package/src/components/Label/Label.tsx +1 -1
- package/src/components/Menu/Menu.tsx +12 -12
- package/src/components/NumberInput/NumberInput.stories.tsx +1 -1
- package/src/components/OtpInput/OtpInput.stories.tsx +168 -0
- package/src/components/OtpInput/OtpInput.tsx +210 -0
- package/src/components/OtpInput/OtpInputGroup.tsx +74 -0
- package/src/components/OtpInput/index.ts +5 -0
- package/src/components/PasswordInput/PasswordInput.stories.tsx +1 -1
- package/src/components/Popover/Popover.tsx +1 -1
- package/src/components/RadioGroup/RadioGroup.stories.tsx +4 -4
- package/src/components/RadioGroup/RadioGroup.tsx +2 -1
- package/src/components/Search/Search.stories.tsx +1 -1
- package/src/components/Search/Search.tsx +6 -2
- package/src/components/Slider/Slider.stories.tsx +7 -7
- package/src/components/Slider/Slider.tsx +1 -1
- package/src/components/Switch/Switch.stories.tsx +4 -4
- package/src/components/Table/Table.tsx +5 -5
- package/src/components/Tabs/Tabs.stories.tsx +1 -1
- package/src/components/Tabs/Tabs.tsx +29 -18
- package/src/components/Text/Text.stories.tsx +1 -1
- package/src/components/Text/Text.tsx +1 -1
- package/src/components/TextArea/TextArea.stories.tsx +1 -1
- package/src/components/TextArea/TextArea.styles.ts +3 -3
- package/src/components/TextInput/TextInput.stories.tsx +7 -7
- package/src/components/TextInput/TextInput.styles.ts +42 -19
- package/src/components/TextInput/TextInput.tsx +3 -1
- package/src/components/Toast/Toast.stories.tsx +1 -1
- package/src/components/Toast/Toast.styles.tsx +7 -7
- package/src/components/Toast/Toast.tsx +5 -4
- package/src/components/Toast/Toaster.tsx +17 -20
- package/src/components/Tree/Tree.stories.tsx +1 -1
- package/src/components/Tree/TreeItem.tsx +1 -1
- package/src/index.ts +5 -0
- package/src/theme/ThemeColorCoverageRuntime.stories.tsx +236 -0
- package/src/theme/direct-token-migration-plan.md +121 -0
- package/src/theme/figma-mcp-check-report.md +225 -0
- package/src/theme/figma-mcp-component-checklist.json +1250 -0
- package/src/theme/presets/colors.js +155 -44
- package/src/theme/themes/xspector/components/loading.css +2 -2
- package/src/theme/tokens/color.css +3 -3
- package/src/theme/tokens/components/action-button.css +1 -1
- package/src/theme/tokens/components/dropdown-menu.css +3 -3
- package/src/theme/tokens/components/loading.css +2 -2
- package/src/theme/tokens/components/switch.css +1 -1
- package/src/theme/utils.js +164 -25
- package/src/utils/colors.ts +92 -0
|
@@ -1,99 +1,197 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
1
|
+
import React, { FormHTMLAttributes, ReactNode } from "react";
|
|
2
|
+
import {
|
|
3
|
+
DefaultValues,
|
|
4
|
+
FieldValues,
|
|
5
|
+
FormProvider,
|
|
6
|
+
Mode,
|
|
7
|
+
Resolver,
|
|
8
|
+
SubmitErrorHandler,
|
|
9
|
+
SubmitHandler,
|
|
10
|
+
useForm,
|
|
11
|
+
UseFormReturn,
|
|
12
|
+
} from "react-hook-form";
|
|
13
|
+
import { yupResolver } from "@hookform/resolvers/yup";
|
|
3
14
|
import * as yup from "yup";
|
|
4
15
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
16
|
+
type FormChildren<TFieldValues extends FieldValues> =
|
|
17
|
+
| ReactNode
|
|
18
|
+
| ((methods: UseFormReturn<TFieldValues>) => ReactNode);
|
|
19
|
+
|
|
20
|
+
export type FormController<TFieldValues extends FieldValues> = {
|
|
21
|
+
submit: () => Promise<void>;
|
|
22
|
+
getValues: () => TFieldValues;
|
|
23
|
+
setValue: UseFormReturn<TFieldValues>["setValue"];
|
|
24
|
+
trigger: UseFormReturn<TFieldValues>["trigger"];
|
|
25
|
+
reset: UseFormReturn<TFieldValues>["reset"];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type FormProps<TFieldValues extends FieldValues> = Omit<
|
|
29
|
+
FormHTMLAttributes<HTMLFormElement>,
|
|
30
|
+
"children" | "onSubmit"
|
|
31
|
+
> & {
|
|
32
|
+
children: FormChildren<TFieldValues>;
|
|
33
|
+
defaultValues: DefaultValues<TFieldValues>;
|
|
34
|
+
methods?: UseFormReturn<TFieldValues>;
|
|
35
|
+
controllerRef?: React.MutableRefObject<FormController<TFieldValues> | null>;
|
|
36
|
+
onSubmit: SubmitHandler<TFieldValues>;
|
|
37
|
+
onInvalidSubmit?: SubmitErrorHandler<TFieldValues>;
|
|
38
|
+
resolver?: Resolver<TFieldValues>;
|
|
39
|
+
validationSchema?: yup.ObjectSchema<any>;
|
|
40
|
+
mode?: Mode;
|
|
41
|
+
// RHF does not export this type in all versions.
|
|
42
|
+
// Keep compatibility with allowed re-validate modes.
|
|
43
|
+
reValidateMode?: Exclude<Mode, "onTouched" | "all">;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const createYupResolver = <TFieldValues extends FieldValues>(
|
|
47
|
+
schema: yup.ObjectSchema<any>
|
|
48
|
+
) => yupResolver(schema) as Resolver<TFieldValues>;
|
|
49
|
+
|
|
50
|
+
export type ControlledFormFactoryOptions<TFieldValues extends FieldValues> = {
|
|
51
|
+
methods: UseFormReturn<TFieldValues>;
|
|
52
|
+
defaultValues: DefaultValues<TFieldValues>;
|
|
53
|
+
controllerRef?: React.MutableRefObject<FormController<TFieldValues> | null>;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type UseControlledFormOptions<TFieldValues extends FieldValues> = {
|
|
57
|
+
defaultValues: DefaultValues<TFieldValues>;
|
|
58
|
+
controllerRef?: React.MutableRefObject<FormController<TFieldValues> | null>;
|
|
59
|
+
resolver?: Resolver<TFieldValues>;
|
|
60
|
+
validationSchema?: yup.ObjectSchema<any>;
|
|
61
|
+
mode?: Mode;
|
|
62
|
+
reValidateMode?: Exclude<Mode, "onTouched" | "all">;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const createControlledForm = <TFieldValues extends FieldValues>({
|
|
66
|
+
methods,
|
|
67
|
+
defaultValues,
|
|
68
|
+
controllerRef,
|
|
69
|
+
}: ControlledFormFactoryOptions<TFieldValues>) => {
|
|
70
|
+
const FormRoot = React.forwardRef<
|
|
71
|
+
FormController<TFieldValues>,
|
|
72
|
+
Omit<FormProps<TFieldValues>, "methods" | "defaultValues" | "controllerRef">
|
|
73
|
+
>(({ children, ...props }, ref) => (
|
|
74
|
+
<Form<TFieldValues>
|
|
75
|
+
{...props}
|
|
76
|
+
ref={ref}
|
|
77
|
+
methods={methods}
|
|
78
|
+
defaultValues={defaultValues}
|
|
79
|
+
controllerRef={controllerRef}
|
|
80
|
+
>
|
|
81
|
+
{children}
|
|
82
|
+
</Form>
|
|
83
|
+
));
|
|
84
|
+
FormRoot.displayName = "ControlledFormRoot";
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
methods,
|
|
88
|
+
FormRoot,
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const useControlledForm = <TFieldValues extends FieldValues>({
|
|
93
|
+
defaultValues,
|
|
94
|
+
controllerRef,
|
|
95
|
+
resolver,
|
|
96
|
+
validationSchema,
|
|
97
|
+
mode = "onSubmit",
|
|
98
|
+
reValidateMode = "onChange",
|
|
99
|
+
}: UseControlledFormOptions<TFieldValues>) => {
|
|
100
|
+
const resolvedResolver =
|
|
101
|
+
resolver ??
|
|
102
|
+
(validationSchema
|
|
103
|
+
? createYupResolver<TFieldValues>(validationSchema)
|
|
104
|
+
: undefined);
|
|
105
|
+
|
|
106
|
+
const methods = useForm<TFieldValues>({
|
|
107
|
+
defaultValues,
|
|
108
|
+
resolver: resolvedResolver,
|
|
109
|
+
mode,
|
|
110
|
+
reValidateMode,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return createControlledForm<TFieldValues>({
|
|
114
|
+
methods,
|
|
115
|
+
defaultValues,
|
|
116
|
+
controllerRef,
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const FormInner = <TFieldValues extends FieldValues>(
|
|
121
|
+
{
|
|
122
|
+
children,
|
|
123
|
+
defaultValues,
|
|
124
|
+
methods: externalMethods,
|
|
125
|
+
controllerRef,
|
|
126
|
+
onSubmit,
|
|
127
|
+
onInvalidSubmit,
|
|
128
|
+
resolver,
|
|
129
|
+
validationSchema,
|
|
130
|
+
mode = "onSubmit",
|
|
131
|
+
reValidateMode = "onChange",
|
|
132
|
+
noValidate = true,
|
|
133
|
+
...formProps
|
|
134
|
+
}: FormProps<TFieldValues>,
|
|
135
|
+
ref: React.ForwardedRef<FormController<TFieldValues>>
|
|
136
|
+
) => {
|
|
137
|
+
const resolvedResolver =
|
|
138
|
+
resolver ??
|
|
139
|
+
(validationSchema
|
|
140
|
+
? createYupResolver<TFieldValues>(validationSchema)
|
|
141
|
+
: undefined);
|
|
142
|
+
|
|
143
|
+
const internalMethods = useForm<TFieldValues>({
|
|
144
|
+
defaultValues,
|
|
145
|
+
resolver: resolvedResolver,
|
|
146
|
+
mode,
|
|
147
|
+
reValidateMode,
|
|
30
148
|
});
|
|
31
|
-
const
|
|
32
|
-
|
|
149
|
+
const methods = externalMethods ?? internalMethods;
|
|
150
|
+
|
|
151
|
+
const controller = React.useMemo<FormController<TFieldValues>>(
|
|
152
|
+
() => ({
|
|
153
|
+
submit: async () => {
|
|
154
|
+
await methods.handleSubmit(onSubmit, onInvalidSubmit)();
|
|
155
|
+
},
|
|
156
|
+
getValues: () => methods.getValues(),
|
|
157
|
+
setValue: methods.setValue,
|
|
158
|
+
trigger: methods.trigger,
|
|
159
|
+
reset: methods.reset,
|
|
160
|
+
}),
|
|
161
|
+
[methods, onSubmit, onInvalidSubmit]
|
|
33
162
|
);
|
|
34
163
|
|
|
35
|
-
|
|
36
|
-
const { name, value } = e.target;
|
|
37
|
-
setValues({ ...values, [name]: value });
|
|
38
|
-
};
|
|
164
|
+
React.useImperativeHandle(ref, () => controller, [controller]);
|
|
39
165
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
err.inner.forEach((error) => {
|
|
50
|
-
if (error.path) {
|
|
51
|
-
validationErrors[error.path as keyof FormValues] = error.message;
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
setErrors(validationErrors);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
};
|
|
166
|
+
React.useEffect(() => {
|
|
167
|
+
if (!controllerRef) return;
|
|
168
|
+
|
|
169
|
+
controllerRef.current = controller;
|
|
170
|
+
|
|
171
|
+
return () => {
|
|
172
|
+
controllerRef.current = null;
|
|
173
|
+
};
|
|
174
|
+
}, [controllerRef, controller]);
|
|
58
175
|
|
|
59
176
|
return (
|
|
60
|
-
<
|
|
61
|
-
<
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
/>
|
|
70
|
-
{errors.name && <span className="error">{errors.name}</span>}
|
|
71
|
-
</div>
|
|
72
|
-
<div className="form-group">
|
|
73
|
-
<label htmlFor="email">Email</label>
|
|
74
|
-
<input
|
|
75
|
-
id="email"
|
|
76
|
-
name="email"
|
|
77
|
-
type="email"
|
|
78
|
-
value={values.email}
|
|
79
|
-
onChange={handleChange}
|
|
80
|
-
/>
|
|
81
|
-
{errors.email && <span className="error">{errors.email}</span>}
|
|
82
|
-
</div>
|
|
83
|
-
<div className="form-group">
|
|
84
|
-
<label htmlFor="password">Password</label>
|
|
85
|
-
<input
|
|
86
|
-
id="password"
|
|
87
|
-
name="password"
|
|
88
|
-
type="password"
|
|
89
|
-
value={values.password}
|
|
90
|
-
onChange={handleChange}
|
|
91
|
-
/>
|
|
92
|
-
{errors.password && <span className="error">{errors.password}</span>}
|
|
93
|
-
</div>
|
|
94
|
-
<button type="submit">Submit</button>
|
|
95
|
-
</form>
|
|
177
|
+
<FormProvider {...methods}>
|
|
178
|
+
<form
|
|
179
|
+
{...formProps}
|
|
180
|
+
noValidate={noValidate}
|
|
181
|
+
onSubmit={methods.handleSubmit(onSubmit, onInvalidSubmit)}
|
|
182
|
+
>
|
|
183
|
+
{typeof children === "function" ? children(methods) : children}
|
|
184
|
+
</form>
|
|
185
|
+
</FormProvider>
|
|
96
186
|
);
|
|
97
187
|
};
|
|
98
188
|
|
|
189
|
+
type FormComponent = <TFieldValues extends FieldValues>(
|
|
190
|
+
props: FormProps<TFieldValues> & {
|
|
191
|
+
ref?: React.Ref<FormController<TFieldValues>>;
|
|
192
|
+
}
|
|
193
|
+
) => React.ReactElement;
|
|
194
|
+
|
|
195
|
+
export const Form = React.forwardRef(FormInner) as FormComponent;
|
|
196
|
+
|
|
99
197
|
export default Form;
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# Form Component Guide
|
|
2
|
+
|
|
3
|
+
This guide explains how to use the `Form` system in this UI package:
|
|
4
|
+
|
|
5
|
+
- `Form` (react-hook-form wrapper)
|
|
6
|
+
- `Field` (adapter for UI components)
|
|
7
|
+
- `FieldMessage` (error message renderer)
|
|
8
|
+
- `ValidationHintList` (rule checklist UI)
|
|
9
|
+
- `useControlledForm` / `createControlledForm` (control form from parent layer)
|
|
10
|
+
- `useOptionBridge` (map `id <-> option` for async dropdown use cases)
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
import * as yup from "yup";
|
|
18
|
+
import { Form, Field } from "@/components/Form";
|
|
19
|
+
import TextInput from "@/components/TextInput/TextInput";
|
|
20
|
+
import Button from "@/components/Button/Button";
|
|
21
|
+
|
|
22
|
+
type LoginFormValues = {
|
|
23
|
+
email: string;
|
|
24
|
+
password: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const schema = yup.object({
|
|
28
|
+
email: yup.string().email("Invalid email").required("Email is required"),
|
|
29
|
+
password: yup.string().min(6, "At least 6 characters").required("Password is required"),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
<Form<LoginFormValues>
|
|
33
|
+
defaultValues={{ email: "", password: "" }}
|
|
34
|
+
validationSchema={schema}
|
|
35
|
+
onSubmit={(values) => {
|
|
36
|
+
console.log(values);
|
|
37
|
+
}}
|
|
38
|
+
>
|
|
39
|
+
<Field<LoginFormValues, "email">
|
|
40
|
+
name="email"
|
|
41
|
+
component={TextInput}
|
|
42
|
+
componentProps={{ label: "Email", required: true }}
|
|
43
|
+
/>
|
|
44
|
+
<Field<LoginFormValues, "password">
|
|
45
|
+
name="password"
|
|
46
|
+
component={TextInput}
|
|
47
|
+
componentProps={{ label: "Password", type: "password", required: true }}
|
|
48
|
+
/>
|
|
49
|
+
<Button type="submit">Sign in</Button>
|
|
50
|
+
</Form>;
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Core Concepts
|
|
56
|
+
|
|
57
|
+
### 1) `defaultValues` is the source of truth
|
|
58
|
+
|
|
59
|
+
Set initial values at `Form.defaultValues` whenever possible.
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
<Form defaultValues={{ role: "dev" }} ... />
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
You can also use `Field.defaultValue` for specific cases, but the primary pattern is form-level defaults.
|
|
66
|
+
|
|
67
|
+
### 2) `Field` injects value/change/blur/ref
|
|
68
|
+
|
|
69
|
+
`Field` internally uses `useController` and injects controlled props into your component.
|
|
70
|
+
|
|
71
|
+
By default:
|
|
72
|
+
|
|
73
|
+
- value prop name: `value`
|
|
74
|
+
- change prop name: `onChange`
|
|
75
|
+
- blur prop name: `onBlur`
|
|
76
|
+
- ref prop name: `ref`
|
|
77
|
+
|
|
78
|
+
If your component uses different prop names, map them with `valuePropName`, `changePropName`, etc.
|
|
79
|
+
|
|
80
|
+
### 3) Validation modes are RHF modes
|
|
81
|
+
|
|
82
|
+
`Form.mode` and `Form.reValidateMode` pass through to react-hook-form.
|
|
83
|
+
|
|
84
|
+
Examples:
|
|
85
|
+
|
|
86
|
+
- `mode="onSubmit"` (default)
|
|
87
|
+
- `mode="onChange"`
|
|
88
|
+
- `mode="onTouched"`
|
|
89
|
+
|
|
90
|
+
Important: `onTouched` requires your input path to call `onBlur` correctly.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## `Field` Props You Will Use Most
|
|
95
|
+
|
|
96
|
+
### `component` and `componentProps`
|
|
97
|
+
|
|
98
|
+
Use `component` for the render target, and `componentProps` for static/custom props.
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
<Field
|
|
102
|
+
name="email"
|
|
103
|
+
component={TextInput}
|
|
104
|
+
componentProps={{ label: "Email", helperText: "Use your work email" }}
|
|
105
|
+
/>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Mapping non-standard component APIs
|
|
109
|
+
|
|
110
|
+
If a component does not use `value` + `onChange`, map them:
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
<Field
|
|
114
|
+
name="role"
|
|
115
|
+
component={Dropdown}
|
|
116
|
+
valuePropName="value"
|
|
117
|
+
changePropName="onSelect"
|
|
118
|
+
/>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Transform value in/out (`formatValue` and `parseValue`)
|
|
122
|
+
|
|
123
|
+
- `formatValue`: form state -> component value
|
|
124
|
+
- `parseValue`: component payload -> form state
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
<Field
|
|
128
|
+
name="roleId"
|
|
129
|
+
component={Dropdown}
|
|
130
|
+
valuePropName="value"
|
|
131
|
+
changePropName="onSelect"
|
|
132
|
+
formatValue={(roleId) => options.find((item) => item.value === roleId)}
|
|
133
|
+
parseValue={(option) => (option as Options | undefined)?.value ?? ""}
|
|
134
|
+
/>
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Optional passthrough toggles
|
|
138
|
+
|
|
139
|
+
Disable specific injected props when a component should not receive them:
|
|
140
|
+
|
|
141
|
+
- `blurPropName={false}`
|
|
142
|
+
- `refPropName={false}`
|
|
143
|
+
- `errorMessagePropName={false}`
|
|
144
|
+
- `invalidPropName={false}`
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Showing Error Messages
|
|
149
|
+
|
|
150
|
+
### A) Through component props (default behavior)
|
|
151
|
+
|
|
152
|
+
`Field` sends:
|
|
153
|
+
|
|
154
|
+
- `errorMessage` (from RHF field error)
|
|
155
|
+
- `error` (boolean invalid state)
|
|
156
|
+
|
|
157
|
+
You can rename these with:
|
|
158
|
+
|
|
159
|
+
- `errorMessagePropName`
|
|
160
|
+
- `invalidPropName`
|
|
161
|
+
|
|
162
|
+
### B) Via `FieldMessage`
|
|
163
|
+
|
|
164
|
+
Use when your input component does not render its own message:
|
|
165
|
+
|
|
166
|
+
```tsx
|
|
167
|
+
<Field name="email" component={TextInput} />
|
|
168
|
+
<FieldMessage name="email" />
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Async Dropdown Pattern (`id` in form, option object in UI)
|
|
174
|
+
|
|
175
|
+
Most projects store only primitive IDs in form state (`string`/`number`) and map to option objects for UI rendering.
|
|
176
|
+
|
|
177
|
+
Use `useOptionBridge` to avoid repeated mapping code.
|
|
178
|
+
|
|
179
|
+
```tsx
|
|
180
|
+
import { useOptionBridge } from "@/components/Form";
|
|
181
|
+
|
|
182
|
+
const { toOption, toValue } = useOptionBridge({
|
|
183
|
+
options, // [{ value: "dev", label: "Developer" }]
|
|
184
|
+
loading,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
<Field<FormValues, "roleId", DropdownProps>
|
|
188
|
+
name="roleId"
|
|
189
|
+
component={Dropdown}
|
|
190
|
+
componentProps={{ options, disabled: loading }}
|
|
191
|
+
valuePropName="value"
|
|
192
|
+
changePropName="onSelect"
|
|
193
|
+
formatValue={(id) => toOption(id)}
|
|
194
|
+
parseValue={(option) => toValue(option as Option | undefined) ?? ""}
|
|
195
|
+
/>;
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
If options can be delayed, use `toOptionWithFallback` and provide `buildFallbackOption` when needed.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Controlled Form From Parent Layer
|
|
203
|
+
|
|
204
|
+
Use `useControlledForm` when a parent (outside form JSX) needs to:
|
|
205
|
+
|
|
206
|
+
- submit
|
|
207
|
+
- read values
|
|
208
|
+
- set values
|
|
209
|
+
- trigger validation
|
|
210
|
+
- reset
|
|
211
|
+
|
|
212
|
+
```tsx
|
|
213
|
+
const { methods, FormRoot } = useControlledForm<FormValues>({
|
|
214
|
+
defaultValues,
|
|
215
|
+
validationSchema: schema,
|
|
216
|
+
mode: "onChange",
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
<Button type="button" onClick={() => methods.handleSubmit(onSubmit)()}>
|
|
220
|
+
Submit outside form
|
|
221
|
+
</Button>
|
|
222
|
+
|
|
223
|
+
<FormRoot onSubmit={onSubmit}>
|
|
224
|
+
<Field name="title" component={TextInput} />
|
|
225
|
+
</FormRoot>;
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
You can also use `controllerRef` / forwarded ref with `FormController`.
|
|
229
|
+
|
|
230
|
+
### Ref API (`FormController`)
|
|
231
|
+
|
|
232
|
+
When using `ref` or `controllerRef`, available methods are:
|
|
233
|
+
|
|
234
|
+
- `submit()`
|
|
235
|
+
- `getValues()`
|
|
236
|
+
- `setValue()`
|
|
237
|
+
- `trigger()`
|
|
238
|
+
- `reset()`
|
|
239
|
+
|
|
240
|
+
```tsx
|
|
241
|
+
const formRef = React.useRef<FormController<FormValues> | null>(null);
|
|
242
|
+
|
|
243
|
+
await formRef.current?.submit();
|
|
244
|
+
formRef.current?.setValue("title", "New title", { shouldValidate: true });
|
|
245
|
+
const values = formRef.current?.getValues();
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Best Practices
|
|
251
|
+
|
|
252
|
+
- Keep form state as primitives (`id`, `code`) unless you truly need full objects.
|
|
253
|
+
- Prefer `Form.defaultValues` over per-field defaults.
|
|
254
|
+
- Keep input components controlled and ensure they call `onChange` and `onBlur`.
|
|
255
|
+
- Use `formatValue`/`parseValue` only when API shape differs.
|
|
256
|
+
- For `mode="onTouched"`, make sure focus leaving the field triggers `onBlur` once.
|
|
257
|
+
- Avoid inline anonymous components in `Field.component` for stable rendering.
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## Common Pitfalls
|
|
262
|
+
|
|
263
|
+
- **Validation not running on touched**: custom input does not call `onBlur`.
|
|
264
|
+
- **Dropdown not showing selected label**: options not loaded yet, or `formatValue` missing.
|
|
265
|
+
- **Submitted payload too heavy**: storing full option objects instead of IDs.
|
|
266
|
+
- **State mismatch**: setting local component default separately from RHF default values.
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## Export Surface
|
|
271
|
+
|
|
272
|
+
From `@/components/Form`:
|
|
273
|
+
|
|
274
|
+
- `Form`
|
|
275
|
+
- `Field`
|
|
276
|
+
- `FieldMessage`
|
|
277
|
+
- `ValidationHintList`
|
|
278
|
+
- `useControlledForm`
|
|
279
|
+
- `createControlledForm`
|
|
280
|
+
- `createYupResolver`
|
|
281
|
+
- `useOptionBridge`
|
|
282
|
+
|
|
283
|
+
Related types are exported alongside these utilities.
|
|
284
|
+
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
|
+
import PasswordInput from "@/components/PasswordInput";
|
|
4
|
+
import { ValidationHintList, ValidationHintRule } from "./ValidationHintList";
|
|
5
|
+
|
|
6
|
+
type PasswordValues = {
|
|
7
|
+
password: string;
|
|
8
|
+
confirmPassword: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const rules: ValidationHintRule<PasswordValues>[] = [
|
|
12
|
+
{
|
|
13
|
+
id: "min-length",
|
|
14
|
+
label: "Minimum 12 characters",
|
|
15
|
+
validate: (values) => values.password.length >= 12,
|
|
16
|
+
when: (values) => Boolean(values.password),
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: "upper-lower",
|
|
20
|
+
label: "At least one upper and lower case",
|
|
21
|
+
validate: (values) =>
|
|
22
|
+
/[a-z]/.test(values.password) && /[A-Z]/.test(values.password),
|
|
23
|
+
when: (values) => Boolean(values.password),
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: "match",
|
|
27
|
+
label: "Password and confirmation are matching",
|
|
28
|
+
validate: (values) =>
|
|
29
|
+
Boolean(values.password) && values.password === values.confirmPassword,
|
|
30
|
+
when: (values) =>
|
|
31
|
+
Boolean(values.password) && Boolean(values.confirmPassword),
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const meta: Meta = {
|
|
36
|
+
title: "Components/Form/ValidationHintList",
|
|
37
|
+
tags: ["autodocs"],
|
|
38
|
+
parameters: {
|
|
39
|
+
layout: "centered",
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export default meta;
|
|
44
|
+
type Story = StoryObj;
|
|
45
|
+
|
|
46
|
+
export const ThreeState = {
|
|
47
|
+
render: () => {
|
|
48
|
+
const [values, setValues] = useState<PasswordValues>({
|
|
49
|
+
password: "",
|
|
50
|
+
confirmPassword: "",
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className="w-[420px] max-w-full rounded-md bg-bg-bg2 p-4">
|
|
55
|
+
<div className="flex flex-col gap-3">
|
|
56
|
+
<PasswordInput
|
|
57
|
+
label="Password"
|
|
58
|
+
value={values.password}
|
|
59
|
+
onChange={(event) =>
|
|
60
|
+
setValues((prev) => ({ ...prev, password: event.target.value }))
|
|
61
|
+
}
|
|
62
|
+
/>
|
|
63
|
+
<PasswordInput
|
|
64
|
+
label="Confirm password"
|
|
65
|
+
value={values.confirmPassword}
|
|
66
|
+
onChange={(event) =>
|
|
67
|
+
setValues((prev) => ({
|
|
68
|
+
...prev,
|
|
69
|
+
confirmPassword: event.target.value,
|
|
70
|
+
}))
|
|
71
|
+
}
|
|
72
|
+
/>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<ValidationHintList values={values} rules={rules} />
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
},
|
|
79
|
+
} satisfies Story;
|
|
80
|
+
|
|
81
|
+
export const TwoState = {
|
|
82
|
+
render: () => {
|
|
83
|
+
const [values, setValues] = useState<PasswordValues>({
|
|
84
|
+
password: "",
|
|
85
|
+
confirmPassword: "",
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<div className="w-[420px] max-w-full rounded-md bg-bg-bg2 p-4">
|
|
90
|
+
<div className="flex flex-col gap-3">
|
|
91
|
+
<PasswordInput
|
|
92
|
+
label="Password"
|
|
93
|
+
value={values.password}
|
|
94
|
+
onChange={(event) =>
|
|
95
|
+
setValues((prev) => ({ ...prev, password: event.target.value }))
|
|
96
|
+
}
|
|
97
|
+
/>
|
|
98
|
+
<PasswordInput
|
|
99
|
+
label="Confirm password"
|
|
100
|
+
value={values.confirmPassword}
|
|
101
|
+
onChange={(event) =>
|
|
102
|
+
setValues((prev) => ({
|
|
103
|
+
...prev,
|
|
104
|
+
confirmPassword: event.target.value,
|
|
105
|
+
}))
|
|
106
|
+
}
|
|
107
|
+
/>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<ValidationHintList
|
|
111
|
+
values={values}
|
|
112
|
+
rules={rules}
|
|
113
|
+
mode={["pending", "valid"]}
|
|
114
|
+
/>
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
},
|
|
118
|
+
} satisfies Story;
|