Skip to content

Commit cc8d88a

Browse files
authored
Merge pull request #35 from buildo/3147479-implement_formformsectionformrow_components
2 parents 862e7a9 + 3b6d98d commit cc8d88a

File tree

10 files changed

+348
-2
lines changed

10 files changed

+348
-2
lines changed

Diff for: src/ContentBlock/ContentBlock.css.ts

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { strictRecipe } from "../util/strictRecipe";
2+
3+
export const contentBlockRecipe = strictRecipe({
4+
variants: {
5+
alignSelf: {
6+
left: {},
7+
center: { margin: "0 auto" },
8+
},
9+
},
10+
});

Diff for: src/ContentBlock/ContentBlock.tsx

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { RequiredResponsiveValue } from "../internal/sprinkles.css";
2+
import { Children } from "..";
3+
import { Box } from "../internal";
4+
import { contentBlockRecipe } from "./ContentBlock.css";
5+
6+
type Props = {
7+
maxWidth: RequiredResponsiveValue<700 | 1440>;
8+
alignSelf?: "center" | "left";
9+
children: Children;
10+
};
11+
12+
export function ContentBlock({ maxWidth, alignSelf = "left", children }: Props) {
13+
return (
14+
<Box className={contentBlockRecipe({ alignSelf })} width="full" maxWidth={maxWidth}>
15+
{children}
16+
</Box>
17+
);
18+
}

Diff for: src/Form/Form.tsx

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { ComponentProps, FunctionComponent } from "react";
2+
import {
3+
ActionsProps,
4+
Body,
5+
ButtonProps,
6+
Children,
7+
ContentBlock,
8+
Display,
9+
LocalizedString,
10+
TextChildren,
11+
} from "..";
12+
13+
import { BentoSprinkles, Box, Stack } from "../internal";
14+
15+
type Props = {
16+
children: Children;
17+
title?: LocalizedString;
18+
description?: TextChildren;
19+
submitButton?: Omit<ButtonProps, "kind">;
20+
secondaryButton?: Omit<ButtonProps, "kind">;
21+
};
22+
23+
export type FormConfig = {
24+
headerTitleSize: ComponentProps<typeof Display>["size"];
25+
headerDescriptionSize: ComponentProps<typeof Body>["size"];
26+
formSpacing: BentoSprinkles["gap"];
27+
headerSpacing: BentoSprinkles["gap"];
28+
actionsSize: ActionsProps["size"];
29+
};
30+
31+
export function createForm(Actions: FunctionComponent<ActionsProps>, config: FormConfig) {
32+
return function Form({ title, description, children, submitButton, secondaryButton }: Props) {
33+
return (
34+
<Box as="form" onSubmit={(e) => e.preventDefault()}>
35+
<ContentBlock maxWidth={700}>
36+
<Stack space={config.formSpacing}>
37+
{(title || description) && (
38+
<Stack space={config.headerSpacing}>
39+
{title && <Display size={config.headerTitleSize}>{title}</Display>}
40+
{description && <Body size={config.headerDescriptionSize}>{description}</Body>}
41+
</Stack>
42+
)}
43+
{children}
44+
{(submitButton || secondaryButton) && (
45+
<Actions
46+
size={config.actionsSize}
47+
primaryAction={submitButton}
48+
secondaryAction={secondaryButton}
49+
/>
50+
)}
51+
</Stack>
52+
</ContentBlock>
53+
</Box>
54+
);
55+
};
56+
}

Diff for: src/Form/FormRow.tsx

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { Children } from "..";
2+
import { BentoSprinkles, Columns } from "../internal";
3+
4+
type Props = {
5+
children: Children;
6+
};
7+
8+
export type FormRowConfig = {
9+
rowSpacing: BentoSprinkles["gap"];
10+
};
11+
12+
export function createFormRow(config: FormRowConfig) {
13+
return function FormRow({ children }: Props) {
14+
return <Columns space={config.rowSpacing} collapseBelow="tablet" children={children} />;
15+
};
16+
}

Diff for: src/Form/FormSection.tsx

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { ComponentProps } from "react";
2+
import { Body, Children, LocalizedString, TextChildren, Title } from "..";
3+
import { BentoSprinkles, Stack } from "../internal";
4+
5+
export type Props = {
6+
title?: LocalizedString;
7+
description?: TextChildren;
8+
children: Children;
9+
};
10+
11+
export type FormSectionConfig = {
12+
sectionTitleSize: ComponentProps<typeof Title>["size"];
13+
sectionDescriptionSize: ComponentProps<typeof Body>["size"];
14+
sectionHeaderSpacing: BentoSprinkles["gap"];
15+
sectionSpacing: BentoSprinkles["gap"];
16+
};
17+
18+
export function createFormSection(config: FormSectionConfig) {
19+
return function FormSection({ title, description, children }: Props) {
20+
return (
21+
<Stack space={config.sectionSpacing} as="section">
22+
{(title || description) && (
23+
<Stack space={config.sectionHeaderSpacing}>
24+
{title && <Title size={config.sectionTitleSize}>{title}</Title>}
25+
{description && <Body size={config.sectionDescriptionSize}>{description}</Body>}
26+
</Stack>
27+
)}
28+
{children}
29+
</Stack>
30+
);
31+
};
32+
}

Diff for: src/Form/createFormLayoutComponents.tsx

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { FunctionComponent } from "react";
2+
import { ActionsProps } from "src";
3+
import { createForm, FormConfig } from "./Form";
4+
import { createFormRow, FormRowConfig } from "./FormRow";
5+
import { createFormSection, FormSectionConfig } from "./FormSection";
6+
7+
type FormLayoutConfig = {
8+
form: FormConfig;
9+
section: FormSectionConfig;
10+
row: FormRowConfig;
11+
};
12+
13+
export function createFormLayoutComponents(
14+
Actions: FunctionComponent<ActionsProps>,
15+
config: FormLayoutConfig = {
16+
form: {
17+
headerTitleSize: "small",
18+
headerDescriptionSize: "medium",
19+
formSpacing: 40,
20+
headerSpacing: 16,
21+
actionsSize: "large",
22+
},
23+
section: {
24+
sectionTitleSize: "large",
25+
sectionDescriptionSize: "medium",
26+
sectionHeaderSpacing: 8,
27+
sectionSpacing: 24,
28+
},
29+
row: {
30+
rowSpacing: 16,
31+
},
32+
}
33+
) {
34+
const Form = createForm(Actions, config.form);
35+
const FormSection = createFormSection(config.section);
36+
const FormRow = createFormRow(config.row);
37+
38+
return { Form, FormSection, FormRow };
39+
}

Diff for: src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ export * from "./Breadcrumb/createBreadcrumb";
77
export * from "./Button/createButton";
88
export * from "./Card/createCard";
99
export * from "./Chip/createChip";
10+
export * from "./ContentBlock/ContentBlock";
1011
export * from "./Disclosure/createDisclosure";
1112
export * from "./DisclosureGroup/createDisclosureGroup";
1213
export * from "./Divider/Divider";
1314
export * from "./Field/createFormFields";
15+
export * from "./Form/createFormLayoutComponents";
1416
export * from "./IconButton/IconButton";
1517
export * from "./Icons";
1618
export * from "./Illustrations/IllustrationProps";

Diff for: src/util/atoms.ts

+4
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ export const responsiveProperties = {
6464
paddingRight: vars.space,
6565
gap: vars.space,
6666
textAlign: ["left", "center", "right", "justify"],
67+
maxWidth: {
68+
700: "700px",
69+
1440: "1440px",
70+
},
6771
} as const;
6872

