📝(react) add RHF examples
Rework a bit the recent work made on RHF example to make some component more generic, such as RhfSelect and RhfDatepicker, which is based on a design using RHF context hooks to provide a seamless integration. Fixes #144
This commit is contained in:
5
.changeset/chilly-ways-dance.md
Normal file
5
.changeset/chilly-ways-dance.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@openfun/cunningham-react": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
add RHF examples
|
||||||
@@ -17,6 +17,8 @@ module.exports = {
|
|||||||
"**/*.stories.tsx",
|
"**/*.stories.tsx",
|
||||||
"**/*.spec.tsx",
|
"**/*.spec.tsx",
|
||||||
"src/tests/*",
|
"src/tests/*",
|
||||||
|
"**/stories-utils.tsx",
|
||||||
|
"**/reactHookFormUtils.tsx"
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ import { yupResolver } from "@hookform/resolvers/yup";
|
|||||||
import { Meta, StoryFn } from "@storybook/react";
|
import { Meta, StoryFn } from "@storybook/react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import * as Yup from "yup";
|
import * as Yup from "yup";
|
||||||
import { Checkbox, CheckboxGroup } from ":/components/Forms/Checkbox/index";
|
|
||||||
import { Button } from ":/components/Button";
|
|
||||||
import {
|
import {
|
||||||
getFieldState,
|
getFieldState,
|
||||||
getFieldErrorMessage,
|
getFieldErrorMessage,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
} from ":/tests/reactHookFormUtils";
|
} from ":/components/Forms/Examples/ReactHookForm/reactHookFormUtils";
|
||||||
|
import { Checkbox, CheckboxGroup } from ":/components/Forms/Checkbox/index";
|
||||||
|
import { Button } from ":/components/Button";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Components/Forms/Checkbox",
|
title: "Components/Forms/Checkbox",
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
import { Meta, StoryFn } from "@storybook/react";
|
import { Meta, StoryFn } from "@storybook/react";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
import * as Yup from "yup";
|
||||||
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
import { CunninghamProvider } from ":/components/Provider";
|
import { CunninghamProvider } from ":/components/Provider";
|
||||||
import { Button } from ":/components/Button";
|
import { Button } from ":/components/Button";
|
||||||
import { DateRangePicker } from ":/components/Forms/DatePicker/DateRangePicker";
|
import { DateRangePicker } from ":/components/Forms/DatePicker/DateRangePicker";
|
||||||
import { DatePicker } from ":/components/Forms/DatePicker/DatePicker";
|
import { DatePicker } from ":/components/Forms/DatePicker/DatePicker";
|
||||||
|
import { onSubmit } from ":/components/Forms/Examples/ReactHookForm/reactHookFormUtils";
|
||||||
|
import { RhfDatePicker } from ":/components/Forms/DatePicker/stories-utils";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Components/Forms/DatePicker",
|
title: "Components/Forms/DatePicker",
|
||||||
@@ -135,6 +140,38 @@ export const Controlled = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ReactHookForm = () => {
|
||||||
|
const methods = useForm({
|
||||||
|
defaultValues: {
|
||||||
|
date: "",
|
||||||
|
},
|
||||||
|
resolver: yupResolver(
|
||||||
|
Yup.object().shape({
|
||||||
|
date: Yup.string().required(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CunninghamProvider>
|
||||||
|
<FormProvider {...methods}>
|
||||||
|
<form
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "1rem",
|
||||||
|
width: "400px",
|
||||||
|
}}
|
||||||
|
onSubmit={methods.handleSubmit(onSubmit)}
|
||||||
|
>
|
||||||
|
<RhfDatePicker name="date" label="Pick a date" fullWidth={true} />
|
||||||
|
<Button fullWidth={true}>Submit</Button>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
</CunninghamProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const RangeDefault = () => {
|
export const RangeDefault = () => {
|
||||||
return (
|
return (
|
||||||
<div style={{ minHeight: "400px" }}>
|
<div style={{ minHeight: "400px" }}>
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
DatePicker,
|
||||||
|
DatePickerProps,
|
||||||
|
} from ":/components/Forms/DatePicker/DatePicker";
|
||||||
|
|
||||||
|
export const RhfDatePicker = (props: DatePickerProps & { name: string }) => {
|
||||||
|
const { control } = useFormContext();
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={props.name}
|
||||||
|
render={({ field, fieldState }) => {
|
||||||
|
return (
|
||||||
|
<DatePicker
|
||||||
|
{...props}
|
||||||
|
state={fieldState.error ? "error" : "default"}
|
||||||
|
text={fieldState.error?.message}
|
||||||
|
onChange={field.onChange}
|
||||||
|
value={field.value}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -10,10 +10,10 @@ import {
|
|||||||
getFieldState,
|
getFieldState,
|
||||||
getFieldErrorMessage,
|
getFieldErrorMessage,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
} from ":/tests/reactHookFormUtils";
|
} from "./reactHookFormUtils";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Components/Forms/Reac-Hook-Form",
|
title: "Components/Forms/Examples/React-Hook-Form",
|
||||||
} as Meta;
|
} as Meta;
|
||||||
|
|
||||||
interface LoginStoryFormValues {
|
interface LoginStoryFormValues {
|
||||||
|
|||||||
@@ -1,64 +1,41 @@
|
|||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
import { Meta } from "@storybook/react";
|
import { Meta } from "@storybook/react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useForm, Controller, ControllerRenderProps } from "react-hook-form";
|
import { useForm, FormProvider } from "react-hook-form";
|
||||||
import * as Yup from "yup";
|
import * as Yup from "yup";
|
||||||
import { Input } from ":/components/Forms/Input";
|
import { Input } from ":/components/Forms/Input";
|
||||||
import { Button } from ":/components/Button";
|
import { Button } from ":/components/Button";
|
||||||
import { Select } from ":/components/Forms/Select";
|
|
||||||
import { CunninghamProvider } from ":/components/Provider";
|
import { CunninghamProvider } from ":/components/Provider";
|
||||||
import { Radio, RadioGroup } from ":/components/Forms/Radio";
|
import { Radio, RadioGroup } from ":/components/Forms/Radio";
|
||||||
|
import { RhfSelect } from ":/components/Forms/Select/stories-utils";
|
||||||
import {
|
import {
|
||||||
getFieldState,
|
getFieldState,
|
||||||
getFieldErrorMessage,
|
getFieldErrorMessage,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
} from ":/tests/reactHookFormUtils";
|
} from "./reactHookFormUtils";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Components/Forms/Reac-Hook-Form",
|
title: "Components/Forms/Examples/React-Hook-Form",
|
||||||
} as Meta;
|
} as Meta;
|
||||||
|
|
||||||
enum GenderEnum {
|
|
||||||
MALE = "male",
|
|
||||||
FEMALE = "female",
|
|
||||||
OTHER = "other",
|
|
||||||
}
|
|
||||||
enum CompetitionEnum {
|
|
||||||
ATHLETICS = "Athletics",
|
|
||||||
SWIMMING = "Swimming",
|
|
||||||
MARATHON = "Marathon",
|
|
||||||
}
|
|
||||||
enum RewardEnum {
|
|
||||||
BRONZE = "bronze",
|
|
||||||
SILVER = "silver",
|
|
||||||
GOLD = "gold",
|
|
||||||
FLOCON = "flocon",
|
|
||||||
OURSON = "ourson",
|
|
||||||
CHAMOIS = "chamois",
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SportsStoryFormValues {
|
interface SportsStoryFormValues {
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
gender: GenderEnum;
|
gender: string;
|
||||||
competition: CompetitionEnum;
|
competition: string;
|
||||||
rewards: RewardEnum[];
|
rewards: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const sportsSchema = Yup.object().shape({
|
const sportsSchema = Yup.object().shape({
|
||||||
firstName: Yup.string().required(),
|
firstName: Yup.string().required(),
|
||||||
lastName: Yup.string().required(),
|
lastName: Yup.string().required(),
|
||||||
gender: Yup.string<GenderEnum>().required(),
|
gender: Yup.string().required(),
|
||||||
competition: Yup.string()
|
competition: Yup.string().defined().required(),
|
||||||
.defined()
|
rewards: Yup.array().of(Yup.string().defined()).defined(),
|
||||||
.required()
|
|
||||||
.oneOf(Object.values(CompetitionEnum)),
|
|
||||||
rewards: Yup.array().of(Yup.string<RewardEnum>().defined()).defined(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const Sports = () => {
|
export const Sports = () => {
|
||||||
const { register, handleSubmit, formState, control } =
|
const methods = useForm<SportsStoryFormValues>({
|
||||||
useForm<SportsStoryFormValues>({
|
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
firstName: "",
|
firstName: "",
|
||||||
lastName: "",
|
lastName: "",
|
||||||
@@ -69,83 +46,9 @@ export const Sports = () => {
|
|||||||
resolver: yupResolver(sportsSchema),
|
resolver: yupResolver(sportsSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderCompetitionSelect = ({
|
|
||||||
field,
|
|
||||||
}: {
|
|
||||||
field: ControllerRenderProps<SportsStoryFormValues, "competition">;
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<Select
|
|
||||||
label="Competition"
|
|
||||||
options={[
|
|
||||||
{
|
|
||||||
label: "Athletics",
|
|
||||||
value: CompetitionEnum.ATHLETICS,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Swimming",
|
|
||||||
value: CompetitionEnum.SWIMMING,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Marathon",
|
|
||||||
value: CompetitionEnum.MARATHON,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
state={getFieldState("competition", formState)}
|
|
||||||
text={getFieldErrorMessage("competition", formState)}
|
|
||||||
onBlur={field.onBlur}
|
|
||||||
onChange={field.onChange}
|
|
||||||
value={field.value}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderRewardsMultiSelect = ({
|
|
||||||
field,
|
|
||||||
}: {
|
|
||||||
field: ControllerRenderProps<SportsStoryFormValues, "rewards">;
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<Select
|
|
||||||
label="Rewards"
|
|
||||||
options={[
|
|
||||||
{
|
|
||||||
label: "Bronze",
|
|
||||||
value: RewardEnum.BRONZE,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Silver",
|
|
||||||
value: RewardEnum.SILVER,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Gold",
|
|
||||||
value: RewardEnum.GOLD,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Flocon",
|
|
||||||
value: RewardEnum.FLOCON,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Ourson",
|
|
||||||
value: RewardEnum.OURSON,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Chamois",
|
|
||||||
value: RewardEnum.CHAMOIS,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
state={getFieldState("rewards", formState)}
|
|
||||||
text={getFieldErrorMessage("rewards", formState)}
|
|
||||||
onBlur={field.onBlur}
|
|
||||||
onChange={field.onChange}
|
|
||||||
value={field.value}
|
|
||||||
multi={true}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CunninghamProvider>
|
<CunninghamProvider>
|
||||||
|
<FormProvider {...methods}>
|
||||||
<form
|
<form
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@@ -153,7 +56,7 @@ export const Sports = () => {
|
|||||||
gap: "1rem",
|
gap: "1rem",
|
||||||
width: "400px",
|
width: "400px",
|
||||||
}}
|
}}
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
onSubmit={methods.handleSubmit(onSubmit)}
|
||||||
>
|
>
|
||||||
<h1
|
<h1
|
||||||
className="fs-h3 fw-bold clr-greyscale-900"
|
className="fs-h3 fw-bold clr-greyscale-900"
|
||||||
@@ -164,55 +67,87 @@ export const Sports = () => {
|
|||||||
<div style={{ display: "flex", gap: "1rem" }}>
|
<div style={{ display: "flex", gap: "1rem" }}>
|
||||||
<Input
|
<Input
|
||||||
label="First name"
|
label="First name"
|
||||||
state={getFieldState("firstName", formState)}
|
state={getFieldState("firstName", methods.formState)}
|
||||||
text={getFieldErrorMessage("firstName", formState)}
|
text={getFieldErrorMessage("firstName", methods.formState)}
|
||||||
{...register("firstName")}
|
{...methods.register("firstName")}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label="Last name"
|
label="Last name"
|
||||||
state={getFieldState("lastName", formState)}
|
state={getFieldState("lastName", methods.formState)}
|
||||||
text={getFieldErrorMessage("lastName", formState)}
|
text={getFieldErrorMessage("lastName", methods.formState)}
|
||||||
{...register("lastName")}
|
{...methods.register("lastName")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
state={getFieldState("gender", formState)}
|
state={getFieldState("gender", methods.formState)}
|
||||||
text={getFieldErrorMessage("gender", formState)}
|
text={getFieldErrorMessage("gender", methods.formState)}
|
||||||
style={{ display: "flex", flexDirection: "row", gap: "0.5rem" }}
|
style={{ display: "flex", flexDirection: "row", gap: "0.5rem" }}
|
||||||
>
|
>
|
||||||
<Radio
|
<Radio
|
||||||
label="Male"
|
label="Male"
|
||||||
fullWidth={true}
|
state={getFieldState("gender", methods.formState)}
|
||||||
state={getFieldState("gender", formState)}
|
{...methods.register("gender")}
|
||||||
{...register("gender")}
|
|
||||||
/>
|
/>
|
||||||
<Radio
|
<Radio
|
||||||
label="Female"
|
label="Female"
|
||||||
state={getFieldState("gender", formState)}
|
state={getFieldState("gender", methods.formState)}
|
||||||
{...register("gender")}
|
{...methods.register("gender")}
|
||||||
/>
|
/>
|
||||||
<Radio
|
<Radio
|
||||||
label="Other"
|
label="Other"
|
||||||
state={getFieldState("gender", formState)}
|
state={getFieldState("gender", methods.formState)}
|
||||||
{...register("gender")}
|
{...methods.register("gender")}
|
||||||
/>
|
/>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
<Controller
|
<RhfSelect
|
||||||
control={control}
|
|
||||||
name="competition"
|
name="competition"
|
||||||
render={renderCompetitionSelect}
|
label="Competition"
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
label: "Athletics",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Swimming",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Marathon",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
fullWidth={true}
|
||||||
/>
|
/>
|
||||||
<Controller
|
<RhfSelect
|
||||||
control={control}
|
|
||||||
name="rewards"
|
name="rewards"
|
||||||
render={renderRewardsMultiSelect}
|
label="Previous rewards"
|
||||||
|
multi={true}
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
label: "Bronze",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Silver",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Gold",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Flocon",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Ourson",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Chamois",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
fullWidth={true}
|
||||||
/>
|
/>
|
||||||
<Button fullWidth={true}>Apply</Button>
|
<Button fullWidth={true}>Apply</Button>
|
||||||
<Button fullWidth={true} color="secondary">
|
<Button fullWidth={true} color="secondary">
|
||||||
Need help ?
|
Need help ?
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
</FormProvider>
|
||||||
</CunninghamProvider>
|
</CunninghamProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ import { Meta } from "@storybook/react";
|
|||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import * as Yup from "yup";
|
import * as Yup from "yup";
|
||||||
import React, { useRef } from "react";
|
import React, { useRef } from "react";
|
||||||
import { Input } from ":/components/Forms/Input/index";
|
|
||||||
import { Button } from ":/components/Button";
|
|
||||||
import {
|
import {
|
||||||
getFieldState,
|
getFieldState,
|
||||||
getFieldErrorMessage,
|
getFieldErrorMessage,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
} from ":/tests/reactHookFormUtils";
|
} from ":/components/Forms/Examples/ReactHookForm/reactHookFormUtils";
|
||||||
|
import { Input } from ":/components/Forms/Input/index";
|
||||||
|
import { Button } from ":/components/Button";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Components/Forms/Input",
|
title: "Components/Forms/Input",
|
||||||
@@ -175,47 +175,6 @@ export const WithRef = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ReactHookForm = () => {
|
|
||||||
interface InputExampleFormValues {
|
|
||||||
email: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const inputExampleSchema = Yup.object().shape({
|
|
||||||
email: Yup.string().email().required(),
|
|
||||||
});
|
|
||||||
const { register, handleSubmit, formState } = useForm<InputExampleFormValues>(
|
|
||||||
{
|
|
||||||
defaultValues: {
|
|
||||||
email: "",
|
|
||||||
},
|
|
||||||
mode: "onChange",
|
|
||||||
reValidateMode: "onChange",
|
|
||||||
resolver: yupResolver(inputExampleSchema),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: "1rem",
|
|
||||||
width: "400px",
|
|
||||||
}}
|
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
label="Email address"
|
|
||||||
fullWidth={true}
|
|
||||||
state={getFieldState("email", formState)}
|
|
||||||
text={getFieldErrorMessage("email", formState)}
|
|
||||||
{...register("email")}
|
|
||||||
/>
|
|
||||||
<Button fullWidth={true}>Log-in</Button>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FormExample = () => {
|
export const FormExample = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -268,3 +227,44 @@ export const FormExample = () => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ReactHookForm = () => {
|
||||||
|
interface InputExampleFormValues {
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputExampleSchema = Yup.object().shape({
|
||||||
|
email: Yup.string().email().required(),
|
||||||
|
});
|
||||||
|
const { register, handleSubmit, formState } = useForm<InputExampleFormValues>(
|
||||||
|
{
|
||||||
|
defaultValues: {
|
||||||
|
email: "",
|
||||||
|
},
|
||||||
|
mode: "onChange",
|
||||||
|
reValidateMode: "onChange",
|
||||||
|
resolver: yupResolver(inputExampleSchema),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "1rem",
|
||||||
|
width: "400px",
|
||||||
|
}}
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
label="Email address"
|
||||||
|
fullWidth={true}
|
||||||
|
state={getFieldState("email", formState)}
|
||||||
|
text={getFieldErrorMessage("email", formState)}
|
||||||
|
{...register("email")}
|
||||||
|
/>
|
||||||
|
<Button fullWidth={true}>Log-in</Button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ import { useForm } from "react-hook-form";
|
|||||||
import * as Yup from "yup";
|
import * as Yup from "yup";
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
import { Meta, StoryFn } from "@storybook/react";
|
import { Meta, StoryFn } from "@storybook/react";
|
||||||
import { Radio, RadioGroup } from ":/components/Forms/Radio/index";
|
|
||||||
import { Button } from ":/components/Button";
|
|
||||||
import {
|
import {
|
||||||
getFieldState,
|
getFieldState,
|
||||||
getFieldErrorMessage,
|
getFieldErrorMessage,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
} from ":/tests/reactHookFormUtils";
|
} from ":/components/Forms/Examples/ReactHookForm/reactHookFormUtils";
|
||||||
|
import { Radio, RadioGroup } from ":/components/Forms/Radio/index";
|
||||||
|
import { Button } from ":/components/Button";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Components/Forms/Radio",
|
title: "Components/Forms/Radio",
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
import { Meta, StoryFn } from "@storybook/react";
|
import { Meta, StoryFn } from "@storybook/react";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useForm, Controller, ControllerRenderProps } from "react-hook-form";
|
import { useForm, FormProvider } from "react-hook-form";
|
||||||
import * as Yup from "yup";
|
import * as Yup from "yup";
|
||||||
import { faker } from "@faker-js/faker";
|
import { faker } from "@faker-js/faker";
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
|
import { onSubmit } from ":/components/Forms/Examples/ReactHookForm/reactHookFormUtils";
|
||||||
import { Select } from ":/components/Forms/Select";
|
import { Select } from ":/components/Forms/Select";
|
||||||
import { Button } from ":/components/Button";
|
import { Button } from ":/components/Button";
|
||||||
import { CunninghamProvider } from ":/components/Provider";
|
import { CunninghamProvider } from ":/components/Provider";
|
||||||
import {
|
import { RhfSelect } from ":/components/Forms/Select/stories-utils";
|
||||||
getFieldState,
|
|
||||||
getFieldErrorMessage,
|
|
||||||
onSubmit,
|
|
||||||
} from ":/tests/reactHookFormUtils";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Components/Forms/Select/Mono",
|
title: "Components/Forms/Select/Mono",
|
||||||
@@ -164,87 +161,6 @@ export const SearchableControlled = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ReactHookForm = () => {
|
|
||||||
enum CitiesOptionEnum {
|
|
||||||
NONE = "",
|
|
||||||
DIJON = "dijon",
|
|
||||||
PARIS = "paris",
|
|
||||||
TOKYO = "tokyo",
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SelectExampleFormValues {
|
|
||||||
joTown: CitiesOptionEnum;
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectExampleSchema = Yup.object().shape({
|
|
||||||
joTown: Yup.string()
|
|
||||||
.required()
|
|
||||||
.oneOf([CitiesOptionEnum.PARIS], "That's not the right town!"),
|
|
||||||
});
|
|
||||||
|
|
||||||
const { handleSubmit, formState, control } = useForm<SelectExampleFormValues>(
|
|
||||||
{
|
|
||||||
defaultValues: {
|
|
||||||
joTown: CitiesOptionEnum.NONE,
|
|
||||||
},
|
|
||||||
mode: "onChange",
|
|
||||||
reValidateMode: "onChange",
|
|
||||||
resolver: yupResolver(selectExampleSchema),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderSelect = ({
|
|
||||||
field,
|
|
||||||
}: {
|
|
||||||
field: ControllerRenderProps<SelectExampleFormValues, "joTown">;
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div>Where will the 2024 Olympics take place?</div>
|
|
||||||
<Select
|
|
||||||
label="Select a city"
|
|
||||||
options={[
|
|
||||||
{
|
|
||||||
label: "Dijon",
|
|
||||||
value: CitiesOptionEnum.DIJON,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Paris",
|
|
||||||
value: CitiesOptionEnum.PARIS,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Tokyo",
|
|
||||||
value: CitiesOptionEnum.TOKYO,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
state={getFieldState("joTown", formState)}
|
|
||||||
text={getFieldErrorMessage("joTown", formState)}
|
|
||||||
onBlur={field.onBlur}
|
|
||||||
onChange={field.onChange}
|
|
||||||
value={field.value}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CunninghamProvider>
|
|
||||||
<form
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: "1rem",
|
|
||||||
width: "400px",
|
|
||||||
}}
|
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
|
||||||
>
|
|
||||||
<Controller control={control} name="joTown" render={renderSelect} />
|
|
||||||
<Button fullWidth={true}>Submit</Button>
|
|
||||||
</form>
|
|
||||||
</CunninghamProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FullWidth = {
|
export const FullWidth = {
|
||||||
render: Template,
|
render: Template,
|
||||||
|
|
||||||
@@ -405,3 +321,69 @@ export const FormExample = () => {
|
|||||||
</CunninghamProvider>
|
</CunninghamProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ReactHookForm = () => {
|
||||||
|
enum CitiesOptionEnum {
|
||||||
|
NONE = "",
|
||||||
|
DIJON = "dijon",
|
||||||
|
PARIS = "paris",
|
||||||
|
TOKYO = "tokyo",
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SelectExampleFormValues {
|
||||||
|
joTown: CitiesOptionEnum;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectExampleSchema = Yup.object().shape({
|
||||||
|
joTown: Yup.string()
|
||||||
|
.required()
|
||||||
|
.oneOf([CitiesOptionEnum.PARIS], "That's not the right town!"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const methods = useForm<SelectExampleFormValues>({
|
||||||
|
defaultValues: {
|
||||||
|
joTown: CitiesOptionEnum.NONE,
|
||||||
|
},
|
||||||
|
mode: "onChange",
|
||||||
|
reValidateMode: "onChange",
|
||||||
|
resolver: yupResolver(selectExampleSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CunninghamProvider>
|
||||||
|
<FormProvider {...methods}>
|
||||||
|
<form
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "1rem",
|
||||||
|
width: "400px",
|
||||||
|
}}
|
||||||
|
onSubmit={methods.handleSubmit(onSubmit)}
|
||||||
|
>
|
||||||
|
<div>Where will the 2024 Olympics take place?</div>
|
||||||
|
<RhfSelect
|
||||||
|
name="joTown"
|
||||||
|
label="Select a city"
|
||||||
|
fullWidth={true}
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
label: "Dijon",
|
||||||
|
value: CitiesOptionEnum.DIJON,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Paris",
|
||||||
|
value: CitiesOptionEnum.PARIS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Tokyo",
|
||||||
|
value: CitiesOptionEnum.TOKYO,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Button fullWidth={true}>Submit</Button>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
</CunninghamProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useForm, Controller, ControllerRenderProps } from "react-hook-form";
|
import { useForm, FormProvider } from "react-hook-form";
|
||||||
import * as Yup from "yup";
|
import * as Yup from "yup";
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
import { Meta, StoryFn } from "@storybook/react";
|
import { Meta, StoryFn } from "@storybook/react";
|
||||||
import { faker } from "@faker-js/faker";
|
import { faker } from "@faker-js/faker";
|
||||||
|
import { onSubmit } from ":/components/Forms/Examples/ReactHookForm/reactHookFormUtils";
|
||||||
import { Select } from ":/components/Forms/Select";
|
import { Select } from ":/components/Forms/Select";
|
||||||
import { CunninghamProvider } from ":/components/Provider";
|
import { CunninghamProvider } from ":/components/Provider";
|
||||||
import { Button } from ":/components/Button";
|
import { Button } from ":/components/Button";
|
||||||
import {
|
import { RhfSelect } from ":/components/Forms/Select/stories-utils";
|
||||||
getFieldState,
|
|
||||||
getFieldErrorMessage,
|
|
||||||
onSubmit,
|
|
||||||
} from ":/tests/reactHookFormUtils";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Components/Forms/Select/Multi",
|
title: "Components/Forms/Select/Multi",
|
||||||
@@ -216,103 +213,6 @@ export const Error = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ReactHookForm = () => {
|
|
||||||
enum CitiesOptionEnum {
|
|
||||||
NONE = "",
|
|
||||||
DIJON = "dijon",
|
|
||||||
PARIS = "paris",
|
|
||||||
TOKYO = "tokyo",
|
|
||||||
VANNES = "vannes",
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SelectExampleFormValues {
|
|
||||||
capitalCity: CitiesOptionEnum[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectExampleSchema = Yup.object().shape({
|
|
||||||
capitalCity: Yup.array()
|
|
||||||
.required()
|
|
||||||
.test({
|
|
||||||
test: (cityList) =>
|
|
||||||
cityList.every((city) =>
|
|
||||||
[CitiesOptionEnum.PARIS, CitiesOptionEnum.TOKYO].includes(city),
|
|
||||||
),
|
|
||||||
message: "Wrong answer!",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const { handleSubmit, formState, control } = useForm<SelectExampleFormValues>(
|
|
||||||
{
|
|
||||||
defaultValues: {
|
|
||||||
capitalCity: [],
|
|
||||||
},
|
|
||||||
mode: "onChange",
|
|
||||||
reValidateMode: "onChange",
|
|
||||||
resolver: yupResolver(selectExampleSchema),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderSelect = ({
|
|
||||||
field,
|
|
||||||
}: {
|
|
||||||
field: ControllerRenderProps<SelectExampleFormValues, "capitalCity">;
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div>Which are the capital of the country ?</div>
|
|
||||||
<Select
|
|
||||||
label="Select a city"
|
|
||||||
multi={true}
|
|
||||||
options={[
|
|
||||||
{
|
|
||||||
label: "Dijon",
|
|
||||||
value: CitiesOptionEnum.DIJON,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Paris",
|
|
||||||
value: CitiesOptionEnum.PARIS,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Tokyo",
|
|
||||||
value: CitiesOptionEnum.TOKYO,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Vannes",
|
|
||||||
value: CitiesOptionEnum.VANNES,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
state={getFieldState("capitalCity", formState)}
|
|
||||||
text={getFieldErrorMessage("capitalCity", formState)}
|
|
||||||
onBlur={field.onBlur}
|
|
||||||
onChange={field.onChange}
|
|
||||||
value={field.value}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CunninghamProvider>
|
|
||||||
<form
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: "1rem",
|
|
||||||
width: "400px",
|
|
||||||
}}
|
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
|
||||||
>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="capitalCity"
|
|
||||||
render={renderSelect}
|
|
||||||
/>
|
|
||||||
<Button fullWidth={true}>Submit</Button>
|
|
||||||
</form>
|
|
||||||
</CunninghamProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FormExample = () => {
|
export const FormExample = () => {
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -409,3 +309,81 @@ export const FormExample = () => {
|
|||||||
</CunninghamProvider>
|
</CunninghamProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ReactHookForm = () => {
|
||||||
|
enum CitiesOptionEnum {
|
||||||
|
NONE = "",
|
||||||
|
DIJON = "dijon",
|
||||||
|
PARIS = "paris",
|
||||||
|
TOKYO = "tokyo",
|
||||||
|
VANNES = "vannes",
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SelectExampleFormValues {
|
||||||
|
capitalCity: CitiesOptionEnum[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectExampleSchema = Yup.object().shape({
|
||||||
|
capitalCity: Yup.array()
|
||||||
|
.required()
|
||||||
|
.test({
|
||||||
|
test: (cityList) =>
|
||||||
|
cityList.every((city) =>
|
||||||
|
[CitiesOptionEnum.PARIS, CitiesOptionEnum.TOKYO].includes(city),
|
||||||
|
),
|
||||||
|
message: "Wrong answer!",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const methods = useForm<SelectExampleFormValues>({
|
||||||
|
defaultValues: {
|
||||||
|
capitalCity: [],
|
||||||
|
},
|
||||||
|
mode: "onChange",
|
||||||
|
reValidateMode: "onChange",
|
||||||
|
resolver: yupResolver(selectExampleSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CunninghamProvider>
|
||||||
|
<FormProvider {...methods}>
|
||||||
|
<form
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "1rem",
|
||||||
|
width: "400px",
|
||||||
|
}}
|
||||||
|
onSubmit={methods.handleSubmit(onSubmit)}
|
||||||
|
>
|
||||||
|
<div>Which cities are capital of countries ?</div>
|
||||||
|
<RhfSelect
|
||||||
|
name="capitalCity"
|
||||||
|
label="Select a city"
|
||||||
|
multi={true}
|
||||||
|
fullWidth={true}
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
label: "Dijon",
|
||||||
|
value: CitiesOptionEnum.DIJON,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Paris",
|
||||||
|
value: CitiesOptionEnum.PARIS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Tokyo",
|
||||||
|
value: CitiesOptionEnum.TOKYO,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Vannes",
|
||||||
|
value: CitiesOptionEnum.VANNES,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Button fullWidth={true}>Submit</Button>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
</CunninghamProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
26
packages/react/src/components/Forms/Select/stories-utils.tsx
Normal file
26
packages/react/src/components/Forms/Select/stories-utils.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
|
import React from "react";
|
||||||
|
import { Select } from ":/components/Forms/Select/index";
|
||||||
|
import { SelectProps } from ":/components/Forms/Select/mono";
|
||||||
|
|
||||||
|
export const RhfSelect = (props: SelectProps & { name: string }) => {
|
||||||
|
const { control } = useFormContext();
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={props.name}
|
||||||
|
render={({ field, fieldState }) => {
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
{...props}
|
||||||
|
state={fieldState.error ? "error" : "default"}
|
||||||
|
text={fieldState.error?.message}
|
||||||
|
onBlur={field.onBlur}
|
||||||
|
onChange={field.onChange}
|
||||||
|
value={field.value}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -3,13 +3,13 @@ import { Meta } from "@storybook/react";
|
|||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import * as Yup from "yup";
|
import * as Yup from "yup";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Switch } from ":/components/Forms/Switch/index";
|
|
||||||
import { Button } from ":/components/Button";
|
|
||||||
import {
|
import {
|
||||||
getFieldState,
|
getFieldState,
|
||||||
getFieldErrorMessage,
|
getFieldErrorMessage,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
} from ":/tests/reactHookFormUtils";
|
} from ":/components/Forms/Examples/ReactHookForm/reactHookFormUtils";
|
||||||
|
import { Switch } from ":/components/Forms/Switch/index";
|
||||||
|
import { Button } from ":/components/Button";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Components/Forms/Switch",
|
title: "Components/Forms/Switch",
|
||||||
@@ -130,49 +130,6 @@ export const Controlled = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ReactHookForm = () => {
|
|
||||||
interface SwitchExampleFormValues {
|
|
||||||
terms: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const switchExampleSchema = Yup.object().shape({
|
|
||||||
terms: Yup.boolean()
|
|
||||||
.required()
|
|
||||||
.oneOf([true], "You have to accept the terms of use"),
|
|
||||||
});
|
|
||||||
|
|
||||||
const { register, handleSubmit, formState } =
|
|
||||||
useForm<SwitchExampleFormValues>({
|
|
||||||
defaultValues: {
|
|
||||||
terms: false,
|
|
||||||
},
|
|
||||||
mode: "onChange",
|
|
||||||
reValidateMode: "onChange",
|
|
||||||
resolver: yupResolver(switchExampleSchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: "1rem",
|
|
||||||
width: "400px",
|
|
||||||
}}
|
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
|
||||||
>
|
|
||||||
<Switch
|
|
||||||
label="I accept the terms of use"
|
|
||||||
fullWidth
|
|
||||||
state={getFieldState("terms", formState)}
|
|
||||||
text={getFieldErrorMessage("terms", formState)}
|
|
||||||
{...register("terms")}
|
|
||||||
/>
|
|
||||||
<Button fullWidth={true}>Log-in</Button>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FormExample = {
|
export const FormExample = {
|
||||||
render: () => {
|
render: () => {
|
||||||
return (
|
return (
|
||||||
@@ -240,3 +197,46 @@ export const FormExampleRight = {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ReactHookForm = () => {
|
||||||
|
interface SwitchExampleFormValues {
|
||||||
|
terms: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const switchExampleSchema = Yup.object().shape({
|
||||||
|
terms: Yup.boolean()
|
||||||
|
.required()
|
||||||
|
.oneOf([true], "You have to accept the terms of use"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { register, handleSubmit, formState } =
|
||||||
|
useForm<SwitchExampleFormValues>({
|
||||||
|
defaultValues: {
|
||||||
|
terms: false,
|
||||||
|
},
|
||||||
|
mode: "onChange",
|
||||||
|
reValidateMode: "onChange",
|
||||||
|
resolver: yupResolver(switchExampleSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "1rem",
|
||||||
|
width: "400px",
|
||||||
|
}}
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
>
|
||||||
|
<Switch
|
||||||
|
label="I accept the terms of use"
|
||||||
|
fullWidth
|
||||||
|
state={getFieldState("terms", formState)}
|
||||||
|
text={getFieldErrorMessage("terms", formState)}
|
||||||
|
{...register("terms")}
|
||||||
|
/>
|
||||||
|
<Button fullWidth={true}>Log-in</Button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user