diff --git a/packages/examples/packages/browserify-plugin/snap.manifest.json b/packages/examples/packages/browserify-plugin/snap.manifest.json index 5e76ac113b..3c4e329585 100644 --- a/packages/examples/packages/browserify-plugin/snap.manifest.json +++ b/packages/examples/packages/browserify-plugin/snap.manifest.json @@ -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", diff --git a/packages/examples/packages/browserify/snap.manifest.json b/packages/examples/packages/browserify/snap.manifest.json index be6a71ec6d..74bae4ffd7 100644 --- a/packages/examples/packages/browserify/snap.manifest.json +++ b/packages/examples/packages/browserify/snap.manifest.json @@ -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", diff --git a/packages/snaps-sdk/src/jsx/components/form/Input.test.tsx b/packages/snaps-sdk/src/jsx/components/form/Input.test.tsx index 9cc32e97af..f446081a89 100644 --- a/packages/snaps-sdk/src/jsx/components/form/Input.test.tsx +++ b/packages/snaps-sdk/src/jsx/components/form/Input.test.tsx @@ -28,13 +28,16 @@ describe('Input', () => { }); it('renders a number input', () => { - const result = ; + const result = ; expect(result).toStrictEqual({ type: 'Input', props: { name: 'foo', type: 'number', + min: 0, + max: 10, + step: 1, }, key: null, }); diff --git a/packages/snaps-sdk/src/jsx/components/form/Input.ts b/packages/snaps-sdk/src/jsx/components/form/Input.ts index c68b44e830..814bac24f5 100644 --- a/packages/snaps-sdk/src/jsx/components/form/Input.ts +++ b/packages/snaps-sdk/src/jsx/components/form/Input.ts @@ -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. * @@ -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'; @@ -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 * + * @example + * */ export const Input = createSnapComponent(TYPE); diff --git a/packages/snaps-sdk/src/jsx/validation.test.tsx b/packages/snaps-sdk/src/jsx/validation.test.tsx index f10db2cf26..9cf87ac8e3 100644 --- a/packages/snaps-sdk/src/jsx/validation.test.tsx +++ b/packages/snaps-sdk/src/jsx/validation.test.tsx @@ -207,6 +207,7 @@ describe('InputStruct', () => { , , , + , ])('validates an input element', (value) => { expect(is(value, InputStruct)).toBe(true); }); @@ -222,6 +223,10 @@ describe('InputStruct', () => { , // @ts-expect-error - Invalid props. , + // @ts-expect-error - Invalid props. + , + // @ts-expect-error - Invalid props. + , foo, foo diff --git a/packages/snaps-sdk/src/jsx/validation.ts b/packages/snaps-sdk/src/jsx/validation.ts index 8240a36ffc..07c1f5e6a7 100644 --- a/packages/snaps-sdk/src/jsx/validation.ts +++ b/packages/snaps-sdk/src/jsx/validation.ts @@ -18,6 +18,7 @@ import { string, tuple, refine, + assign, } from '@metamask/superstruct'; import { CaipAccountIdStruct, @@ -194,6 +195,24 @@ function element( }); } +/** + * 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, + props: selectiveUnion(selector), + key: nullable(KeyStruct), + }); +} + /** * A struct for the {@link ImageElement} type. */ @@ -240,17 +259,69 @@ export const CheckboxStruct: Describe = element('Checkbox', { }); /** - * A struct for the {@link InputElement} type. + * A struct for the generic input element props. */ -export const InputStruct: Describe = 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 = 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. */