diff --git a/packages/examples/packages/browserify-plugin/snap.manifest.json b/packages/examples/packages/browserify-plugin/snap.manifest.json index 0d829905dc..ac37af9969 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": "ssGPi4fxyZSvKOMXbVdOa/vnTx0qrq6WZWCUV91dLqo=", + "shasum": "BiCtwmtmQRs6IrssjgH8PXZWH7afx75+RKXMnl9KDZ8=", "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 1376a8e7eb..e30bb3119f 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": "+342Ghzfo9UpTxJgqIPOieHvqRTnf4h00s8r0DpbozY=", + "shasum": "9K88gT7CbGCXTc/Qx63zbS91VQtHTkFFXWLjyXoM6YU=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/snaps-rpc-methods/src/permitted/createInterface.test.tsx b/packages/snaps-rpc-methods/src/permitted/createInterface.test.tsx index 29ec7d0b25..1403fa95a3 100644 --- a/packages/snaps-rpc-methods/src/permitted/createInterface.test.tsx +++ b/packages/snaps-rpc-methods/src/permitted/createInterface.test.tsx @@ -141,7 +141,7 @@ describe('snap_createInterface', () => { error: { code: -32602, message: - 'Invalid params: At path: ui -- Expected type to be one of: "Address", "Bold", "Box", "Button", "Copyable", "Divider", "Dropdown", "RadioGroup", "Field", "FileInput", "Form", "Heading", "Input", "Image", "Italic", "Link", "Row", "Spinner", "Text", "Tooltip", "Checkbox", "Card", "Icon", "Selector", "Section", "Avatar", "Banner", "Container", but received: undefined.', + 'Invalid params: At path: ui -- Expected type to be one of: "Address", "Bold", "Box", "Button", "Copyable", "Divider", "Dropdown", "RadioGroup", "Field", "FileInput", "Form", "Heading", "Input", "Image", "Italic", "Link", "Row", "Spinner", "Text", "Tooltip", "Checkbox", "Card", "Icon", "Selector", "Section", "Avatar", "Banner", "Skeleton", "Container", but received: undefined.', stack: expect.any(String), }, id: 1, diff --git a/packages/snaps-sdk/src/internals/structs.test.ts b/packages/snaps-sdk/src/internals/structs.test.ts index 2e7a007274..0c20e92ce6 100644 --- a/packages/snaps-sdk/src/internals/structs.test.ts +++ b/packages/snaps-sdk/src/internals/structs.test.ts @@ -66,7 +66,7 @@ describe('typedUnion', () => { const result = validate(Text({}), unionStruct); expect(result[0]?.message).toBe( - 'At path: props.children -- Expected type to be one of: "Bold", "Italic", "Link", "Icon", but received: undefined', + 'At path: props.children -- Expected type to be one of: "Bold", "Italic", "Link", "Icon", "Skeleton", but received: undefined', ); }); @@ -75,7 +75,7 @@ describe('typedUnion', () => { const result = validate(Text({}), nestedUnionStruct); expect(result[0]?.message).toBe( - 'At path: props.children -- Expected type to be one of: "Bold", "Italic", "Link", "Icon", but received: undefined', + 'At path: props.children -- Expected type to be one of: "Bold", "Italic", "Link", "Icon", "Skeleton", but received: undefined', ); }); diff --git a/packages/snaps-sdk/src/jsx/components/Banner.ts b/packages/snaps-sdk/src/jsx/components/Banner.ts index a558d90b17..91cbe9cf66 100644 --- a/packages/snaps-sdk/src/jsx/components/Banner.ts +++ b/packages/snaps-sdk/src/jsx/components/Banner.ts @@ -3,6 +3,7 @@ import type { ButtonElement } from './form/Button'; import type { StandardFormattingElement } from './formatting'; import type { IconElement } from './Icon'; import type { LinkElement } from './Link'; +import type { SkeletonElement } from './Skeleton'; import type { TextElement } from './Text'; /** @@ -14,6 +15,7 @@ export type BannerChildren = SnapsChildren< | LinkElement | IconElement | ButtonElement + | SkeletonElement >; /** diff --git a/packages/snaps-sdk/src/jsx/components/Image.ts b/packages/snaps-sdk/src/jsx/components/Image.ts index e81821f225..ae4e2fcf36 100644 --- a/packages/snaps-sdk/src/jsx/components/Image.ts +++ b/packages/snaps-sdk/src/jsx/components/Image.ts @@ -1,4 +1,5 @@ import { createSnapComponent } from '../component'; +import type { BorderRadius } from './utils'; /** * The props of the {@link Image} component. @@ -12,7 +13,7 @@ import { createSnapComponent } from '../component'; type ImageProps = { src: string; alt?: string | undefined; - borderRadius?: 'none' | 'medium' | 'full' | undefined; + borderRadius?: BorderRadius | undefined; }; const TYPE = 'Image'; diff --git a/packages/snaps-sdk/src/jsx/components/Row.ts b/packages/snaps-sdk/src/jsx/components/Row.ts index b64511150f..44dc409f24 100644 --- a/packages/snaps-sdk/src/jsx/components/Row.ts +++ b/packages/snaps-sdk/src/jsx/components/Row.ts @@ -2,6 +2,7 @@ import { createSnapComponent } from '../component'; import type { AddressElement } from './Address'; import type { ImageElement } from './Image'; import type { LinkElement } from './Link'; +import type { SkeletonElement } from './Skeleton'; import type { TextElement } from './Text'; import type { ValueElement } from './Value'; @@ -13,7 +14,8 @@ export type RowChildren = | ImageElement | TextElement | ValueElement - | LinkElement; + | LinkElement + | SkeletonElement; /** * The props of the {@link Row} component. diff --git a/packages/snaps-sdk/src/jsx/components/Skeleton.test.tsx b/packages/snaps-sdk/src/jsx/components/Skeleton.test.tsx new file mode 100644 index 0000000000..6869203534 --- /dev/null +++ b/packages/snaps-sdk/src/jsx/components/Skeleton.test.tsx @@ -0,0 +1,17 @@ +import { Skeleton } from './Skeleton'; + +describe('Skeleton', () => { + it('renders a skeleton component', () => { + const result = ; + + expect(result).toStrictEqual({ + type: 'Skeleton', + key: null, + props: { + width: 320, + height: 32, + borderRadius: 'medium', + }, + }); + }); +}); diff --git a/packages/snaps-sdk/src/jsx/components/Skeleton.ts b/packages/snaps-sdk/src/jsx/components/Skeleton.ts new file mode 100644 index 0000000000..17510f5f15 --- /dev/null +++ b/packages/snaps-sdk/src/jsx/components/Skeleton.ts @@ -0,0 +1,36 @@ +import { createSnapComponent } from '../component'; +import type { BorderRadius } from './utils'; + +/** + * The props of the {@link Skeleton} component. + * + * @param width - Width of the Skeleton. + * @param width - Height of the Skeleton. + * @param borderRadius - Border radius of the Skeleton. + */ +export type SkeletonProps = { + width?: number | string | undefined; + height?: number | string | undefined; + borderRadius?: BorderRadius | undefined; +}; + +const TYPE = 'Skeleton'; + +/** + * A Skeleton component, which is used to display skeleton of loading content. + * + * @param props - The props of the component. + * @param props.width - Width of the Skeleton. + * @param props.width - Height of the Skeleton. + * @param props.borderRadius - Border radius of the Skeleton. + * @example + * + */ +export const Skeleton = createSnapComponent(TYPE); + +/** + * A Skeleton element. + * + * @see Skeleton + */ +export type SkeletonElement = ReturnType; diff --git a/packages/snaps-sdk/src/jsx/components/Text.ts b/packages/snaps-sdk/src/jsx/components/Text.ts index 9994713b2c..b7a6b81d79 100644 --- a/packages/snaps-sdk/src/jsx/components/Text.ts +++ b/packages/snaps-sdk/src/jsx/components/Text.ts @@ -3,12 +3,17 @@ import { createSnapComponent } from '../component'; import type { StandardFormattingElement } from './formatting'; import type { IconElement } from './Icon'; import type { LinkElement } from './Link'; +import type { SkeletonElement } from './Skeleton'; /** * The children of the {@link Text} component. */ export type TextChildren = SnapsChildren< - string | StandardFormattingElement | LinkElement | IconElement + | string + | StandardFormattingElement + | LinkElement + | IconElement + | SkeletonElement >; /** diff --git a/packages/snaps-sdk/src/jsx/components/index.ts b/packages/snaps-sdk/src/jsx/components/index.ts index d26cbcd338..2f4d9521ba 100644 --- a/packages/snaps-sdk/src/jsx/components/index.ts +++ b/packages/snaps-sdk/src/jsx/components/index.ts @@ -15,6 +15,7 @@ import type { ImageElement } from './Image'; import type { LinkElement } from './Link'; import type { RowElement } from './Row'; import type { SectionElement } from './Section'; +import type { SkeletonElement } from './Skeleton'; import type { SpinnerElement } from './Spinner'; import type { TextElement } from './Text'; import type { TooltipElement } from './Tooltip'; @@ -41,6 +42,8 @@ export * from './Footer'; export * from './Container'; export * from './Section'; export * from './Banner'; +export * from './Skeleton'; +export * from './utils'; /** * A built-in JSX element, which can be used in a Snap user interface. @@ -66,4 +69,5 @@ export type JSXElement = | SpinnerElement | TextElement | TooltipElement - | BannerElement; + | BannerElement + | SkeletonElement; diff --git a/packages/snaps-sdk/src/jsx/components/utils.ts b/packages/snaps-sdk/src/jsx/components/utils.ts new file mode 100644 index 0000000000..66b35ba763 --- /dev/null +++ b/packages/snaps-sdk/src/jsx/components/utils.ts @@ -0,0 +1,4 @@ +/** + * Definition of border radius. + */ +export type BorderRadius = 'none' | 'medium' | 'full' | undefined; diff --git a/packages/snaps-sdk/src/jsx/validation.test.tsx b/packages/snaps-sdk/src/jsx/validation.test.tsx index c656ac598f..f7c38514aa 100644 --- a/packages/snaps-sdk/src/jsx/validation.test.tsx +++ b/packages/snaps-sdk/src/jsx/validation.test.tsx @@ -34,6 +34,7 @@ import { Section, Avatar, Banner, + Skeleton, } from './components'; import { AddressStruct, @@ -72,6 +73,7 @@ import { SectionStruct, AvatarStruct, BannerStruct, + SkeletonStruct, } from './validation'; describe('KeyStruct', () => { @@ -1618,3 +1620,34 @@ describe('BannerStruct', () => { expect(is(value, BannerStruct)).toBe(false); }); }); + +describe('SkeletonStruct', () => { + it.each([ + , + , + , + , + , + , + , + ])(`validates a Skeleton element`, (value) => { + expect(is(value, SkeletonStruct)).toBe(true); + }); + + it.each([ + 'foo', + 42, + null, + undefined, + {}, + [], + // @ts-expect-error - Invalid props. + foo, + // @ts-expect-error - Invalid props. + } severity="info"> + foo + , + ])('does not validate "%p"', (value) => { + expect(is(value, SkeletonStruct)).toBe(false); + }); +}); diff --git a/packages/snaps-sdk/src/jsx/validation.ts b/packages/snaps-sdk/src/jsx/validation.ts index 5b6c03a5e1..6d286850ef 100644 --- a/packages/snaps-sdk/src/jsx/validation.ts +++ b/packages/snaps-sdk/src/jsx/validation.ts @@ -48,7 +48,7 @@ import type { SnapsChildren, StringElement, } from './component'; -import type { AvatarElement } from './components'; +import type { AvatarElement, SkeletonElement } from './components'; import { type AddressElement, type BoldElement, @@ -692,6 +692,17 @@ export const LinkStruct: Describe = element('Link', { ]), }); +/** + * A struct for the {@link SkeletonElement} type. + */ +export const SkeletonStruct: Describe = element('Skeleton', { + width: optional(union([number(), string()])), + height: optional(union([number(), string()])), + borderRadius: optional( + nullUnion([literal('none'), literal('medium'), literal('full')]), + ), +}); + /** * A struct for the {@link TextElement} type. */ @@ -701,7 +712,13 @@ export const TextStruct: Describe = element('Text', { if (typeof value === 'string') { return string(); } - return typedUnion([BoldStruct, ItalicStruct, LinkStruct, IconStruct]); + return typedUnion([ + BoldStruct, + ItalicStruct, + LinkStruct, + IconStruct, + SkeletonStruct, + ]); }), ]), alignment: optional( @@ -797,6 +814,7 @@ export const BannerStruct: Describe = element('Banner', { ButtonStruct, BoldStruct, ItalicStruct, + SkeletonStruct, ]), title: string(), severity: union([ @@ -818,6 +836,7 @@ export const RowStruct: Describe = element('Row', { TextStruct, ValueStruct, LinkStruct, + SkeletonStruct, ]), variant: optional( nullUnion([literal('default'), literal('warning'), literal('critical')]), @@ -863,6 +882,7 @@ export const BoxChildStruct = typedUnion([ SectionStruct, AvatarStruct, BannerStruct, + SkeletonStruct, ]); /** @@ -932,6 +952,7 @@ export const JSXElementStruct: Describe = typedUnion([ SectionStruct, AvatarStruct, BannerStruct, + SkeletonStruct, ]); /**