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:
parent
ce8d9deb81
commit
8a339fc31b
20 changed files with 5398 additions and 529 deletions
5
.changeset/gentle-poems-relax.md
Normal file
5
.changeset/gentle-poems-relax.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
"@saleor/react-hook-form-macaw": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Introduction of the library integrating Macaw with the React Hook Forms.
|
15
packages/react-hook-form-macaw/.eslintrc
Normal file
15
packages/react-hook-form-macaw/.eslintrc
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
2
packages/react-hook-form-macaw/.gitignore
vendored
Normal file
2
packages/react-hook-form-macaw/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
# Ignore storybook build artifacts
|
||||||
|
storybook-static
|
29
packages/react-hook-form-macaw/.storybook/main.ts
Normal file
29
packages/react-hook-form-macaw/.storybook/main.ts
Normal 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()],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
47
packages/react-hook-form-macaw/.storybook/preview.tsx
Normal file
47
packages/react-hook-form-macaw/.storybook/preview.tsx
Normal 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;
|
4
packages/react-hook-form-macaw/.storybook/styles.css
Normal file
4
packages/react-hook-form-macaw/.storybook/styles.css
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
.sbdocs-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
1
packages/react-hook-form-macaw/CHANGELOG.md
Normal file
1
packages/react-hook-form-macaw/CHANGELOG.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# @saleor/react-hook-form-macaw
|
1
packages/react-hook-form-macaw/README.md
Normal file
1
packages/react-hook-form-macaw/README.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# react-hook-form-macaw
|
3
packages/react-hook-form-macaw/index.ts
Normal file
3
packages/react-hook-form-macaw/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export * from "./src/components/Input";
|
||||||
|
export * from "./src/components/Combobox";
|
||||||
|
export * from "./src/components/Multiselect";
|
5
packages/react-hook-form-macaw/next-env.d.ts
vendored
Normal file
5
packages/react-hook-form-macaw/next-env.d.ts
vendored
Normal 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.
|
38
packages/react-hook-form-macaw/package.json
Normal file
38
packages/react-hook-form-macaw/package.json
Normal 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"
|
||||||
|
}
|
|
@ -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",
|
||||||
|
},
|
||||||
|
};
|
36
packages/react-hook-form-macaw/src/components/Combobox.tsx
Normal file
36
packages/react-hook-form-macaw/src/components/Combobox.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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",
|
||||||
|
},
|
||||||
|
};
|
36
packages/react-hook-form-macaw/src/components/Input.tsx
Normal file
36
packages/react-hook-form-macaw/src/components/Input.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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",
|
||||||
|
},
|
||||||
|
};
|
|
@ -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}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
17
packages/react-hook-form-macaw/src/stories/Introduction.mdx
Normal file
17
packages/react-hook-form-macaw/src/stories/Introduction.mdx
Normal 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.
|
20
packages/react-hook-form-macaw/tsconfig.json
Normal file
20
packages/react-hook-form-macaw/tsconfig.json
Normal 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"]
|
||||||
|
}
|
5428
pnpm-lock.yaml
5428
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue