Skip to content

Commit

Permalink
Add min, max and step props to Input (#2821)
Browse files Browse the repository at this point in the history
  • Loading branch information
GuillaumeRx authored Oct 9, 2024
1 parent 02c70c7 commit 9142775
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"url": "https://github.com/MetaMask/snaps.git"
},
"source": {
"shasum": "HEAbfXBUqw5fNP+sJVyvUpXucZy6CwiCFXJi36CEfUw=",
"shasum": "4Waitn0bnIxi/w96Dqni+0tHJkuPURlZznHkrdp2cPc=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand Down
2 changes: 1 addition & 1 deletion packages/examples/packages/browserify/snap.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"url": "https://github.com/MetaMask/snaps.git"
},
"source": {
"shasum": "mCoDlMSdhDJAXd9zT74ST7jHysifHdQ8r0++b8uPbOs=",
"shasum": "27AQQByAFXL3KIpdFzj+ke9/98WSR8o8Fan72A4LuXg=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand Down
5 changes: 4 additions & 1 deletion packages/snaps-sdk/src/jsx/components/form/Input.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,16 @@ describe('Input', () => {
});

it('renders a number input', () => {
const result = <Input name="foo" type="number" />;
const result = <Input name="foo" type="number" min={0} max={10} step={1} />;

expect(result).toStrictEqual({
type: 'Input',
props: {
name: 'foo',
type: 'number',
min: 0,
max: 10,
step: 1,
},
key: null,
});
Expand Down
42 changes: 36 additions & 6 deletions packages/snaps-sdk/src/jsx/components/form/Input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,23 @@ import { createSnapComponent } from '../../component';

// TODO: Add the `onChange` prop to the `InputProps` type.

export type GenericInputProps = {
name: string;
value?: string | undefined;
placeholder?: string | undefined;
};

export type TextInputProps = { type: 'text' } & GenericInputProps;

export type PasswordInputProps = { type: 'password' } & GenericInputProps;

export type NumberInputProps = {
type: 'number';
min?: number;
max?: number;
step?: number;
} & GenericInputProps;

/**
* The props of the {@link Input} component.
*
Expand All @@ -10,13 +27,18 @@ import { createSnapComponent } from '../../component';
* @property type - The type of the input field. Defaults to `text`.
* @property value - The value of the input field.
* @property placeholder - The placeholder text of the input field.
* @property min - The minimum value of the input field.
* Only applicable to the type `number` input.
* @property max - The maximum value of the input field.
* Only applicable to the type `number` input.
* @property step - The step value of the input field.
* Only applicable to the type `number` input.
*/
export type InputProps = {
name: string;
type?: 'text' | 'password' | 'number' | undefined;
value?: string | undefined;
placeholder?: string | undefined;
};
export type InputProps =
| GenericInputProps
| TextInputProps
| PasswordInputProps
| NumberInputProps;

const TYPE = 'Input';

Expand All @@ -29,9 +51,17 @@ const TYPE = 'Input';
* @param props.type - The type of the input field.
* @param props.value - The value of the input field.
* @param props.placeholder - The placeholder text of the input field.
* @param props.min - The minimum value of the input field.
* Only applicable to the type `number` input.
* @param props.max - The maximum value of the input field.
* Only applicable to the type `number` input.
* @param props.step - The step value of the input field.
* Only applicable to the type `number` input.
* @returns An input element.
* @example
* <Input name="username" type="text" />
* @example
* <Input name="numeric" type="number" min={1} max={100} step={1} />
*/
export const Input = createSnapComponent<InputProps, typeof TYPE>(TYPE);

Expand Down
5 changes: 5 additions & 0 deletions packages/snaps-sdk/src/jsx/validation.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ describe('InputStruct', () => {
<Input name="foo" type="number" />,
<Input name="foo" type="text" value="bar" />,
<Input name="foo" type="text" placeholder="bar" />,
<Input name="foo" type="number" min={0} max={10} step={1} />,
])('validates an input element', (value) => {
expect(is(value, InputStruct)).toBe(true);
});
Expand All @@ -222,6 +223,10 @@ describe('InputStruct', () => {
<Input />,
// @ts-expect-error - Invalid props.
<Input name="foo" type="foo" />,
// @ts-expect-error - Invalid props.
<Input name="foo" min="foo" />,
// @ts-expect-error - Invalid props.
<Input name="foo" type="text" min={42} />,
<Text>foo</Text>,
<Box>
<Text>foo</Text>
Expand Down
81 changes: 76 additions & 5 deletions packages/snaps-sdk/src/jsx/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
string,
tuple,
refine,
assign,
} from '@metamask/superstruct';
import {
CaipAccountIdStruct,
Expand Down Expand Up @@ -194,6 +195,24 @@ function element<Name extends string, Props extends ObjectSchema = EmptyObject>(
});
}

/**
* A helper function for creating a struct for a JSX element with selective props.
*
* @param name - The name of the element.
* @param selector - The selector function choosing the struct to validate with.
* @returns The struct for the element.
*/
function elementWithSelectiveProps<
Name extends string,
Selector extends (value: any) => AnyStruct,
>(name: Name, selector: Selector) {
return object({
type: literal(name) as unknown as Struct<Name, Name>,
props: selectiveUnion(selector),
key: nullable(KeyStruct),
});
}

/**
* A struct for the {@link ImageElement} type.
*/
Expand Down Expand Up @@ -240,17 +259,69 @@ export const CheckboxStruct: Describe<CheckboxElement> = element('Checkbox', {
});

/**
* A struct for the {@link InputElement} type.
* A struct for the generic input element props.
*/
export const InputStruct: Describe<InputElement> = element('Input', {
export const GenericInputPropsStruct = object({
name: string(),
type: optional(
nullUnion([literal('text'), literal('password'), literal('number')]),
),
value: optional(string()),
placeholder: optional(string()),
});

/**
* A struct for the text type input props.
*/
export const TextInputPropsStruct = assign(
GenericInputPropsStruct,
object({
type: literal('text'),
}),
);

/**
* A struct for the password type input props.
*/
export const PasswordInputPropsStruct = assign(
GenericInputPropsStruct,
object({
type: literal('password'),
}),
);

/**
* A struct for the number type input props.
*/
export const NumberInputPropsStruct = assign(
GenericInputPropsStruct,
object({
type: literal('number'),
min: optional(number()),
max: optional(number()),
step: optional(number()),
}),
);

/**
* A struct for the {@link InputElement} type.
*/
export const InputStruct: Describe<InputElement> = elementWithSelectiveProps(
'Input',
(value) => {
if (isPlainObject(value) && hasProperty(value, 'type')) {
switch (value.type) {
case 'text':
return TextInputPropsStruct;
case 'password':
return PasswordInputPropsStruct;
case 'number':
return NumberInputPropsStruct;
default:
return GenericInputPropsStruct;
}
}
return GenericInputPropsStruct;
},
);

/**
* A struct for the {@link OptionElement} type.
*/
Expand Down

0 comments on commit 9142775

Please sign in to comment.