diff --git a/packages/examples/packages/bip32/snap.manifest.json b/packages/examples/packages/bip32/snap.manifest.json
index 2a6b19fe5b..02bd6bc20a 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": "9wUdqSqbVJe+0aQ6y/LxGbJK0h4bVGVG8Y6gIAP5zyU=",
+ "shasum": "GVN0FD0c4xnr+osY4rjRvF/KSGBgVC2fccbLwzGoFnk=",
"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 fc263b7d46..e0331f508e 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": "9Ysf5NAslS75tHeB5y4T8SkAEChEx6m2Bf4bJdU6z84=",
+ "shasum": "j9j34JRDV5P98XgonRGfiqRSntlcaM4P53Z/S1g/XxA=",
"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 7f2c88349d..95871122d5 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": "d8uw9NVVT/AWUsXjh+nwUOLaAtifQXT+sCJ0gTqPVjc=",
+ "shasum": "AVhrthHcnFfQFZgbhwiIzjbqzRW7HWRiPKHkv+Fu+fA=",
"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 cdba9832df..4b593ee0e4 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": "xWwHeCztUjPa8YIFoCtTKf77qJczDgWw3v89S6ZJXwI=",
+ "shasum": "2FquW6bL4OaB6c97+QdgLoVQCBZv22QanqtU+LzmndE=",
"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 b418a1292c..d994247a46 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": "tgk38f1RctSTY3valbyPvAWMltiFhlInsHywZH/Bq+A=",
+ "shasum": "935lBT4lhnqdiUEnatcORNsAEjSfMRmMz0HT2KJK6yg=",
"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 cd16941744..1e24f7846e 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": "dF33PfyAEyIG+0x/1ggNeCScROxGheANb3U4k63pz0I=",
+ "shasum": "Ory0F194oSN15zWAVA5Dyvzpg2LFoWbi0ylfI2qLVz4=",
"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 273fd67d89..b658eee473 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": "7eaW1wlK1vuLEQoOyGfl4s6j7HvYwfN8lvhTim/ofUk=",
+ "shasum": "4a7IfQp8AuCTx4BLTRow8IfZA0BArTuEb9CT8FLa3zE=",
"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 ebfc5c462c..05cd07850a 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": "DYAfBoGmsxtJAY/473htgzk0a262nFgmdkhFcbfVQlA=",
+ "shasum": "25goBURWf+OrQTAgbwpuwN+TH/odY9fTHLDT/64oWvM=",
"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 9e272cbadf..8f0cef0389 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": "6aVCE+EKxHD9mcS5asPqN4ejs6mU3+rxd6qre4Hx4v8=",
+ "shasum": "vj40D2zP2bG8E7+ADfp8htEhYGgP1Z+8ThcvjyEoyOk=",
"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 392113413a..d1fbc7f2c8 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": "P+vBqlJolHAqbfdM9F7nGXjF/YR1bs+lthGTX4lwNX8=",
+ "shasum": "GHUM2ZesXFlhKOPU4mZ4Fty2qRln9aUuqHbVihttUKo=",
"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 4b40a2eafa..df9d4387fd 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": "HOT/KmUzzXrhnAI5PNYLapCB8kKoi9cE2ztzc8saaM4=",
+ "shasum": "ExzexStVLaw/chL4NySLzoH0UAHpw8oSmf7PjP/P1fY=",
"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 3f82d35e78..535489d2df 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": "rWABOhiFONdtEPQ6iFVw7XBVDswURwCNb2efK8oaBmE=",
+ "shasum": "FOe+60S6JcvSTyawpkPqd5ZsPmpqEWY1xG524iMMNKk=",
"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 9138e73575..075933027b 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": "7dHD4QcAJrRiowUw2LKyJDvNxz/2WN9jDPajCuHLtcg=",
+ "shasum": "ehTebDIqhb6jIhvvYnh8x6VLUmu9TmZLwGpWh5CmbMA=",
"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 dc7d4022ad..aae83bbae1 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": "2WrIpZaJtZdS9P0tcQn5pB5IvK61XLCArZVJtTRBfJo=",
+ "shasum": "NaByhc+fxmIG7PPdMjC6gH86rkKftx0/b3cwXc3XliA=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
diff --git a/packages/examples/packages/interactive-ui/src/components/InteractiveForm.tsx b/packages/examples/packages/interactive-ui/src/components/InteractiveForm.tsx
index 91158f938b..4a8780e011 100644
--- a/packages/examples/packages/interactive-ui/src/components/InteractiveForm.tsx
+++ b/packages/examples/packages/interactive-ui/src/components/InteractiveForm.tsx
@@ -6,6 +6,8 @@ import {
Heading,
Form,
Input,
+ Dropdown,
+ Option,
} from '@metamask/snaps-sdk/jsx';
export const InteractiveForm: SnapComponent = () => {
@@ -16,6 +18,13 @@ export const InteractiveForm: SnapComponent = () => {
+
+
+
+
+
+
+
diff --git a/packages/examples/packages/interactive-ui/src/index.test.tsx b/packages/examples/packages/interactive-ui/src/index.test.tsx
index 779e70d88f..afe94f80cd 100644
--- a/packages/examples/packages/interactive-ui/src/index.test.tsx
+++ b/packages/examples/packages/interactive-ui/src/index.test.tsx
@@ -43,12 +43,16 @@ describe('onRpcRequest', () => {
await formScreen.typeInField('example-input', 'foobar');
+ await formScreen.selectInDropdown('example-dropdown', 'option3');
+
await formScreen.clickElement('submit');
const resultScreen = await response.getInterface();
expect(resultScreen).toRender(
- ,
+ ,
);
await resultScreen.ok();
@@ -72,7 +76,7 @@ describe('onRpcRequest', () => {
const resultScreen = await response.getInterface();
expect(resultScreen).toRender(
- ,
+ ,
);
await resultScreen.ok();
@@ -93,12 +97,16 @@ describe('onHomePage', () => {
await formScreen.typeInField('example-input', 'foobar');
+ await formScreen.selectInDropdown('example-dropdown', 'option3');
+
await formScreen.clickElement('submit');
const resultScreen = response.getInterface();
expect(resultScreen).toRender(
- ,
+ ,
);
});
});
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 1524b1ce44..329969c409 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": "GgSToaZ1ZQ2EAUsER9Ipz7JjXiP34tAh8yHlSeZnrIw=",
+ "shasum": "dMnmXtz6qjVPnvfPCjLI3jcU/TiR3gN6qhp/y0La5uI=",
"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 fd0ac77c74..22a36779b0 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": "qRjdWlOFuznynW12exjTado0bOvmHuyD9tJ6EYllTfI=",
+ "shasum": "IR1h1od8fD/rVtnohzFC8zz4XP3S+K/KOYcCR/V4hOU=",
"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 e3e0795675..29b741462c 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": "ecGcLmxHbfbcggo+xXwKiKqn0jwvZpJBy9jzwbuhx14=",
+ "shasum": "Zh19stzSbkk4e0rl3mnZ69x6c1Ij+PEfJ3VGLcY1wEI=",
"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 277f76f48f..2b8316fd74 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": "HoevSNqYDSD1M1rbAEDLdad/KnqdeJCR/qVQpXoRrfI=",
+ "shasum": "yqVsBhPZiHirCgF8KkdbvA5uMKdP4JsYipRTGFQ4kf8=",
"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 409dc919ca..1571ab29b2 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": "hoY7FWJwVVeWRRz7u0VKyQTnMoLGLSa5aRfyDXPj9AM=",
+ "shasum": "ADZ5fA/qc6b9sLivha59OaZ8Q+/nvm2Br8QI8uTiMFQ=",
"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 da9bf5d660..8e4cbd49ad 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": "y+DckJzrDa6DAdy9g1K7sWK98XLi3b6byj6/SW+mvp0=",
+ "shasum": "K9QSwxnBwkz0Zf2tA8HWhNiuWJp6DGaD8UhgvtVtENE=",
"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 7f44e705c1..c2a229d320 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": "QRgU12P+k4MbET92wLU6RpueK9KYZb3HGItOH8pdXqo=",
+ "shasum": "g/Wi1iKD5OHqHBMHGAZJxx9BXJK3DVpt7401DP5vxiA=",
"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 5fc9965b1b..b3679eef98 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": "idx+4WZjLaansFM6HrxdUpXkhcr3EDknwrRF5xcDPL0=",
+ "shasum": "3rJJn2lKBzyAgCn9J3m6XflIryIAT4qMqyhmGIL/tnE=",
"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 7a8461bd6a..718189dd2b 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": "OozOdoPDn3miL7PzQHBhVDjbA8QULaNurUjXe8pEO5E=",
+ "shasum": "ta+iT3ThiGeJcfTy1DaDq5tOHJ6B7kweRWujb8hQzsc=",
"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 7c54e56ca6..7a130a83c4 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": "3NBcGdsi/8Tw7mYaRS+hhHcLMDbfxZmz6AUJHSPEJ1k=",
+ "shasum": "0cIQp4wDFLHahG8XiwmCcEcQc2ZiY0kBuOV0MBmc9yo=",
"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 baf469b636..cafdd10d55 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": "DqHMyRiHljEYgKJZXJaYhFNCeXZAlc2sCjyI8+7Vgkg=",
+ "shasum": "kSmphocy6yvQmcbeEAdVxF1WEEJw/wQbRyWEgViyYQo=",
"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 33fe04acc3..5cc2615256 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": "oo7KkCxPI9OXMaS+Ik9qdj4AulwiwTrSNCHtMe0WPP4=",
+ "shasum": "1IdcaOOx96qPQYfTZTRM7brevrP9jpCuxqxaGs0H+Bc=",
"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 43346be206..085602af06 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": "p6jcfAtN/r6hg314wA7Z9ihSAVP7+zm4KuZbB/Cu6+k=",
+ "shasum": "1PbMUDBzJvOO4kM1VIMsnJCXbQT5A3vt9Hh3gboTsyo=",
"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 21b47ef5b8..0451f3b5f5 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": "zgVFc93FtIDmzDPyP80bN1gTPgIE+uhPrABgpoP83Gs=",
+ "shasum": "Eli9yqWMDpM34s1qfBJ5vjBZfkerVJqJyCvgp1Xsmoo=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
diff --git a/packages/snaps-controllers/coverage.json b/packages/snaps-controllers/coverage.json
index 8b18decb67..cbf36ffd58 100644
--- a/packages/snaps-controllers/coverage.json
+++ b/packages/snaps-controllers/coverage.json
@@ -1,5 +1,5 @@
{
- "branches": 91.54,
+ "branches": 91.57,
"functions": 96.75,
"lines": 97.88,
"statements": 97.55
diff --git a/packages/snaps-controllers/src/interface/utils.test.tsx b/packages/snaps-controllers/src/interface/utils.test.tsx
index 95fb2a8b70..f0ee259f5e 100644
--- a/packages/snaps-controllers/src/interface/utils.test.tsx
+++ b/packages/snaps-controllers/src/interface/utils.test.tsx
@@ -1,4 +1,13 @@
-import { Box, Button, Field, Form, Input, Text } from '@metamask/snaps-sdk/jsx';
+import {
+ Box,
+ Button,
+ Dropdown,
+ Option,
+ Field,
+ Form,
+ Input,
+ Text,
+} from '@metamask/snaps-sdk/jsx';
import { assertNameIsUnique, constructState } from './utils';
@@ -223,6 +232,42 @@ describe('constructState', () => {
});
});
+ it('supports root level dropdowns', () => {
+ const element = (
+
+
+
+
+
+
+ );
+
+ const result = constructState({}, element);
+ expect(result).toStrictEqual({
+ foo: 'option2',
+ });
+ });
+
+ it('supports dropdowns in forms', () => {
+ const element = (
+
+
+
+ );
+
+ const result = constructState({}, element);
+ expect(result).toStrictEqual({
+ form: { foo: 'option2' },
+ });
+ });
+
it('deletes unused root level values', () => {
const element = (
diff --git a/packages/snaps-controllers/src/interface/utils.ts b/packages/snaps-controllers/src/interface/utils.ts
index 1c315f3964..3b5e54f0a0 100644
--- a/packages/snaps-controllers/src/interface/utils.ts
+++ b/packages/snaps-controllers/src/interface/utils.ts
@@ -7,6 +7,7 @@ import type {
} from '@metamask/snaps-sdk';
import type {
ButtonElement,
+ DropdownElement,
FieldElement,
InputElement,
JSXElement,
@@ -54,7 +55,10 @@ export function assertNameIsUnique(state: InterfaceState, name: string) {
* @param element - The input element.
* @returns The input state.
*/
-function constructInputState(oldState: InterfaceState, element: InputElement) {
+function constructInputState(
+ oldState: InterfaceState,
+ element: InputElement | DropdownElement,
+) {
return element.props.value ?? oldState[element.props.name] ?? null;
}
@@ -68,7 +72,7 @@ function constructInputState(oldState: InterfaceState, element: InputElement) {
*/
function constructFormInputState(
oldState: InterfaceState,
- component: InputElement,
+ component: InputElement | DropdownElement,
form: string,
) {
const oldFormState = oldState[form] as FormState;
@@ -159,7 +163,7 @@ export function constructState(
return newState;
}
- if (component.type === 'Input') {
+ if (component.type === 'Input' || component.type === 'Dropdown') {
assertNameIsUnique(newState, component.props.name);
newState[component.props.name] = constructInputState(oldState, component);
}
diff --git a/packages/snaps-jest/src/helpers.test.tsx b/packages/snaps-jest/src/helpers.test.tsx
index d074e26d92..faada8db17 100644
--- a/packages/snaps-jest/src/helpers.test.tsx
+++ b/packages/snaps-jest/src/helpers.test.tsx
@@ -403,6 +403,7 @@ describe('installSnap', () => {
content: Hello, world!,
clickElement: expect.any(Function),
typeInField: expect.any(Function),
+ selectInDropdown: expect.any(Function),
ok: expect.any(Function),
cancel: expect.any(Function),
});
@@ -461,6 +462,7 @@ describe('installSnap', () => {
content: Hello, world!,
clickElement: expect.any(Function),
typeInField: expect.any(Function),
+ selectInDropdown: expect.any(Function),
ok: expect.any(Function),
cancel: expect.any(Function),
});
@@ -518,6 +520,7 @@ describe('installSnap', () => {
content: Hello, world!,
clickElement: expect.any(Function),
typeInField: expect.any(Function),
+ selectInDropdown: expect.any(Function),
ok: expect.any(Function),
});
diff --git a/packages/snaps-jest/src/internals/request.test.ts b/packages/snaps-jest/src/internals/request.test.tsx
similarity index 85%
rename from packages/snaps-jest/src/internals/request.test.ts
rename to packages/snaps-jest/src/internals/request.test.tsx
index 9f9cb127a1..49abb716ed 100644
--- a/packages/snaps-jest/src/internals/request.test.ts
+++ b/packages/snaps-jest/src/internals/request.test.tsx
@@ -1,6 +1,7 @@
import { SnapInterfaceController } from '@metamask/snaps-controllers';
import type { SnapId } from '@metamask/snaps-sdk';
import { UserInputEventType, button, input, text } from '@metamask/snaps-sdk';
+import { Dropdown, Option } from '@metamask/snaps-sdk/jsx';
import { getJsxElementFromComponent, HandlerType } from '@metamask/snaps-utils';
import { MOCK_SNAP_ID } from '@metamask/snaps-utils/test-utils';
@@ -194,6 +195,7 @@ describe('getInterfaceApi', () => {
content: getJsxElementFromComponent(content),
clickElement: expect.any(Function),
typeInField: expect.any(Function),
+ selectInDropdown: expect.any(Function),
});
});
@@ -223,6 +225,7 @@ describe('getInterfaceApi', () => {
content: getJsxElementFromComponent(content),
clickElement: expect.any(Function),
typeInField: expect.any(Function),
+ selectInDropdown: expect.any(Function),
});
});
@@ -328,4 +331,57 @@ describe('getInterfaceApi', () => {
},
);
});
+
+ it('sends the request to the snap when using `selectInDropdown`', async () => {
+ const controllerMessenger = getRootControllerMessenger();
+
+ jest.spyOn(controllerMessenger, 'call');
+
+ // eslint-disable-next-line no-new
+ new SnapInterfaceController({
+ messenger:
+ getRestrictedSnapInterfaceControllerMessenger(controllerMessenger),
+ });
+
+ const content = (
+
+
+
+
+ );
+
+ const getInterface = await getInterfaceApi(
+ { content },
+ MOCK_SNAP_ID,
+ controllerMessenger,
+ );
+
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const snapInterface = getInterface!();
+
+ await snapInterface.selectInDropdown('foo', 'option2');
+
+ expect(controllerMessenger.call).toHaveBeenNthCalledWith(
+ 6,
+ 'ExecutionService:handleRpcRequest',
+ MOCK_SNAP_ID,
+ {
+ origin: '',
+ handler: HandlerType.OnUserInput,
+ request: {
+ jsonrpc: '2.0',
+ method: ' ',
+ params: {
+ event: {
+ type: UserInputEventType.InputChangeEvent,
+ name: 'foo',
+ value: 'option2',
+ },
+ id: expect.any(String),
+ context: null,
+ },
+ },
+ },
+ );
+ });
});
diff --git a/packages/snaps-jest/src/internals/request.ts b/packages/snaps-jest/src/internals/request.ts
index c34222fbf8..ee52b932e2 100644
--- a/packages/snaps-jest/src/internals/request.ts
+++ b/packages/snaps-jest/src/internals/request.ts
@@ -16,6 +16,7 @@ import {
getInterface,
getNotifications,
typeInField,
+ selectInDropdown,
} from './simulation';
import type { RunSagaFunction, Store } from './simulation';
import type { RootControllerMessenger } from './simulation/controllers';
@@ -190,6 +191,16 @@ export async function getInterfaceApi(
value,
);
},
+ selectInDropdown: async (name, value) => {
+ await selectInDropdown(
+ controllerMessenger,
+ interfaceId,
+ content,
+ snapId,
+ name,
+ value,
+ );
+ },
};
};
}
diff --git a/packages/snaps-jest/src/internals/simulation/interface.test.tsx b/packages/snaps-jest/src/internals/simulation/interface.test.tsx
index 3bf7fcbb92..bc47197e96 100644
--- a/packages/snaps-jest/src/internals/simulation/interface.test.tsx
+++ b/packages/snaps-jest/src/internals/simulation/interface.test.tsx
@@ -9,7 +9,14 @@ import {
panel,
text,
} from '@metamask/snaps-sdk';
-import { Button, Text } from '@metamask/snaps-sdk/jsx';
+import {
+ Button,
+ Text,
+ Dropdown,
+ Option,
+ Box,
+ Input,
+} from '@metamask/snaps-sdk/jsx';
import {
getJsxElementFromComponent,
HandlerType,
@@ -31,6 +38,7 @@ import {
getInterface,
getInterfaceResponse,
mergeValue,
+ selectInDropdown,
typeInField,
} from './interface';
import type { RunSagaFunction } from './store';
@@ -51,7 +59,11 @@ async function getResolve(runSaga: RunSagaFunction) {
}
describe('getInterfaceResponse', () => {
- const interfaceActions = { clickElement: jest.fn(), typeInField: jest.fn() };
+ const interfaceActions = {
+ clickElement: jest.fn(),
+ typeInField: jest.fn(),
+ selectInDropdown: jest.fn(),
+ };
it('returns an `ok` function that resolves the user interface with `null` for alert dialogs', async () => {
const { runSaga } = createStore(getMockOptions());
@@ -67,6 +79,7 @@ describe('getInterfaceResponse', () => {
content: foo,
clickElement: expect.any(Function),
typeInField: expect.any(Function),
+ selectInDropdown: expect.any(Function),
ok: expect.any(Function),
});
@@ -89,6 +102,7 @@ describe('getInterfaceResponse', () => {
content: foo,
clickElement: expect.any(Function),
typeInField: expect.any(Function),
+ selectInDropdown: expect.any(Function),
ok: expect.any(Function),
cancel: expect.any(Function),
});
@@ -113,6 +127,7 @@ describe('getInterfaceResponse', () => {
content: foo,
clickElement: expect.any(Function),
typeInField: expect.any(Function),
+ selectInDropdown: expect.any(Function),
ok: expect.any(Function),
cancel: expect.any(Function),
});
@@ -136,6 +151,7 @@ describe('getInterfaceResponse', () => {
content: foo,
clickElement: expect.any(Function),
typeInField: expect.any(Function),
+ selectInDropdown: expect.any(Function),
ok: expect.any(Function),
cancel: expect.any(Function),
});
@@ -159,6 +175,7 @@ describe('getInterfaceResponse', () => {
content: foo,
clickElement: expect.any(Function),
typeInField: expect.any(Function),
+ selectInDropdown: expect.any(Function),
ok: expect.any(Function),
cancel: expect.any(Function),
});
@@ -183,6 +200,7 @@ describe('getInterfaceResponse', () => {
content: foo,
clickElement: expect.any(Function),
typeInField: expect.any(Function),
+ selectInDropdown: expect.any(Function),
ok: expect.any(Function),
cancel: expect.any(Function),
});
@@ -547,6 +565,148 @@ describe('typeInField', () => {
});
});
+describe('selectInDropdown', () => {
+ const rootControllerMessenger = getRootControllerMessenger();
+ const controllerMessenger = getRestrictedSnapInterfaceControllerMessenger(
+ rootControllerMessenger,
+ );
+
+ const interfaceController = new SnapInterfaceController({
+ messenger: controllerMessenger,
+ });
+
+ const handleRpcRequestMock = jest.fn();
+
+ rootControllerMessenger.registerActionHandler(
+ 'ExecutionService:handleRpcRequest',
+ handleRpcRequestMock,
+ );
+
+ it('updates the interface state and sends an InputChangeEvent', async () => {
+ jest.spyOn(rootControllerMessenger, 'call');
+
+ const content = (
+
+
+
+
+ );
+
+ const interfaceId = await interfaceController.createInterface(
+ MOCK_SNAP_ID,
+ content,
+ );
+
+ await selectInDropdown(
+ rootControllerMessenger,
+ interfaceId,
+ content,
+ MOCK_SNAP_ID,
+ 'foo',
+ 'option2',
+ );
+
+ expect(rootControllerMessenger.call).toHaveBeenCalledWith(
+ 'SnapInterfaceController:updateInterfaceState',
+ interfaceId,
+ { foo: 'option2' },
+ );
+
+ expect(handleRpcRequestMock).toHaveBeenCalledWith(MOCK_SNAP_ID, {
+ origin: '',
+ handler: HandlerType.OnUserInput,
+ request: {
+ jsonrpc: '2.0',
+ method: ' ',
+ params: {
+ event: {
+ type: UserInputEventType.InputChangeEvent,
+ name: 'foo',
+ value: 'option2',
+ },
+ id: interfaceId,
+ context: null,
+ },
+ },
+ });
+ });
+
+ it('throws if selected option does not exist', async () => {
+ const content = (
+
+
+
+
+ );
+
+ const interfaceId = await interfaceController.createInterface(
+ MOCK_SNAP_ID,
+ content,
+ );
+
+ await expect(
+ selectInDropdown(
+ rootControllerMessenger,
+ interfaceId,
+ content,
+ MOCK_SNAP_ID,
+ 'foo',
+ 'option3',
+ ),
+ ).rejects.toThrow(
+ 'The dropdown with the name "foo" does not contain "option3"',
+ );
+ });
+
+ it('throws if there is no dropdowns in the interface', async () => {
+ const content = (
+
+ Foo
+
+ );
+
+ const interfaceId = await interfaceController.createInterface(
+ MOCK_SNAP_ID,
+ content,
+ );
+
+ await expect(
+ selectInDropdown(
+ rootControllerMessenger,
+ interfaceId,
+ content,
+ MOCK_SNAP_ID,
+ 'bar',
+ 'baz',
+ ),
+ ).rejects.toThrow(
+ 'Could not find an element in the interface with the name "bar".',
+ );
+ });
+
+ it('throws if the element is not a dropdown', async () => {
+ const content = ;
+
+ const interfaceId = await interfaceController.createInterface(
+ MOCK_SNAP_ID,
+ content,
+ );
+
+ await expect(
+ selectInDropdown(
+ rootControllerMessenger,
+ interfaceId,
+ content,
+ MOCK_SNAP_ID,
+ 'foo',
+ 'baz',
+ ),
+ ).rejects.toThrow(
+ 'Expected an element of type "Dropdown", but found "Input".',
+ );
+ });
+});
+
describe('getInterface', () => {
const rootControllerMessenger = getRootControllerMessenger();
const controllerMessenger = getRestrictedSnapInterfaceControllerMessenger(
@@ -577,6 +737,7 @@ describe('getInterface', () => {
content: getJsxElementFromComponent(content),
clickElement: expect.any(Function),
typeInField: expect.any(Function),
+ selectInDropdown: expect.any(Function),
ok: expect.any(Function),
});
});
@@ -603,6 +764,7 @@ describe('getInterface', () => {
content: getJsxElementFromComponent(content),
clickElement: expect.any(Function),
typeInField: expect.any(Function),
+ selectInDropdown: expect.any(Function),
ok: expect.any(Function),
});
});
@@ -691,4 +853,52 @@ describe('getInterface', () => {
},
);
});
+
+ it('sends a request to the snap when `selectInDropdown` is called', async () => {
+ jest.spyOn(rootControllerMessenger, 'call');
+ const { store, runSaga } = createStore(getMockOptions());
+
+ const content = (
+
+
+
+
+ );
+ const id = await interfaceController.createInterface(MOCK_SNAP_ID, content);
+ const type = DialogType.Alert;
+ const ui = { type, id };
+
+ store.dispatch(setInterface(ui));
+
+ const result = await runSaga(
+ getInterface,
+ runSaga,
+ MOCK_SNAP_ID,
+ rootControllerMessenger,
+ ).toPromise();
+
+ await result.selectInDropdown('foo', 'option2');
+
+ expect(rootControllerMessenger.call).toHaveBeenCalledWith(
+ 'ExecutionService:handleRpcRequest',
+ MOCK_SNAP_ID,
+ {
+ origin: '',
+ handler: HandlerType.OnUserInput,
+ request: {
+ jsonrpc: '2.0',
+ method: ' ',
+ params: {
+ event: {
+ type: UserInputEventType.InputChangeEvent,
+ name: 'foo',
+ value: 'option2',
+ },
+ id,
+ context: null,
+ },
+ },
+ },
+ );
+ });
});
diff --git a/packages/snaps-jest/src/internals/simulation/interface.ts b/packages/snaps-jest/src/internals/simulation/interface.ts
index 1eb75211e9..f392a14fe7 100644
--- a/packages/snaps-jest/src/internals/simulation/interface.ts
+++ b/packages/snaps-jest/src/internals/simulation/interface.ts
@@ -7,7 +7,12 @@ import type {
} from '@metamask/snaps-sdk';
import { DialogType, UserInputEventType, assert } from '@metamask/snaps-sdk';
import type { FormElement, JSXElement } from '@metamask/snaps-sdk/jsx';
-import { HandlerType, unwrapError, walkJsx } from '@metamask/snaps-utils';
+import {
+ HandlerType,
+ getJsxChildren,
+ unwrapError,
+ walkJsx,
+} from '@metamask/snaps-utils';
import { hasProperty } from '@metamask/utils';
import type { PayloadAction } from '@reduxjs/toolkit';
import { type SagaIterator } from 'redux-saga';
@@ -415,6 +420,80 @@ export async function typeInField(
});
}
+/**
+ * Type a value in an interface element.
+ *
+ * @param controllerMessenger - The controller messenger used to call actions.
+ * @param id - The interface ID.
+ * @param content - The interface Components.
+ * @param snapId - The Snap ID.
+ * @param name - The element name.
+ * @param value - The value to type in the element.
+ */
+export async function selectInDropdown(
+ controllerMessenger: RootControllerMessenger,
+ id: string,
+ content: JSXElement,
+ snapId: SnapId,
+ name: string,
+ value: string,
+) {
+ const result = getElement(content, name);
+
+ assert(
+ result !== undefined,
+ `Could not find an element in the interface with the name "${name}".`,
+ );
+
+ assert(
+ result.element.type === 'Dropdown',
+ `Expected an element of type "Dropdown", but found "${result.element.type}".`,
+ );
+
+ const options = getJsxChildren(result.element) as JSXElement[];
+ const selectedOption = options.find(
+ (option) =>
+ hasProperty(option.props, 'value') && option.props.value === value,
+ );
+
+ assert(
+ selectedOption !== undefined,
+ `The dropdown with the name "${name}" does not contain "${value}".`,
+ );
+
+ const { state, context } = controllerMessenger.call(
+ 'SnapInterfaceController:getInterface',
+ snapId,
+ id,
+ );
+
+ const newState = mergeValue(state, name, value, result.form);
+
+ controllerMessenger.call(
+ 'SnapInterfaceController:updateInterfaceState',
+ id,
+ newState,
+ );
+
+ await controllerMessenger.call('ExecutionService:handleRpcRequest', snapId, {
+ origin: '',
+ handler: HandlerType.OnUserInput,
+ request: {
+ jsonrpc: '2.0',
+ method: ' ',
+ params: {
+ event: {
+ type: UserInputEventType.InputChangeEvent,
+ name: result.element.props.name,
+ value,
+ },
+ id,
+ context,
+ },
+ },
+ });
+}
+
/**
* Get a user interface object from a Snap.
*
@@ -442,6 +521,16 @@ export function* getInterface(
typeInField: async (name: string, value: string) => {
await typeInField(controllerMessenger, id, content, snapId, name, value);
},
+ selectInDropdown: async (name: string, value: string) => {
+ await selectInDropdown(
+ controllerMessenger,
+ id,
+ content,
+ snapId,
+ name,
+ value,
+ );
+ },
};
return getInterfaceResponse(runSaga, type, content, interfaceActions);
diff --git a/packages/snaps-jest/src/types.ts b/packages/snaps-jest/src/types.ts
index e3b8fbda83..bdf3652f31 100644
--- a/packages/snaps-jest/src/types.ts
+++ b/packages/snaps-jest/src/types.ts
@@ -96,6 +96,14 @@ export type SnapInterfaceActions = {
* @param value - The value to type.
*/
typeInField(name: string, value: string): Promise;
+
+ /**
+ * Select an option with a value in a dropdown.
+ *
+ * @param name - The element name to type in.
+ * @param value - The value to type.
+ */
+ selectInDropdown(name: string, value: string): Promise;
};
/**
diff --git a/packages/snaps-sdk/src/jsx/components/form/Dropdown.ts b/packages/snaps-sdk/src/jsx/components/form/Dropdown.ts
new file mode 100644
index 0000000000..fa133c526e
--- /dev/null
+++ b/packages/snaps-sdk/src/jsx/components/form/Dropdown.ts
@@ -0,0 +1,45 @@
+import type { MaybeArray } from '../../component';
+import { createSnapComponent } from '../../component';
+import type { OptionElement } from './Option';
+
+/**
+ * The props of the {@link Dropdown} component.
+ *
+ * @property name - The name of the dropdown. This is used to identify the
+ * state in the form data.
+ * @property value - The selected value of the dropdown.
+ * @property children - The children of the dropdown.
+ */
+type DropdownProps = {
+ name: string;
+ value?: string;
+ children: MaybeArray;
+};
+
+const TYPE = 'Dropdown';
+
+/**
+ * A dropdown component, which is used to create a dropdown. This component
+ * can only be used as a child of the {@link Field} component.
+ *
+ * @param props - The props of the component.
+ * @param props.name - The name of the dropdown field. This is used to identify the
+ * state in the form data.
+ * @param props.value - The selected value of the dropdown.
+ * @param props.children - The children of the dropdown.
+ * @returns A dropdown element.
+ * @example
+ *
+ *
+ *
+ *
+ *
+ */
+export const Dropdown = createSnapComponent(TYPE);
+
+/**
+ * A dropdown element.
+ *
+ * @see Dropdown
+ */
+export type DropdownElement = ReturnType;
diff --git a/packages/snaps-sdk/src/jsx/components/form/Field.test.tsx b/packages/snaps-sdk/src/jsx/components/form/Field.test.tsx
index ee4183f98e..0ff73f6aaf 100644
--- a/packages/snaps-sdk/src/jsx/components/form/Field.test.tsx
+++ b/packages/snaps-sdk/src/jsx/components/form/Field.test.tsx
@@ -1,6 +1,8 @@
import { Button } from './Button';
+import { Dropdown } from './Dropdown';
import { Field } from './Field';
import { Input } from './Input';
+import { Option } from './Option';
describe('Field', () => {
it('renders a field element', () => {
@@ -86,4 +88,48 @@ describe('Field', () => {
},
});
});
+
+ it('renders a dropdown element', () => {
+ const result = (
+
+
+
+
+
+
+ );
+
+ expect(result).toStrictEqual({
+ type: 'Field',
+ key: null,
+ props: {
+ label: 'Label',
+ children: {
+ type: 'Dropdown',
+ key: null,
+ props: {
+ name: 'foo',
+ children: [
+ {
+ type: 'Option',
+ key: null,
+ props: {
+ children: 'Option 1',
+ value: 'option1',
+ },
+ },
+ {
+ type: 'Option',
+ key: null,
+ props: {
+ children: 'Option 2',
+ value: 'option2',
+ },
+ },
+ ],
+ },
+ },
+ },
+ });
+ });
});
diff --git a/packages/snaps-sdk/src/jsx/components/form/Field.ts b/packages/snaps-sdk/src/jsx/components/form/Field.ts
index e7d9fba7be..d149bc67c8 100644
--- a/packages/snaps-sdk/src/jsx/components/form/Field.ts
+++ b/packages/snaps-sdk/src/jsx/components/form/Field.ts
@@ -1,5 +1,6 @@
import { createSnapComponent } from '../../component';
import type { ButtonElement } from './Button';
+import type { DropdownElement } from './Dropdown';
import type { InputElement } from './Input';
/**
@@ -12,7 +13,7 @@ import type { InputElement } from './Input';
export type FieldProps = {
label?: string | undefined;
error?: string | undefined;
- children: [InputElement, ButtonElement] | InputElement;
+ children: [InputElement, ButtonElement] | InputElement | DropdownElement;
};
const TYPE = 'Field';
diff --git a/packages/snaps-sdk/src/jsx/components/form/Option.ts b/packages/snaps-sdk/src/jsx/components/form/Option.ts
new file mode 100644
index 0000000000..91b6484ad2
--- /dev/null
+++ b/packages/snaps-sdk/src/jsx/components/form/Option.ts
@@ -0,0 +1,40 @@
+import { createSnapComponent } from '../../component';
+
+/**
+ * The props of the {@link Option} component.
+ *
+ * @property value - The value of the dropdown option. This is used to populate the
+ * state in the form data.
+ * @property children - The text to display.
+ */
+type OptionProps = {
+ value: string;
+ children: string;
+};
+
+const TYPE = 'Option';
+
+/**
+ * A dropdown option component, which is used to create a dropdown option. This component
+ * can only be used as a child of the {@link Dropdown} component.
+ *
+ * @param props - The props of the component.
+ * @param props.value - The value of the dropdown option. This is used to populate the
+ * state in the form data.
+ * @param props.children - The text to display.
+ * @returns A dropdown option element.
+ * @example
+ *
+ *
+ *
+ *
+ *
+ */
+export const Option = createSnapComponent(TYPE);
+
+/**
+ * A dropdown option element.
+ *
+ * @see Option
+ */
+export type OptionElement = ReturnType;
diff --git a/packages/snaps-sdk/src/jsx/components/form/index.ts b/packages/snaps-sdk/src/jsx/components/form/index.ts
index 8947b303e8..bfc310a0cd 100644
--- a/packages/snaps-sdk/src/jsx/components/form/index.ts
+++ b/packages/snaps-sdk/src/jsx/components/form/index.ts
@@ -1,9 +1,13 @@
import type { ButtonElement } from './Button';
+import type { DropdownElement } from './Dropdown';
import type { FieldElement } from './Field';
import type { FormElement } from './Form';
import type { InputElement } from './Input';
+import type { OptionElement } from './Option';
export * from './Button';
+export * from './Dropdown';
+export * from './Option';
export * from './Field';
export * from './Form';
export * from './Input';
@@ -12,4 +16,6 @@ export type StandardFormElement =
| ButtonElement
| FormElement
| FieldElement
- | InputElement;
+ | InputElement
+ | DropdownElement
+ | OptionElement;
diff --git a/packages/snaps-sdk/src/jsx/validation.test.tsx b/packages/snaps-sdk/src/jsx/validation.test.tsx
index 36208deff9..c1ec1b897f 100644
--- a/packages/snaps-sdk/src/jsx/validation.test.tsx
+++ b/packages/snaps-sdk/src/jsx/validation.test.tsx
@@ -7,6 +7,8 @@ import {
Button,
Copyable,
Divider,
+ Dropdown,
+ Option,
Field,
Form,
Heading,
@@ -26,6 +28,7 @@ import {
ButtonStruct,
CopyableStruct,
DividerStruct,
+ DropdownStruct,
ElementStruct,
FieldStruct,
FormStruct,
@@ -205,6 +208,12 @@ describe('FieldStruct', () => {
,
+
+
+
+
+
+ ,
])('validates a field element', (value) => {
expect(is(value, FieldStruct)).toBe(true);
});
@@ -696,6 +705,37 @@ describe('SpinnerStruct', () => {
});
});
+describe('Dropdown', () => {
+ it.each([
+
+
+
+ ,
+ ])('validates a dropdown element', (value) => {
+ expect(is(value, DropdownStruct)).toBe(true);
+ });
+
+ it.each([
+ 'foo',
+ 42,
+ null,
+ undefined,
+ {},
+ [],
+ // @ts-expect-error - Invalid props.
+ foo,
+ foo,
+
+ foo
+ ,
+
+
+
,
+ ])('does not validate "%p"', (value) => {
+ expect(is(value, DropdownStruct)).toBe(false);
+ });
+});
+
describe('isJSXElement', () => {
it.each([
foo,
diff --git a/packages/snaps-sdk/src/jsx/validation.ts b/packages/snaps-sdk/src/jsx/validation.ts
index 1e3792f8e6..3c97c70f20 100644
--- a/packages/snaps-sdk/src/jsx/validation.ts
+++ b/packages/snaps-sdk/src/jsx/validation.ts
@@ -38,6 +38,8 @@ import type {
ButtonElement,
CopyableElement,
DividerElement,
+ DropdownElement,
+ OptionElement,
FieldElement,
FormElement,
HeadingElement,
@@ -126,13 +128,34 @@ export const InputStruct: Describe = element('Input', {
placeholder: optional(string()),
});
+/**
+ * A struct for the {@link OptionElement} type.
+ */
+export const OptionStruct: Describe = element('Option', {
+ value: string(),
+ children: string(),
+});
+
+/**
+ * A struct for the {@link DropdownElement} type.
+ */
+export const DropdownStruct: Describe = element('Dropdown', {
+ name: string(),
+ value: optional(string()),
+ children: maybeArray(OptionStruct),
+});
+
/**
* A struct for the {@link FieldElement} type.
*/
export const FieldStruct: Describe = element('Field', {
label: optional(string()),
error: optional(string()),
- children: nullUnion([tuple([InputStruct, ButtonStruct]), InputStruct]),
+ children: nullUnion([
+ tuple([InputStruct, ButtonStruct]),
+ InputStruct,
+ DropdownStruct,
+ ]),
});
/**
@@ -291,6 +314,7 @@ export const BoxChildStruct = nullUnion([
RowStruct,
SpinnerStruct,
TextStruct,
+ DropdownStruct,
]);
/**
@@ -319,6 +343,8 @@ export const JSXElementStruct: Describe = nullUnion([
RowStruct,
SpinnerStruct,
TextStruct,
+ DropdownStruct,
+ OptionStruct,
]);
/**