diff --git a/packages/examples/packages/bip32/snap.manifest.json b/packages/examples/packages/bip32/snap.manifest.json index abdb7ebda0..2acc3430e6 100644 --- a/packages/examples/packages/bip32/snap.manifest.json +++ b/packages/examples/packages/bip32/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "ik7uBPZA2o4lF5QljJZQSa2RdCng8nYtKsEulOWXZU4=", + "shasum": "meplfIhDKAdAYfRjk1slGTks3rI6+uPewth+sL7o/Go=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/bip44/snap.manifest.json b/packages/examples/packages/bip44/snap.manifest.json index f948ab344d..b21a1db41a 100644 --- a/packages/examples/packages/bip44/snap.manifest.json +++ b/packages/examples/packages/bip44/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "vpc0ie9a1H8KWv8UMpOBaPZyGJTjESuvjv1ERUMttaM=", + "shasum": "hM4uyKv1VEidiHratstws0S/LdBzPWjOQncUkHBNDP0=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/browserify-plugin/snap.manifest.json b/packages/examples/packages/browserify-plugin/snap.manifest.json index ef8b7f456f..9ca1082f37 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": "HRGY/ewBpqh8U7fsF/3QgRrpKqn0uDNTdpi83ZCqPQ0=", + "shasum": "uFeB4RsFZsV7ZYjLPEp8R+R96iuN9gpHjYB1MKUy0bw=", "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 d59b9a9553..e4f36d7c03 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": "x1+/hSsNK4ymlrifMWvkwpibVxNSUekwvYdIwmw7T2k=", + "shasum": "9RcL8rtVPwRiXki5Vs+pEA6lO1CthaofVv2UQF1COlY=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/client-status/snap.manifest.json b/packages/examples/packages/client-status/snap.manifest.json index f2a1e5f211..3c27f9032c 100644 --- a/packages/examples/packages/client-status/snap.manifest.json +++ b/packages/examples/packages/client-status/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "YaCBzI0O1i5L5l5gFfzmNhfwuvnnxbkfmsNjLkwFhfY=", + "shasum": "BmrBVhdD2ZmEdDjNVdEiVSzd3uS8PiGgmZcvYmHnHxw=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/cronjobs/snap.manifest.json b/packages/examples/packages/cronjobs/snap.manifest.json index 22b484c404..6beb76defc 100644 --- a/packages/examples/packages/cronjobs/snap.manifest.json +++ b/packages/examples/packages/cronjobs/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "dxp0O4Nhq7QOU1k6S5FJvaxvuc5wGnLsInCZ184N6gM=", + "shasum": "oZbPamYypPK6BHoVJ/Mx0l7pLIBjQRBtbZNZWyDMPGs=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/dialogs/snap.manifest.json b/packages/examples/packages/dialogs/snap.manifest.json index 7536f130ac..d56d58df82 100644 --- a/packages/examples/packages/dialogs/snap.manifest.json +++ b/packages/examples/packages/dialogs/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "AP2i5PiuYC9Dr9chFNMP1FMDJmPwpsFBr34+OKEfo/4=", + "shasum": "prxccwZ+383T+7jjMrSabwO74aBRyoKEnnP1o7r1Aac=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/ethereum-provider/snap.manifest.json b/packages/examples/packages/ethereum-provider/snap.manifest.json index 205345a143..2e06422fbb 100644 --- a/packages/examples/packages/ethereum-provider/snap.manifest.json +++ b/packages/examples/packages/ethereum-provider/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "6NIpWbenThi4e3LAiAAB0MP2PQcAcdadhcKAccM66yo=", + "shasum": "GSpRcUSfCYEWViNqYyG3NdTa4/nO8EGukSHvGBVXtT8=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/ethers-js/snap.manifest.json b/packages/examples/packages/ethers-js/snap.manifest.json index 8cd9af62a3..1561e38dc2 100644 --- a/packages/examples/packages/ethers-js/snap.manifest.json +++ b/packages/examples/packages/ethers-js/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "4Nt428/mGU2ozcXM+X28gZjmKbyI1BQxuDntChDFl0M=", + "shasum": "bG6octHXbG5c1Keftfj2kCbHcATOJMnVe5oy4ENLGYM=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/file-upload/snap.manifest.json b/packages/examples/packages/file-upload/snap.manifest.json index 72aa5cbb98..001a5d998c 100644 --- a/packages/examples/packages/file-upload/snap.manifest.json +++ b/packages/examples/packages/file-upload/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "YNic7/VXJF32HBUr5npNpXopQB7wwY50d9ejLpVclKI=", + "shasum": "dRJ64XJAM36aNfsZBN7FC8fGmh/4b3nd0V0wJZQfmzk=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/get-entropy/snap.manifest.json b/packages/examples/packages/get-entropy/snap.manifest.json index 9f8aba5e20..d6b20a49c1 100644 --- a/packages/examples/packages/get-entropy/snap.manifest.json +++ b/packages/examples/packages/get-entropy/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "0vquB1L/2Ftqhkth3a2hhU9P8Ij6QAh0VFDhvFi60sw=", + "shasum": "z/xO4vaYBErIumDuzkS1JuWD0w3M/JQzrkl7QdH+Rsw=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/get-file/snap.manifest.json b/packages/examples/packages/get-file/snap.manifest.json index 215968be2f..f8010e3d09 100644 --- a/packages/examples/packages/get-file/snap.manifest.json +++ b/packages/examples/packages/get-file/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "FZAIkcXgzle5cFsso165dzSTwerIFAzASA8rptjezmc=", + "shasum": "RY6WMOfBdTEWYMUX7zIzNy7ZmMjZdxEZUbsW4d7bAWc=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/home-page/snap.manifest.json b/packages/examples/packages/home-page/snap.manifest.json index 9e16069e29..8c1a57c967 100644 --- a/packages/examples/packages/home-page/snap.manifest.json +++ b/packages/examples/packages/home-page/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "B2lQ3m9r4PRZ2vRrrFgenZvRSomQKIs8sJOSM5U9hDQ=", + "shasum": "7w5lE7ZVBqS66ghFwA5z3USgqOxh6vU2d71JBi5dx+s=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/images/snap.manifest.json b/packages/examples/packages/images/snap.manifest.json index f34ddbc021..2ebb259b78 100644 --- a/packages/examples/packages/images/snap.manifest.json +++ b/packages/examples/packages/images/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "kA505849kj9G8hG1R92ZD0qGYEoUr4TXXk8PxGBG5nQ=", + "shasum": "xJUmec7F6/It7kfSljqHWM76MSJSmytZlPvlgyLxxXI=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/interactive-ui/snap.manifest.json b/packages/examples/packages/interactive-ui/snap.manifest.json index c7c91722ae..23c09c39b3 100644 --- a/packages/examples/packages/interactive-ui/snap.manifest.json +++ b/packages/examples/packages/interactive-ui/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "9RyN2IznzsqwJO/7mhIKxp4DVQd0ongnYbN9MeFowI0=", + "shasum": "rvE1wmZmbTWiskg2VPVLbn8KT2PFkFQ5v3tGFqybsRE=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/invoke-snap/packages/consumer-signer/snap.manifest.json b/packages/examples/packages/invoke-snap/packages/consumer-signer/snap.manifest.json index 1cbf6a1e30..b6219626cd 100644 --- a/packages/examples/packages/invoke-snap/packages/consumer-signer/snap.manifest.json +++ b/packages/examples/packages/invoke-snap/packages/consumer-signer/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "IPgrx/srJ0pnYXbMpymuHMWWHMb9MZfyekCiiH8IDjQ=", + "shasum": "dsc+T0qqSAe1LoKYI/8HPVk3yBktRoaRSOvRMjlF5B8=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/invoke-snap/packages/core-signer/snap.manifest.json b/packages/examples/packages/invoke-snap/packages/core-signer/snap.manifest.json index e3272dd40a..d538a916e6 100644 --- a/packages/examples/packages/invoke-snap/packages/core-signer/snap.manifest.json +++ b/packages/examples/packages/invoke-snap/packages/core-signer/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "WFtsYTJglOJChmrR8AtwRRamhFXzrqmtyCizcg1zIK4=", + "shasum": "XZ4Pt2LJwOMXBm1wQXtJP3l/qZgC218Z/n3xYE8BGjM=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/json-rpc/snap.manifest.json b/packages/examples/packages/json-rpc/snap.manifest.json index 64b1a9718d..1f15ba024d 100644 --- a/packages/examples/packages/json-rpc/snap.manifest.json +++ b/packages/examples/packages/json-rpc/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "YDat1yOn2nLtxCIls+5lbNYnahRdV9EfASQI/b/9lhg=", + "shasum": "wokl2HTmrbZObmcc1MH9a7so9xrZkXNhRcCqtfepGA0=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/jsx/snap.manifest.json b/packages/examples/packages/jsx/snap.manifest.json index e0c590f6f3..bb82c77ab6 100644 --- a/packages/examples/packages/jsx/snap.manifest.json +++ b/packages/examples/packages/jsx/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "+Nb1THmpw/hGhx5AmYY8lJ8sdELuLKHv/n4ndO1WTCo=", + "shasum": "v2qKGCM4FTPM6wUB4lbKO556tDV00aYZDjd3RxvMcsI=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/lifecycle-hooks/snap.manifest.json b/packages/examples/packages/lifecycle-hooks/snap.manifest.json index 2309fb924a..88dc983401 100644 --- a/packages/examples/packages/lifecycle-hooks/snap.manifest.json +++ b/packages/examples/packages/lifecycle-hooks/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "Jrk/yg039/Qf51OfijNmqn+jA4+jS+DdbDTTXGK52t8=", + "shasum": "iyRAkal8Vql5iD1XJwIp/9KNi/kk3HjlfdfklQf3UW0=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/localization/snap.manifest.json b/packages/examples/packages/localization/snap.manifest.json index 0bda07313f..2330e551b1 100644 --- a/packages/examples/packages/localization/snap.manifest.json +++ b/packages/examples/packages/localization/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "iwYePZv3WxiRNwYWLJMYFVezr4FVNt9vKENR+l5cUv4=", + "shasum": "LofJCTQeoNFWE+F1S1OTGzcUyJPJiTskgK9aIRrt1NM=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/manage-state/snap.manifest.json b/packages/examples/packages/manage-state/snap.manifest.json index 22b8f89220..416a85065a 100644 --- a/packages/examples/packages/manage-state/snap.manifest.json +++ b/packages/examples/packages/manage-state/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "yfuwl/IJSpXoW950VQsJYOt5wugF4zqrwEJmHpfXdH0=", + "shasum": "f6IlmDjDvF8G3hEaIF8mrBl5irEpYgg6WPqpdk7P+is=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/network-access/snap.manifest.json b/packages/examples/packages/network-access/snap.manifest.json index a137982e73..6af024bf18 100644 --- a/packages/examples/packages/network-access/snap.manifest.json +++ b/packages/examples/packages/network-access/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "W6MFWW9qbN2L2Jrx49JjLzAvoiQA9uUcnWPTkNUFvE4=", + "shasum": "JtcrsUNAGdf3v4dr4uo1XQgom71wdtifM/rh54HgnEs=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/notifications/snap.manifest.json b/packages/examples/packages/notifications/snap.manifest.json index 748b123e3d..d9db789b5b 100644 --- a/packages/examples/packages/notifications/snap.manifest.json +++ b/packages/examples/packages/notifications/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "MZSLIUmv+W4OVk8BdXpbCsrkpKZ2DcXbyWCbje1bZjA=", + "shasum": "Nm2UUq7WZAa+oFy8y08o0bDHwLrVKpP00WW+F/UVw4o=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/rollup-plugin/snap.manifest.json b/packages/examples/packages/rollup-plugin/snap.manifest.json index 7da1f6816e..689f5119d6 100644 --- a/packages/examples/packages/rollup-plugin/snap.manifest.json +++ b/packages/examples/packages/rollup-plugin/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "CeDhOKcmnMXKwDdC2HozIABPQ+q/oVUFxrhiNNIclDc=", + "shasum": "DFfEzW8wLRohClvMLn+mN/pN+YmB39HgrDcQavUDISg=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/signature-insights/snap.manifest.json b/packages/examples/packages/signature-insights/snap.manifest.json index f390e3465c..a85625b34d 100644 --- a/packages/examples/packages/signature-insights/snap.manifest.json +++ b/packages/examples/packages/signature-insights/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "0eVnlZsCe7wuh964O9xM/hGczrpDZF7yekn9DZKgXNM=", + "shasum": "hsCvm1U9gV/4FzppkTRoVd8kjAmDHc1ar2KaV1SLUcM=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/transaction-insights/snap.manifest.json b/packages/examples/packages/transaction-insights/snap.manifest.json index 69e614b8ca..fdc321a4b3 100644 --- a/packages/examples/packages/transaction-insights/snap.manifest.json +++ b/packages/examples/packages/transaction-insights/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "sq30Pjd883FkDKbg1hAvud3vc0XD3MOsHT6vqpNh5kE=", + "shasum": "Vizjf/UbRArRlUspGuMRFkCZXEMwK20c/8wTCJhNSc0=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/wasm/snap.manifest.json b/packages/examples/packages/wasm/snap.manifest.json index 470a80a9ef..7abc742414 100644 --- a/packages/examples/packages/wasm/snap.manifest.json +++ b/packages/examples/packages/wasm/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "726OomzJecbcMjZqZEFPBdM+Zn49gOljb6pFJNOmSW0=", + "shasum": "I5yXL+XH56rFZiJfGc6Pq4v4lcXwuUIhkk/vbMbS5O4=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/webpack-plugin/snap.manifest.json b/packages/examples/packages/webpack-plugin/snap.manifest.json index 25c530dc63..7c5ba582e7 100644 --- a/packages/examples/packages/webpack-plugin/snap.manifest.json +++ b/packages/examples/packages/webpack-plugin/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "LuxkyGh4b6uksSaom6Yp4Uzezibjg9oskNh4L62vvMA=", + "shasum": "5hX1yMdOi/HxZesj7T4qXvR/Cu4GVpQHnfbavDPS45U=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/snaps-controllers/coverage.json b/packages/snaps-controllers/coverage.json index d03ef020c3..aa297f891c 100644 --- a/packages/snaps-controllers/coverage.json +++ b/packages/snaps-controllers/coverage.json @@ -1,5 +1,5 @@ { - "branches": 92.54, + "branches": 92.59, "functions": 96.91, "lines": 98.01, "statements": 97.71 diff --git a/packages/snaps-controllers/src/interface/utils.test.tsx b/packages/snaps-controllers/src/interface/utils.test.tsx index a8a698765d..10403584d5 100644 --- a/packages/snaps-controllers/src/interface/utils.test.tsx +++ b/packages/snaps-controllers/src/interface/utils.test.tsx @@ -12,6 +12,9 @@ import { Checkbox, RadioGroup, Radio, + Selector, + Card, + SelectorOption, } from '@metamask/snaps-sdk/jsx'; import { assertNameIsUnique, constructState, getJsxInterface } from './utils'; @@ -456,6 +459,94 @@ describe('constructState', () => { }); }); + it('sets default value for root level Selector', () => { + const element = ( + + + + + + + + + + + ); + + const result = constructState({}, element); + expect(result).toStrictEqual({ + foo: 'option1', + }); + }); + + it('supports root level Selector', () => { + const element = ( + + + + + + + + + + + ); + + const result = constructState({}, element); + expect(result).toStrictEqual({ + foo: 'option2', + }); + }); + + it('sets default value for Selector in form', () => { + const element = ( + +
+ + + + + + + + + + +
+
+ ); + + const result = constructState({}, element); + expect(result).toStrictEqual({ + form: { foo: 'option1' }, + }); + }); + + it('supports Selector in form', () => { + const element = ( + +
+ + + + + + + + + + +
+
+ ); + + const result = constructState({}, element); + expect(result).toStrictEqual({ + form: { foo: 'option2' }, + }); + }); + it('supports nested fields', () => { const element = ( diff --git a/packages/snaps-controllers/src/interface/utils.ts b/packages/snaps-controllers/src/interface/utils.ts index 1701b91f8f..24923138a4 100644 --- a/packages/snaps-controllers/src/interface/utils.ts +++ b/packages/snaps-controllers/src/interface/utils.ts @@ -15,6 +15,8 @@ import type { CheckboxElement, RadioGroupElement, RadioElement, + SelectorElement, + SelectorOptionElement, } from '@metamask/snaps-sdk/jsx'; import { isJSXElementUnsafe } from '@metamask/snaps-sdk/jsx'; import { @@ -63,7 +65,12 @@ export function assertNameIsUnique(state: InterfaceState, name: string) { * @returns The default state for the specific component, if any. */ function constructComponentSpecificDefaultState( - element: InputElement | DropdownElement | RadioGroupElement | CheckboxElement, + element: + | InputElement + | DropdownElement + | RadioGroupElement + | CheckboxElement + | SelectorElement, ) { switch (element.type) { case 'Dropdown': { @@ -76,6 +83,11 @@ function constructComponentSpecificDefaultState( return children[0]?.props.value; } + case 'Selector': { + const children = getJsxChildren(element) as SelectorOptionElement[]; + return children[0]?.props.value; + } + case 'Checkbox': return false; @@ -94,7 +106,12 @@ function constructComponentSpecificDefaultState( * @returns The state value for a given component. */ function getComponentStateValue( - element: InputElement | DropdownElement | RadioGroupElement | CheckboxElement, + element: + | InputElement + | DropdownElement + | RadioGroupElement + | CheckboxElement + | SelectorElement, ) { switch (element.type) { case 'Checkbox': @@ -120,7 +137,8 @@ function constructInputState( | DropdownElement | RadioGroupElement | FileInputElement - | CheckboxElement, + | CheckboxElement + | SelectorElement, form?: string, ) { const oldStateUnwrapped = form ? (oldState[form] as FormState) : oldState; @@ -177,7 +195,8 @@ export function constructState( component.type === 'Dropdown' || component.type === 'RadioGroup' || component.type === 'FileInput' || - component.type === 'Checkbox') + component.type === 'Checkbox' || + component.type === 'Selector') ) { const formState = newState[currentForm.name] as FormState; assertNameIsUnique(formState, component.props.name); @@ -195,7 +214,8 @@ export function constructState( component.type === 'Dropdown' || component.type === 'RadioGroup' || component.type === 'FileInput' || - component.type === 'Checkbox' + component.type === 'Checkbox' || + component.type === 'Selector' ) { assertNameIsUnique(newState, component.props.name); newState[component.props.name] = constructInputState(oldState, component); diff --git a/packages/snaps-sdk/src/jsx/components/form/Field.ts b/packages/snaps-sdk/src/jsx/components/form/Field.ts index b36a8e609d..933e27d0cb 100644 --- a/packages/snaps-sdk/src/jsx/components/form/Field.ts +++ b/packages/snaps-sdk/src/jsx/components/form/Field.ts @@ -5,6 +5,7 @@ import type { DropdownElement } from './Dropdown'; import type { FileInputElement } from './FileInput'; import type { InputElement } from './Input'; import type { RadioGroupElement } from './RadioGroup'; +import type { SelectorElement } from './Selector'; /** * The props of the {@link Field} component. @@ -22,7 +23,8 @@ export type FieldProps = { | RadioGroupElement | FileInputElement | InputElement - | CheckboxElement; + | CheckboxElement + | SelectorElement; }; const TYPE = 'Field'; diff --git a/packages/snaps-sdk/src/jsx/components/form/Selector.test.tsx b/packages/snaps-sdk/src/jsx/components/form/Selector.test.tsx new file mode 100644 index 0000000000..843025d3d0 --- /dev/null +++ b/packages/snaps-sdk/src/jsx/components/form/Selector.test.tsx @@ -0,0 +1,103 @@ +import { Card } from '../Card'; +import { Selector } from './Selector'; +import { SelectorOption } from './SelectorOption'; + +describe('Selector', () => { + it('renders a selector with options', () => { + const result = ( + + + + + + + + + ); + + expect(result).toStrictEqual({ + type: 'Selector', + props: { + name: 'selector', + value: 'foo', + title: 'Choose an option', + children: [ + { + type: 'SelectorOption', + props: { + value: 'foo', + children: { + type: 'Card', + props: { + title: 'Foo', + value: '$1', + }, + key: null, + }, + }, + key: null, + }, + { + type: 'SelectorOption', + props: { + value: 'bar', + children: { + type: 'Card', + props: { + title: 'Bar', + value: '$1', + }, + key: null, + }, + }, + key: null, + }, + ], + }, + key: null, + }); + }); + + it('renders a selector with a conditional option', () => { + const result = ( + + + + + {false && ( + + + + )} + + ); + + expect(result).toStrictEqual({ + type: 'Selector', + props: { + name: 'selector', + value: 'foo', + title: 'Choose an option', + children: [ + { + type: 'SelectorOption', + props: { + value: 'foo', + children: { + type: 'Card', + props: { + title: 'Foo', + value: '$1', + }, + key: null, + }, + }, + key: null, + }, + false, + ], + }, + key: null, + }); + }); +}); diff --git a/packages/snaps-sdk/src/jsx/components/form/Selector.ts b/packages/snaps-sdk/src/jsx/components/form/Selector.ts new file mode 100644 index 0000000000..67e8adb9c8 --- /dev/null +++ b/packages/snaps-sdk/src/jsx/components/form/Selector.ts @@ -0,0 +1,47 @@ +import type { SnapsChildren } from '../../component'; +import { createSnapComponent } from '../../component'; +import type { SelectorOptionElement } from './SelectorOption'; + +/** + * The props of the {@link Selector} component. + * + * @property name - The name of the selector. This is used to identify the + * state in the form data. + * @property title - The title of the selector. This is displayed in the UI. + * @property value - The selected value of the selector. + * @property children - The children of the selector. + */ +export type SelectorProps = { + name: string; + title: string; + value?: string | undefined; + children: SnapsChildren; +}; + +const TYPE = 'Selector'; + +/** + * A selector component, which is used to create a selector. + * + * @param props - The props of the component. + * @param props.name - The name of the selector field. This is used to identify the + * state in the form data. + * @param props.title - The title of the selector field. This is displayed in the UI. + * @param props.value - The selected value of the selector. + * @param props.children - The children of the selector. + * @returns A selector element. + * @example + * + * + * + * + * + */ +export const Selector = createSnapComponent(TYPE); + +/** + * A selector element. + * + * @see Selector + */ +export type SelectorElement = ReturnType; diff --git a/packages/snaps-sdk/src/jsx/components/form/SelectorOption.test.tsx b/packages/snaps-sdk/src/jsx/components/form/SelectorOption.test.tsx new file mode 100644 index 0000000000..65dd9aeb0a --- /dev/null +++ b/packages/snaps-sdk/src/jsx/components/form/SelectorOption.test.tsx @@ -0,0 +1,28 @@ +import { Card } from '../Card'; +import { SelectorOption } from './SelectorOption'; + +describe('Option', () => { + it('renders a selector option', () => { + const result = ( + + + + ); + + expect(result).toStrictEqual({ + type: 'SelectorOption', + props: { + value: 'foo', + children: { + type: 'Card', + props: { + title: 'Foo', + value: 'Bar', + }, + key: null, + }, + }, + key: null, + }); + }); +}); diff --git a/packages/snaps-sdk/src/jsx/components/form/SelectorOption.ts b/packages/snaps-sdk/src/jsx/components/form/SelectorOption.ts new file mode 100644 index 0000000000..67734f5716 --- /dev/null +++ b/packages/snaps-sdk/src/jsx/components/form/SelectorOption.ts @@ -0,0 +1,44 @@ +import { createSnapComponent } from '../../component'; +import type { CardElement } from '../Card'; + +/** + * The props of the {@link SelectorOption} component. + * + * @property value - The value of the selector option. This is used to populate the + * state in the form data. + * @property children - The component to display. + */ +export type SelectorOptionProps = { + value: string; + children: CardElement; +}; + +const TYPE = 'SelectorOption'; + +/** + * A selector option component, which is used to create a selector option. This component + * can only be used as a child of the {@link Selector} component. + * + * @param props - The props of the component. + * @param props.value - The value of the selector option. This is used to populate the + * state in the form data. + * @param props.children - The component to display. + * @returns A selector option element. + * @example + * + * + * + * + * + */ +export const SelectorOption = createSnapComponent< + SelectorOptionProps, + typeof TYPE +>(TYPE); + +/** + * A selector option element. + * + * @see SelectorOption + */ +export type SelectorOptionElement = ReturnType; diff --git a/packages/snaps-sdk/src/jsx/components/form/index.ts b/packages/snaps-sdk/src/jsx/components/form/index.ts index 8078353702..3dc4221376 100644 --- a/packages/snaps-sdk/src/jsx/components/form/index.ts +++ b/packages/snaps-sdk/src/jsx/components/form/index.ts @@ -8,6 +8,8 @@ import type { InputElement } from './Input'; import type { OptionElement } from './Option'; import type { RadioElement } from './Radio'; import type { RadioGroupElement } from './RadioGroup'; +import type { SelectorElement } from './Selector'; +import type { SelectorOptionElement } from './SelectorOption'; export * from './Button'; export * from './Checkbox'; @@ -19,6 +21,8 @@ export * from './Field'; export * from './FileInput'; export * from './Form'; export * from './Input'; +export * from './Selector'; +export * from './SelectorOption'; export type StandardFormElement = | ButtonElement @@ -30,4 +34,6 @@ export type StandardFormElement = | DropdownElement | OptionElement | RadioElement - | RadioGroupElement; + | RadioGroupElement + | SelectorElement + | SelectorOptionElement; diff --git a/packages/snaps-sdk/src/jsx/validation.test.tsx b/packages/snaps-sdk/src/jsx/validation.test.tsx index 713b5b1578..c9a6064536 100644 --- a/packages/snaps-sdk/src/jsx/validation.test.tsx +++ b/packages/snaps-sdk/src/jsx/validation.test.tsx @@ -29,6 +29,8 @@ import { Container, Card, Icon, + Selector, + SelectorOption, } from './components'; import { AddressStruct, @@ -63,6 +65,7 @@ import { TooltipStruct, ValueStruct, IconStruct, + SelectorStruct, } from './validation'; describe('KeyStruct', () => { @@ -248,6 +251,16 @@ describe('FieldStruct', () => { , + + + + + + + + + + , ])('validates a field element', (value) => { expect(is(value, FieldStruct)).toBe(true); }); @@ -794,6 +807,59 @@ describe('FileInputStruct', () => { }); }); +describe('SelectorStruct', () => { + it.each([ + + + + + + + + , + ])('validates a selector element', (value) => { + expect(is(value, SelectorStruct)).toBe(true); + }); + + it.each([ + 'foo', + 42, + null, + undefined, + {}, + [], + // @ts-expect-error - Invalid props. + foo, + // @ts-expect-error - Invalid props. + + + + + , + // @ts-expect-error - Invalid props. + + + + + , + // @ts-expect-error - Invalid props. + + + + + , + foo, + + foo + , + + alt + , + ])('does not validate "%p"', (value) => { + expect(is(value, SelectorStruct)).toBe(false); + }); +}); + describe('HeadingStruct', () => { it.each([Hello])( 'validates a heading element', diff --git a/packages/snaps-sdk/src/jsx/validation.ts b/packages/snaps-sdk/src/jsx/validation.ts index cd83d62173..dc03e09602 100644 --- a/packages/snaps-sdk/src/jsx/validation.ts +++ b/packages/snaps-sdk/src/jsx/validation.ts @@ -68,6 +68,8 @@ import { type ContainerElement, type FooterElement, type IconElement, + type SelectorElement, + type SelectorOptionElement, IconName, } from './components'; @@ -204,7 +206,7 @@ export const InputStruct: Describe = element('Input', { */ export const OptionStruct: Describe = element('Option', { value: string(), - children: string(), + children: nullUnion([string()]), }); /** @@ -216,6 +218,38 @@ export const DropdownStruct: Describe = element('Dropdown', { children: children([OptionStruct]), }); +/** + * A struct for the {@link CardElement} type. + */ +export const CardStruct: Describe = element('Card', { + image: optional(string()), + title: string(), + description: optional(string()), + value: string(), + extra: optional(string()), +}); + +/** + * A struct for the {@link SelectorOptionElement} type. + */ +export const SelectorOptionStruct: Describe = element( + 'SelectorOption', + { + value: string(), + children: CardStruct, + }, +); + +/** + * A struct for the {@link SelectorElement} type. + */ +export const SelectorStruct: Describe = element('Selector', { + name: string(), + title: string(), + value: optional(string()), + children: children([SelectorOptionStruct]), +}); + /** * A struct for the {@link RadioElement} type. */ @@ -265,12 +299,14 @@ const FIELD_CHILDREN_ARRAY = [ RadioGroupStruct, FileInputStruct, CheckboxStruct, + SelectorStruct, ] as [ typeof InputStruct, typeof DropdownStruct, typeof RadioGroupStruct, typeof FileInputStruct, typeof CheckboxStruct, + typeof SelectorStruct, ]; /** @@ -431,17 +467,6 @@ export const ValueStruct: Describe = element('Value', { extra: string(), }); -/** - * A struct for the {@link CardElement} type. - */ -export const CardStruct: Describe = element('Card', { - image: optional(string()), - title: string(), - description: optional(string()), - value: string(), - extra: optional(string()), -}); - /** * A struct for the {@link HeadingElement} type. */ @@ -563,6 +588,7 @@ export const BoxChildStruct = typedUnion([ CheckboxStruct, CardStruct, IconStruct, + SelectorStruct, ]); /** @@ -606,6 +632,8 @@ export const JSXElementStruct: Describe = typedUnion([ ContainerStruct, CardStruct, IconStruct, + SelectorStruct, + SelectorOptionStruct, ]); /**