Skip to content

Commit d281375

Browse files
committed
Add input and field components
1 parent 7b44bd2 commit d281375

File tree

18 files changed

+440
-13
lines changed

18 files changed

+440
-13
lines changed
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import type { Meta, Story } from '@metamask/snaps-storybook';
2+
3+
import { Button } from './Button';
4+
import type { FieldProps } from './Field';
5+
import { Field } from './Field';
6+
import { Input } from './Input';
7+
8+
const meta: Meta<typeof Field> = {
9+
title: 'Forms/Field',
10+
component: Field,
11+
argTypes: {
12+
label: {
13+
description: 'The label of the field.',
14+
control: 'text',
15+
},
16+
error: {
17+
description: 'The error message of the field.',
18+
control: 'text',
19+
},
20+
children: {
21+
description: 'The form component to render inside the field.',
22+
table: {
23+
type: {
24+
summary:
25+
'[InputElement, ButtonElement] | DropdownElement | FileInputElement | InputElement | CheckboxElement',
26+
},
27+
},
28+
},
29+
},
30+
};
31+
32+
export default meta;
33+
34+
/**
35+
* The field component wraps an input element with a label and optional error
36+
* message.
37+
*/
38+
export const Default: Story<FieldProps> = {
39+
render: (props) => <Field {...props} />,
40+
args: {
41+
label: 'Field label',
42+
children: (
43+
<Input
44+
name="input"
45+
type="text"
46+
value=""
47+
placeholder="Input placeholder"
48+
/>
49+
),
50+
},
51+
};
52+
53+
/**
54+
* The field component can display an error message.
55+
*/
56+
export const Error: Story<FieldProps> = {
57+
render: (props) => <Field {...props} />,
58+
args: {
59+
label: 'Field label',
60+
error: 'Field error',
61+
children: (
62+
<Input
63+
name="input"
64+
type="text"
65+
value=""
66+
placeholder="Input placeholder"
67+
/>
68+
),
69+
},
70+
};
71+
72+
/**
73+
* Inputs can be combined with a button, for example, to submit a form, or to
74+
* perform some action.
75+
*/
76+
export const InputWithButton: Story<FieldProps> = {
77+
render: (props) => <Field {...props} />,
78+
args: {
79+
label: 'Field label',
80+
children: [
81+
<Input
82+
name="input"
83+
type="text"
84+
value=""
85+
placeholder="Input placeholder"
86+
/>,
87+
<Button type="submit">Submit</Button>,
88+
],
89+
},
90+
};
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import type { Meta, Story } from '@metamask/snaps-storybook';
2+
3+
import type { InputProps } from './Input';
4+
import { Input } from './Input';
5+
6+
const meta: Meta<typeof Input> = {
7+
title: 'Forms/Input',
8+
component: Input,
9+
argTypes: {
10+
name: {
11+
description:
12+
'The name of the input field. This is used to identify the input field in the form data.',
13+
},
14+
type: {
15+
description: 'The type of the input field.',
16+
options: ['text', 'password', 'number'],
17+
control: 'select',
18+
},
19+
value: {
20+
description: 'The default value of the input field.',
21+
control: 'text',
22+
},
23+
placeholder: {
24+
description: 'The placeholder text of the input field.',
25+
control: 'text',
26+
},
27+
},
28+
};
29+
30+
export default meta;
31+
32+
/**
33+
* The input component renders an input field.
34+
*/
35+
export const Default: Story<InputProps> = {
36+
render: (props) => <Input {...props} />,
37+
args: {
38+
name: 'input',
39+
placeholder: 'This is the placeholder text',
40+
},
41+
};
42+
43+
/**
44+
* Number inputs only accept numbers.
45+
*/
46+
export const Number: Story<InputProps> = {
47+
render: (props) => <Input {...props} />,
48+
args: {
49+
name: 'input',
50+
type: 'number',
51+
placeholder: 'This input only accepts numbers',
52+
},
53+
};
54+
55+
/**
56+
* Password inputs hide the entered text.
57+
*/
58+
export const Password: Story<InputProps> = {
59+
render: (props) => <Input {...props} />,
60+
args: {
61+
name: 'input',
62+
type: 'password',
63+
placeholder: 'This is a password input',
64+
},
65+
};
66+
67+
/**
68+
* It's possible to set a default value for the input. The value can be changed
69+
* by the user.
70+
*/
71+
export const DefaultValue: Story<InputProps> = {
72+
render: (props) => <Input {...props} />,
73+
args: {
74+
name: 'input',
75+
value: 'Default value',
76+
placeholder: 'This input has a default value',
77+
},
78+
};

packages/snaps-storybook/.eslintrc.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,14 @@ module.exports = {
44
parserOptions: {
55
tsconfigRootDir: __dirname,
66
},
7+
8+
overrides: [
9+
{
10+
files: ['**/theme/**/*.ts', '**/components/snaps/**/*.styles.ts'],
11+
rules: {
12+
'@typescript-eslint/naming-convention': 'off',
13+
'@typescript-eslint/unbound-method': 'off',
14+
},
15+
},
16+
],
717
};

