Skip to content

Commit

Permalink
Add input and field components
Browse files Browse the repository at this point in the history
  • Loading branch information
Mrtenz committed Jul 4, 2024
1 parent 8462cda commit 1eb1322
Show file tree
Hide file tree
Showing 18 changed files with 440 additions and 13 deletions.
90 changes: 90 additions & 0 deletions packages/snaps-sdk/src/jsx/components/form/Field.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import type { Meta, Story } from '@metamask/snaps-storybook';

import { Button } from './Button';
import type { FieldProps } from './Field';
import { Field } from './Field';
import { Input } from './Input';

const meta: Meta<typeof Field> = {
title: 'Forms/Field',
component: Field,
argTypes: {
label: {
description: 'The label of the field.',
control: 'text',
},
error: {
description: 'The error message of the field.',
control: 'text',
},
children: {
description: 'The form component to render inside the field.',
table: {
type: {
summary:
'[InputElement, ButtonElement] | DropdownElement | FileInputElement | InputElement | CheckboxElement',
},
},
},
},
};

export default meta;

/**
* The field component wraps an input element with a label and optional error
* message.
*/
export const Default: Story<FieldProps> = {
render: (props) => <Field {...props} />,
args: {
label: 'Field label',
children: (
<Input
name="input"
type="text"
value=""
placeholder="Input placeholder"
/>
),
},
};

/**
* The field component can display an error message.
*/
export const Error: Story<FieldProps> = {
render: (props) => <Field {...props} />,
args: {
label: 'Field label',
error: 'Field error',
children: (
<Input
name="input"
type="text"
value=""
placeholder="Input placeholder"
/>
),
},
};

/**
* Inputs can be combined with a button, for example, to submit a form, or to
* perform some action.
*/
export const InputWithButton: Story<FieldProps> = {
render: (props) => <Field {...props} />,
args: {
label: 'Field label',
children: [
<Input
name="input"
type="text"
value=""
placeholder="Input placeholder"
/>,
<Button type="submit">Submit</Button>,
],
},
};
78 changes: 78 additions & 0 deletions packages/snaps-sdk/src/jsx/components/form/Input.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import type { Meta, Story } from '@metamask/snaps-storybook';

import type { InputProps } from './Input';
import { Input } from './Input';

const meta: Meta<typeof Input> = {
title: 'Forms/Input',
component: Input,
argTypes: {
name: {
description:
'The name of the input field. This is used to identify the input field in the form data.',
},
type: {
description: 'The type of the input field.',
options: ['text', 'password', 'number'],
control: 'select',
},
value: {
description: 'The default value of the input field.',
control: 'text',
},
placeholder: {
description: 'The placeholder text of the input field.',
control: 'text',
},
},
};

export default meta;

/**
* The input component renders an input field.
*/
export const Default: Story<InputProps> = {
render: (props) => <Input {...props} />,
args: {
name: 'input',
placeholder: 'This is the placeholder text',
},
};

/**
* Number inputs only accept numbers.
*/
export const Number: Story<InputProps> = {
render: (props) => <Input {...props} />,
args: {
name: 'input',
type: 'number',
placeholder: 'This input only accepts numbers',
},
};

/**
* Password inputs hide the entered text.
*/
export const Password: Story<InputProps> = {
render: (props) => <Input {...props} />,
args: {
name: 'input',
type: 'password',
placeholder: 'This is a password input',
},
};