6973
const color = {

Diff for: stories/Components/Form.stories.tsx

+167
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import { action } from "@storybook/addon-actions";
2+
import { ComponentProps, useState } from "react";
3+
import { SelectFieldProps } from "../../src/SelectField/createSelectField";
4+
import {
5+
Form,
6+
FormSection,
7+
FormRow,
8+
TextField,
9+
NumberField,
10+
SelectField,
11+
RadioGroupField,
12+
} from "..";
13+
import { createComponentStories, formatMessage } from "../util";
14+
import { Omit } from "../../src/util/Omit";
15+
16+
const { defaultExport, createStory } = createComponentStories({
17+
component: Form,
18+
args: {},
19+
argTypes: {},
20+
});
21+
22+
export default defaultExport;
23+
24+
const ExampleTextField = (
25+
props: Omit<
26+
ComponentProps<typeof TextField>,
27+
"placeholder" | "value" | "onChange" | "name" | "onBlur"
28+
>
29+
) => {
30+
const [value, onChange] = useState("");
31+
return (
32+
<TextField
33+
placeholder={formatMessage("Insert a value")}
34+
value={value}
35+
onChange={onChange}
36+
onBlur={() => {}}
37+
name="textField"
38+
hint={formatMessage("Some useful advice on how to fill this field")}
39+
{...props}
40+
/>
41+
);
42+
};
43+
44+
const ExampleNumberField = (
45+
props: Omit<
46+
ComponentProps<typeof NumberField>,
47+
"placeholder" | "value" | "onChange" | "name" | "onBlur"
48+
>
49+
) => {
50+
const [value, onChange] = useState<number | undefined>(undefined);
51+
return (
52+
<NumberField
53+
placeholder={formatMessage("Insert a value")}
54+
value={value}
55+
onChange={onChange}
56+
onBlur={() => {}}
57+
name="numberField"
58+
{...props}
59+
/>
60+
);
61+
};
62+
63+
const ExampleSelectField = <A extends {}>(
64+
props: Omit<
65+
SelectFieldProps<A, false>,
66+
"placeholder" | "value" | "onChange" | "name" | "onBlur" | "multiValueMessage"
67+
>
68+
) => {
69+
const [value, onChange] = useState<A | undefined>(undefined);
70+
return (
71+
<SelectField
72+
placeholder={formatMessage("Select a value")}
73+
value={value}
74+
onChange={onChange}
75+
name="selectField"
76+
onBlur={() => {}}
77+
isMulti={false}
78+
multiValueMessage={() => formatMessage("")}
79+
{...props}
80+
/>
81+
);
82+
};
83+
84+
const ExampleRadioGroupField = (
85+
props: Omit<ComponentProps<typeof RadioGroupField>, "value" | "onChange" | "name" | "onBlur">
86+
) => {
87+
const [value, onChange] = useState<string | number | boolean | undefined>(undefined);
88+
return (
89+
<RadioGroupField
90+
value={value}
91+
onChange={onChange}
92+
name="radioGroupField"
93+
onBlur={() => {}}
94+
{...props}
95+
/>
96+
);
97+
};
98+
99+
export const multipleSections = createStory({
100+
title: formatMessage("Sign-up"),
101+
description: formatMessage("We will ask you some data in order to sign you up"),
102+
submitButton: {
103+
onPress: action("Submit"),
104+
label: formatMessage("Sign up"),
105+
},
106+
secondaryButton: {
107+
onPress: action("Cancel"),
108+
label: formatMessage("Never mind"),
109+
},
110+
children: [
111+
<FormSection title={formatMessage("Personal information")}>
112+
<FormRow>
113+
<ExampleTextField label={formatMessage("First name")} />
114+
<ExampleTextField label={formatMessage("Last name")} />
115+
</FormRow>
116+
</FormSection>,
117+
<FormSection
118+
title={formatMessage("Address")}
119+
description={formatMessage("We need this data for invoicing purposes")}
120+
>
121+
<FormRow>
122+
<ExampleTextField label={formatMessage("Street")} />
123+
<ExampleTextField label={formatMessage("Number")} />
124+
</FormRow>
125+
<FormRow>
126+
<ExampleTextField label={formatMessage("City")} />
127+
</FormRow>
128+
<FormRow>
129+
<ExampleTextField label={formatMessage("Country")} />
130+
</FormRow>
131+
</FormSection>,
132+
<FormSection title={formatMessage("Tell us more about you")}>
133+
<FormRow>
134+
<ExampleNumberField
135+
label={formatMessage("Average income")}
136+
kind="currency"
137+
currency="EUR"
138+
/>
139+
<ExampleNumberField
140+
label={formatMessage("% of income spent on candies")}
141+
kind="percentage"
142+
/>
143+
</FormRow>
144+
<FormRow>
145+
<ExampleSelectField
146+
size="medium"
147+
label={formatMessage("Select your gender")}
148+
options={[
149+
{ label: formatMessage("Male"), value: "M", kind: "single-line" },
150+
{ label: formatMessage("Female"), value: "F", kind: "single-line" },
151+
{ label: formatMessage("Other"), value: "O", kind: "single-line" },
152+
]}
153+
/>
154+
</FormRow>
155+
<FormRow>
156+
<ExampleRadioGroupField
157+
label={formatMessage("What's your main income source?")}
158+
options={[
159+
{ label: formatMessage("Working"), value: "W" },
160+
{ label: formatMessage("Inheritance"), value: "I" },
161+
{ label: formatMessage("Other"), value: "O" },
162+
]}
163+
/>
164+
</FormRow>
165+
</FormSection>,
166+
],
167+
});

Diff for: stories/index.tsx

+4-2
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,20 @@ import {
2020
createTabs,
2121
createAreaLoader,
2222
createAvatar,
23+
createFormLayoutComponents,
2324
} from "../src";
2425
import { sprinkles } from "./sprinkles.css";
2526

2627
export * from "../src";
2728
export const Box = createBentoBox(sprinkles);
2829
export const { Stack, Column, Columns, Inline, Inset } = createLayoutComponents(Box);
30+
export const Button = createButton({});
31+
export const Actions = createActions(Button);
32+
export const { Form, FormSection, FormRow } = createFormLayoutComponents(Actions);
2933
export const { CheckboxField, NumberField, RadioGroupField, SelectField, TextField } =
3034
createFormFields();
31-
export const Button = createButton({});
3235
export const Banner = createBanner({});
3336
export const { Toast, ToastProvider } = createToast(Button, {});
34-
export const Actions = createActions(Button);
3537
export const Card = createCard<24 | 32 | 40>({});
3638
export const Link = createLink();
3739
export const Breadcrumb = createBreadcrumb(Link);

0 commit comments

Comments
 (0)