packages/snaps-storybook/jest.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ module.exports = deepmerge(baseConfig, {
3131
{
3232
tsconfig: {
3333
jsx: 'react-jsx',
34-
jsxImportSource: resolve(__dirname, './src/jsx'),
34+
jsxImportSource: 'react',
3535
},
3636
},
3737
],

packages/snaps-storybook/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"@babel/parser": "^7.24.7",
4949
"@babel/traverse": "^7.24.7",
5050
"@babel/types": "^7.23.0",
51+
"@chakra-ui/anatomy": "^2.1.1",
5152
"@chakra-ui/react": "^2.6.1",
5253
"@emotion/react": "^11.10.8",
5354
"@emotion/styled": "^11.10.8",

packages/snaps-storybook/src/components/snaps/button/Button.styles.tsx renamed to packages/snaps-storybook/src/components/snaps/button/Button.styles.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
/* eslint-disable @typescript-eslint/naming-convention */
2-
31
import { defineStyle, defineStyleConfig } from '@chakra-ui/react';
42

53
export const styles = defineStyleConfig({
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { formAnatomy, formErrorAnatomy } from '@chakra-ui/anatomy';
2+
import {
3+
createMultiStyleConfigHelpers,
4+
defineStyleConfig,
5+
} from '@chakra-ui/react';
6+
7+
const { definePartsStyle, defineMultiStyleConfig } =
8+
createMultiStyleConfigHelpers(formAnatomy.keys);
9+
10+
const {
11+
definePartsStyle: defineErrorPartsStyle,
12+
defineMultiStyleConfig: defineErrorMultiStyleConfig,
13+
} = createMultiStyleConfigHelpers(formErrorAnatomy.keys);
14+
15+
export const styles = {
16+
FormControl: defineMultiStyleConfig({
17+
baseStyle: definePartsStyle({
18+
helperText: {
19+
color: 'error.default',
20+
marginTop: '1',
21+
fontSize: '2xs',
22+
},
23+
}),
24+
}),
25+
26+
FormError: defineErrorMultiStyleConfig({
27+
baseStyle: defineErrorPartsStyle({
28+
text: {
29+
color: 'error.default',
30+
fontSize: '2xs',
31+
marginTop: '1',
32+
},
33+
}),
34+
}),
35+
36+
FormLabel: defineStyleConfig({
37+
baseStyle: {
38+
color: 'text.default',
39+
fontSize: 'sm',
40+
marginBottom: '0',
41+
},
42+
}),
43+
};
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { FormControl, FormErrorMessage, FormLabel } from '@chakra-ui/react';
2+
import type { FieldProps } from '@metamask/snaps-sdk/jsx';
3+
import type { FunctionComponent } from 'react';
4+
5+
import type { RenderProps } from '../../Renderer';
6+
import { Input } from './components';
7+
8+
/**
9+
* The field component, which wraps an input element with a label and optional
10+
* error message. See the {@link FieldProps} type for the props.
11+
*
12+
* @param props - The props of the component.
13+
* @param props.label - The label of the field.
14+
* @param props.error - The error message of the field.
15+
* @param props.children - The input field to render inside the field.
16+
* @param props.Renderer - The renderer to use for the input field.
17+
* @returns The field element.
18+
*/
19+
export const Field: FunctionComponent<RenderProps<FieldProps>> = ({
20+
label,
21+
error,
22+
children,
23+
Renderer,
24+
}) => (
25+
<FormControl isInvalid={Boolean(error)}>
26+
<FormLabel>{label}</FormLabel>
27+
<Input element={children} Renderer={Renderer} />
28+
{error && <FormErrorMessage>{error}</FormErrorMessage>}
29+
</FormControl>
30+
);
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { InputGroup, InputRightElement } from '@chakra-ui/react';
2+
import type { FieldProps } from '@metamask/snaps-sdk/jsx';
3+
import type { FunctionComponent } from 'react';
4+
import { useEffect, useState, useRef } from 'react';
5+
6+
import type { RendererProps } from '../../../Renderer';
7+
8+
/**
9+
* The props for the {@link Input} component.
10+
*/
11+
export type InputProps = {
12+
/**
13+
* The input element to render.
14+
*/
15+
element: FieldProps['children'];
16+
17+
/**
18+
* The renderer to use for the input field.
19+
*/
20+
// eslint-disable-next-line @typescript-eslint/naming-convention
21+
Renderer: FunctionComponent<RendererProps>;
22+
};
23+
24+
/**
25+
* This is a wrapper of the input component, which allows for rendering
26+
* different types of input fields.
27+
*
28+
* @param props - The component props.
29+
* @param props.element - The input element to render.
30+
* @param props.Renderer - The renderer to use for the input field.
31+
* @returns The rendered input component.
32+
*/
33+
export const Input: FunctionComponent<InputProps> = ({ element, Renderer }) => {
34+
const ref = useRef<HTMLDivElement>(null);
35+
const [width, setWidth] = useState<number>(0);
36+
37+
useEffect(() => {
38+
if (ref.current) {
39+
setWidth(ref.current.offsetWidth);
40+
}
41+
}, [element, ref]);
42+
43+
if (Array.isArray(element)) {
44+
const [input, button] = element;
45+
return (
46+
<InputGroup
47+
sx={{
48+
input: {
49+
paddingRight: `${width}px`,
50+
},
51+
}}
52+
>
53+
<Renderer id="field-input" element={input} />
54+
<InputRightElement ref={ref} width="auto" paddingX="4">
55+
<Renderer id="field-button" element={button} />
56+
</InputRightElement>
57+
</InputGroup>
58+
);
59+
}
60+
61+
return <Renderer id="field-input" element={element} />;
62+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './Input';

0 commit comments

Comments
 (0)