Introduce react hook form macaw bindings (#469)

* Add components and update the configuration

* Export components to be used in apps
This commit is contained in:
Krzysztof Wolski 2023-05-22 17:47:33 +02:00 committed by GitHub
parent ce8d9deb81
commit 8a339fc31b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 5398 additions and 529 deletions

View file

@ -0,0 +1,5 @@
---
"@saleor/react-hook-form-macaw": patch
---
Introduction of the library integrating Macaw with the React Hook Forms.

View file

@ -0,0 +1,15 @@
{
"root": true,
"extends": ["saleor"],
"overrides": [
{
"files": ["*stories.tsx"],
"rules": {
// Stories require default export for storybook
"import/no-default-export": "off",
// Story wrapper is an exception to the rule
"react-hooks/rules-of-hooks": "off"
}
}
]
}

View file

@ -0,0 +1,2 @@
# Ignore storybook build artifacts
storybook-static

View file

@ -0,0 +1,29 @@
import { mergeConfig } from "vite";
export default {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions",
],
framework: {
name: "@storybook/react-vite",
options: {},
},
staticDirs: [
{
from: "../public",
to: "/assets",
},
"./public",
],
features: {
storyStoreV7: true,
},
async viteFinal(config) {
return mergeConfig(config, {
plugins: [require("@vanilla-extract/vite-plugin").vanillaExtractPlugin()],
});
},
};

View file

@ -0,0 +1,47 @@
import "@saleor/macaw-ui/next/style";
import "./styles.css";
import React from "react";
import { Preview } from "@storybook/react";
import { Box, DefaultTheme, ThemeProvider, useTheme } from "@saleor/macaw-ui/next";
const ThemeSwitcher = ({ children, theme }) => {
const { setTheme } = useTheme();
React.useEffect(() => {
setTheme(theme);
}, [theme]);
return (
<Box display="flex" justifyContent="center" __backgroundColor="white">
{children}
</Box>
);
};
const themes: DefaultTheme[] = ["defaultLight", "defaultDark"];
const preview: Preview = {
globalTypes: {
theme: {
name: "Theme",
description: "Global theme for components",
defaultValue: themes[0],
toolbar: {
icon: "mirror",
items: themes,
dynamicTitle: true,
},
},
},
decorators: [
(Story, context) => (
<ThemeProvider defaultTheme={context.globals.theme}>
<ThemeSwitcher theme={context.globals.theme}>
<Story />
</ThemeSwitcher>
</ThemeProvider>
),
],
};
export default preview;

View file

@ -0,0 +1,4 @@
.sbdocs-wrapper {
width: 100%;
}

View file

@ -0,0 +1 @@
# @saleor/react-hook-form-macaw

View file

@ -0,0 +1 @@
# react-hook-form-macaw

View file

@ -0,0 +1,3 @@
export * from "./src/components/Input";
export * from "./src/components/Combobox";
export * from "./src/components/Multiselect";

View file

@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

View file

@ -0,0 +1,38 @@
{
"name": "@saleor/react-hook-form-macaw",
"version": "0.0.1",
"devDependencies": {
"@babel/core": "^7.21.8",
"@saleor/macaw-ui": "0.8.0-pre.84",
"@storybook/addon-actions": "^7.0.12",
"@storybook/addon-essentials": "^7.0.12",
"@storybook/addon-interactions": "^7.0.12",
"@storybook/addon-links": "^7.0.12",
"@storybook/blocks": "^7.0.12",
"@storybook/react": "^7.0.12",
"@storybook/react-vite": "^7.0.12",
"@storybook/testing-library": "^0.0.14-next.2",
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"@vanilla-extract/vite-plugin": "^3.8.1",
"eslint-config-saleor": "workspace:*",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.43.9",
"storybook": "^7.0.12",
"typescript": "^5.0.4",
"vite": "^4.3.6",
"webpack": "^5.82.1"
},
"peerDependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"scripts": {
"lint:fix": "eslint --fix .",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"start-storybook-build": "pnpm dlx http-server ./storybook-static"
},
"main": "index.ts"
}

View file

@ -0,0 +1,70 @@
import { useEffect } from "react";
import { Meta, StoryObj } from "@storybook/react";
import { Combobox } from "./Combobox";
import { useForm } from "react-hook-form";
import { action } from "@storybook/addon-actions";
const meta: Meta<typeof Combobox> = {
title: "Components / Combobox",
component: Combobox,
};
export default meta;
type Story = StoryObj<typeof Combobox>;
const ComboboxTemplate: Story = {
render: (args) => {
const { control, watch, setError } = useForm();
const name = "comboboxField";
if (args.error) {
setError(name, { message: "Error message" });
}
useEffect(() => {
const subscription = watch((value) => action("[React Hooks Form] Form value changed")(value));
return () => subscription.unsubscribe();
}, [watch]);
return (
<Combobox
{...args}
control={control}
label="Combobox field"
name={name}
options={[
{ value: "1", label: "One" },
{ value: "2", label: "Two" },
{ value: "3", label: "Three" },
]}
/>
);
},
};
export const Default: Story = {
...ComboboxTemplate,
};
export const Errored: Story = {
...ComboboxTemplate,
args: {
error: true,
},
};
export const Disabled: Story = {
...ComboboxTemplate,
args: {
disabled: true,
},
};
export const WithHelpText: Story = {
...ComboboxTemplate,
args: {
helperText: "Helper text",
},
};

View file