/**
* It's possible to set a default value for the input. The value can be changed
* by the user.
*/
export const DefaultValue: Story<InputProps> = {
render: (props) => <Input {...props} />,
args: {
name: 'input',
value: 'Default value',
placeholder: 'This input has a default value',
},
};
10 changes: 10 additions & 0 deletions packages/snaps-storybook/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,14 @@ module.exports = {
parserOptions: {
tsconfigRootDir: __dirname,
},

overrides: [
{
files: ['**/theme/**/*.ts', '**/components/snaps/**/*.styles.ts'],
rules: {
'@typescript-eslint/naming-convention': 'off',
'@typescript-eslint/unbound-method': 'off',
},
},
],
};
2 changes: 1 addition & 1 deletion packages/snaps-storybook/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ module.exports = deepmerge(baseConfig, {
{
tsconfig: {
jsx: 'react-jsx',
jsxImportSource: resolve(__dirname, './src/jsx'),
jsxImportSource: 'react',
},
},
],
Expand Down
1 change: 1 addition & 0 deletions packages/snaps-storybook/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"@babel/parser": "^7.24.7",
"@babel/traverse": "^7.24.7",
"@babel/types": "^7.23.0",
"@chakra-ui/anatomy": "^2.1.1",
"@chakra-ui/react": "^2.6.1",
"@emotion/react": "^11.10.8",
"@emotion/styled": "^11.10.8",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
/* eslint-disable @typescript-eslint/naming-convention */

import { defineStyle, defineStyleConfig } from '@chakra-ui/react';

export const styles = defineStyleConfig({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { formAnatomy, formErrorAnatomy } from '@chakra-ui/anatomy';
import {
createMultiStyleConfigHelpers,
defineStyleConfig,
} from '@chakra-ui/react';

const { definePartsStyle, defineMultiStyleConfig } =
createMultiStyleConfigHelpers(formAnatomy.keys);

const {
definePartsStyle: defineErrorPartsStyle,
defineMultiStyleConfig: defineErrorMultiStyleConfig,
} = createMultiStyleConfigHelpers(formErrorAnatomy.keys);

export const styles = {
FormControl: defineMultiStyleConfig({
baseStyle: definePartsStyle({
helperText: {
color: 'error.default',
marginTop: '1',
fontSize: '2xs',
},
}),
}),

FormError: defineErrorMultiStyleConfig({
baseStyle: defineErrorPartsStyle({
text: {
color: 'error.default',
fontSize: '2xs',
marginTop: '1',
},
}),
}),

FormLabel: defineStyleConfig({
baseStyle: {
color: 'text.default',
fontSize: 'sm',
marginBottom: '0',
},
}),
};
30 changes: 30 additions & 0 deletions packages/snaps-storybook/src/components/snaps/field/Field.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { FormControl, FormErrorMessage, FormLabel } from '@chakra-ui/react';
import type { FieldProps } from '@metamask/snaps-sdk/jsx';
import type { FunctionComponent } from 'react';

import type { RenderProps } from '../../Renderer';
import { Input } from './components';

/**
* The field component, which wraps an input element with a label and optional
* error message. See the {@link FieldProps} type for the props.
*
* @param props - The props of the component.
* @param props.label - The label of the field.
* @param props.error - The error message of the field.
* @param props.children - The input field to render inside the field.
* @param props.Renderer - The renderer to use for the input field.
* @returns The field element.
*/
export const Field: FunctionComponent<RenderProps<FieldProps>> = ({
label,
error,
children,
Renderer,
}) => (
<FormControl isInvalid={Boolean(error)}>
<FormLabel>{label}</FormLabel>
<Input element={children} Renderer={Renderer} />
{error && <FormErrorMessage>{error}</FormErrorMessage>}
</FormControl>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { InputGroup, InputRightElement } from '@chakra-ui/react';
import type { FieldProps } from '@metamask/snaps-sdk/jsx';
import type { FunctionComponent } from 'react';
import { useEffect, useState, useRef } from 'react';

import type { RendererProps } from '../../../Renderer';

/**
* The props for the {@link Input} component.
*/
export type InputProps = {
/**
* The input element to render.
*/
element: FieldProps['children'];

/**
* The renderer to use for the input field.
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
Renderer: FunctionComponent<RendererProps>;
};

/**
* This is a wrapper of the input component, which allows for rendering
* different types of input fields.
*
* @param props - The component props.
* @param props.element - The input element to render.
* @param props.Renderer - The renderer to use for the input field.
* @returns The rendered input component.
*/
export const Input: FunctionComponent<InputProps> = ({ element, Renderer }) => {
const ref = useRef<HTMLDivElement>(null);
const [width, setWidth] = useState<number>(0);

useEffect(() => {
if (ref.current) {
setWidth(ref.current.offsetWidth);
}
}, [element, ref]);

if (Array.isArray(element)) {
const [input, button] = element;
return (
<InputGroup
sx={{
input: {
paddingRight: `${width}px`,
},
}}
>
<Renderer id="field-input" element={input} />
<InputRightElement ref={ref} width="auto" paddingX="4">
<Renderer id="field-button" element={button} />
</InputRightElement>
</InputGroup>
);
}

return <Renderer id="field-input" element={element} />;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Input';
2 changes: 2 additions & 0 deletions packages/snaps-storybook/src/components/snaps/field/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { Field as Component } from './Field';
export { styles } from './Field.styles';
4 changes: 4 additions & 0 deletions packages/snaps-storybook/src/components/snaps/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import * as Box from './box';
import * as Button from './button';
import * as Copyable from './copyable';
import * as Divider from './divider';
import * as Field from './field';
import * as Footer from './footer';
import * as Heading from './heading';
import * as Input from './input';
import * as Italic from './italic';
import * as Link from './link';
import * as Row from './row';
Expand All @@ -26,8 +28,10 @@ export const SNAPS_COMPONENTS: Record<string, Component> = {
Button,
Copyable,
Divider,
Field,
Footer,
Heading,
Input,
Italic,
Link,
Row,
Expand Down
Loading

0 comments on commit 1eb1322

Please sign in to comment.