@ -0,0 +1,36 @@
import { Combobox as $Combobox, type ComboboxProps as $ComboboxProps } from "@saleor/macaw-ui/next";
import { Control, Controller, FieldPath, FieldValues } from "react-hook-form";
export type ComboboxProps<T extends FieldValues = FieldValues> = Omit<$ComboboxProps, "name"> & {
name: FieldPath<T>;
control: Control<T>;
};
export function Combobox<TFieldValues extends FieldValues = FieldValues>({
type,
required,
name,
control,
options,
...rest
}: ComboboxProps<TFieldValues>): JSX.Element {
return (
<Controller
name={name}
control={control}
render={({ field: { value, ...field }, fieldState: { error } }) => (
<$Combobox
{...rest}
{...field}
options={options}
value={value || ""}
name={name}
required={required}
type={type}
error={!!error}
helperText={rest.helperText}
/>
)}
/>
);
}

View file

@ -0,0 +1,58 @@
import { useEffect } from "react";
import { Meta, StoryObj } from "@storybook/react";
import { Input } from "./Input";
import { useForm } from "react-hook-form";
import { action } from "@storybook/addon-actions";
const meta: Meta<typeof Input> = {
title: "Components / Input",
component: Input,
};
export default meta;
type Story = StoryObj<typeof Input>;
const InputTemplate: Story = {
render: (args) => {
const { control, watch, setError } = useForm();
const name = "inputField";
if (args.error) {
setError(name, { message: "Error message" });
}
useEffect(() => {
const subscription = watch((value) => action("[React Hooks Form] Form value changed")(value));
return () => subscription.unsubscribe();
}, [watch]);
return <Input {...args} control={control} label="Input field" name={name} />;
},
};
export const Default: Story = {
...InputTemplate,
};
export const Errored: Story = {
...InputTemplate,
args: {
error: true,
},
};
export const Disabled: Story = {
...InputTemplate,
args: {
disabled: true,
},
};
export const WithHelpText: Story = {
...InputTemplate,
args: {
helperText: "Helper text",
},
};

View file

@ -0,0 +1,36 @@
import { Input as $Input, type InputProps as $InputProps } from "@saleor/macaw-ui/next";
import { Control, Controller, FieldPath, FieldValues } from "react-hook-form";
export type TextFieldElementProps<T extends FieldValues = FieldValues> = Omit<
$InputProps,
"name"
> & {
name: FieldPath<T>;
control: Control<T>;
};
export function Input<TFieldValues extends FieldValues = FieldValues>({
type,
required,
name,
control,
...rest
}: TextFieldElementProps<TFieldValues>): JSX.Element {
return (
<Controller
name={name}
control={control}
render={({ field, fieldState: { error } }) => (
<$Input
{...rest}
{...field}
name={name}
required={required}
type={type}
error={!!error}
helperText={rest.helperText}
/>
)}
/>
);
}

View file

@ -0,0 +1,70 @@
import { useEffect } from "react";
import { Meta, StoryObj } from "@storybook/react";
import { Multiselect } from "./Multiselect";
import { useForm } from "react-hook-form";
import { action } from "@storybook/addon-actions";
const meta: Meta<typeof Multiselect> = {
title: "Components / Multiselect",
component: Multiselect,
};
export default meta;
type Story = StoryObj<typeof Multiselect>;
const MultiselectTemplate: Story = {
render: (args) => {
const { control, watch, setError } = useForm();
const name = "multiselectField";
if (args.error) {
setError(name, { message: "Error message" });
}
useEffect(() => {
const subscription = watch((value) => action("[React Hooks Form] Form value changed")(value));
return () => subscription.unsubscribe();
}, [watch]);
return (
<Multiselect
{...args}
control={control}
label="Multiselect field"
name={name}
options={[
{ value: "1", label: "One" },
{ value: "2", label: "Two" },
{ value: "3", label: "Three" },
]}
/>
);
},
};
export const Default: Story = {
...MultiselectTemplate,
};
export const Errored: Story = {
...MultiselectTemplate,
args: {
error: true,
},
};
export const Disabled: Story = {
...MultiselectTemplate,
args: {
disabled: true,
},
};
export const WithHelpText: Story = {
...MultiselectTemplate,
args: {
helperText: "Helper text",
},
};

View file

@ -0,0 +1,42 @@
import {
Multiselect as $Multiselect,
type MultiselectProps as $MultiselectProps,
} from "@saleor/macaw-ui/next";
import { Control, Controller, FieldPath, FieldValues } from "react-hook-form";
export type MultiselectProps<T extends FieldValues = FieldValues> = Omit<
$MultiselectProps,
"name"
> & {
name: FieldPath<T>;
control: Control<T>;
};
export function Multiselect<TFieldValues extends FieldValues = FieldValues>({
type,
required,
name,
control,
options,
...rest
}: MultiselectProps<TFieldValues>): JSX.Element {
return (
<Controller
name={name}
control={control}
render={({ field: { value, ...field }, fieldState: { error } }) => (
<$Multiselect
{...rest}
{...field}
options={options}
value={value || []}
name={name}
required={required}
type={type}
error={!!error}
helperText={rest.helperText}
/>
)}
/>
);
}

View file

@ -0,0 +1,17 @@
import { Meta } from "@storybook/blocks";
<Meta title="Introduction" />
# React Hook Form Macaw
Package contains ready to use bindings to use Macaw with the React Hook Library.
To use it with forms, pass `control` object to the input as in example:
```ts
const { control } = useForm();
<Input control={control} label="Input field" name="name" />;
```
Components will respect default values and error states set by the RHF.

View file

@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}

File diff suppressed because it is too large Load diff