diff --git a/packages/snaps-rpc-methods/src/permitted/createInterface.test.tsx b/packages/snaps-rpc-methods/src/permitted/createInterface.test.tsx index adb621d501..bac78f688d 100644 --- a/packages/snaps-rpc-methods/src/permitted/createInterface.test.tsx +++ b/packages/snaps-rpc-methods/src/permitted/createInterface.test.tsx @@ -195,7 +195,7 @@ describe('snap_createInterface', () => { error: { code: -32602, message: - 'Invalid params: At path: ui -- Expected type to be one of: "AccountSelector", "Address", "AssetSelector", "AddressInput", "Bold", "Box", "Button", "Copyable", "DateTimePicker", "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.', + 'Invalid params: At path: ui -- Expected type to be one of: "AccountSelector", "Address", "AssetSelector", "AddressInput", "Bold", "Box", "Button", "Copyable", "DateTimePicker", "Divider", "Dropdown", "RadioGroup", "Field", "FileInput", "Form", "Heading", "Input", "Image", "Italic", "Link", "Row", "Spinner", "Text", "Tooltip", "Checkbox", "Card", "Icon", "Selector", "Section", "Avatar", "Banner", "Skeleton", "CollapsibleSection", "Container", but received: undefined.', stack: expect.any(String), }, id: 1, diff --git a/packages/snaps-sdk/src/jsx/components/CollapsibleSection.test.tsx b/packages/snaps-sdk/src/jsx/components/CollapsibleSection.test.tsx new file mode 100644 index 0000000000..11eadfee0b --- /dev/null +++ b/packages/snaps-sdk/src/jsx/components/CollapsibleSection.test.tsx @@ -0,0 +1,129 @@ +import { Address } from './Address'; +import { CollapsibleSection } from './CollapsibleSection'; +import { Row } from './Row'; +import { Text } from './Text'; + +describe('CollapsibleSection', () => { + it('renders a collapsible section', () => { + const result = ( + + Hello + + ); + + expect(result).toStrictEqual({ + type: 'CollapsibleSection', + key: null, + props: { + label: 'Details', + children: { + type: 'Text', + key: null, + props: { + children: 'Hello', + }, + }, + }, + }); + }); + + it('renders a collapsible section with multiple children', () => { + const result = ( + + +
+ + +
+ + + ); + + expect(result).toStrictEqual({ + type: 'CollapsibleSection', + key: null, + props: { + label: 'Transaction details', + children: [ + { + type: 'Row', + key: null, + props: { + label: 'From', + children: { + type: 'Address', + key: null, + props: { + address: '0x1234567890123456789012345678901234567890', + }, + }, + }, + }, + { + type: 'Row', + key: null, + props: { + label: 'To', + tooltip: 'This address has been deemed dangerous.', + variant: 'warning', + children: { + type: 'Address', + key: null, + props: { + address: '0x0000000000000000000000000000000000000000', + }, + }, + }, + }, + ], + }, + }); + }); + + it('renders a collapsible section with props', () => { + const result = ( + + Hello + World + + ); + + expect(result).toStrictEqual({ + type: 'CollapsibleSection', + key: null, + props: { + label: 'Details', + direction: 'horizontal', + alignment: 'space-between', + isExpanded: true, + isLoading: false, + children: [ + { + type: 'Text', + key: null, + props: { + children: 'Hello', + }, + }, + { + type: 'Text', + key: null, + props: { + children: 'World', + }, + }, + ], + }, + }); + }); +}); diff --git a/packages/snaps-sdk/src/jsx/components/CollapsibleSection.ts b/packages/snaps-sdk/src/jsx/components/CollapsibleSection.ts new file mode 100644 index 0000000000..3419674507 --- /dev/null +++ b/packages/snaps-sdk/src/jsx/components/CollapsibleSection.ts @@ -0,0 +1,65 @@ +import type { GenericSnapElement, SnapsChildren } from '../component'; +import { createSnapComponent } from '../component'; + +/** + * The props of the {@link CollapsibleSection} component. + * + * @property children - The children of the collapsible section. + * @property label - The label of the collapsible section. + * @property isLoading - Whether the section is still loading. + * @property isExpanded - Whether the section should start expanded. + * @property direction - The direction to stack the components within the section. Defaults to `vertical`. + * @property alignment - The alignment mode to use within the section. Defaults to `start`. + * @category Component Props + */ +export type CollapsibleSectionProps = { + // We can't use `JSXElement` because it causes a circular reference. + children: SnapsChildren; + label: string; + isLoading?: boolean; + isExpanded?: boolean; + direction?: 'vertical' | 'horizontal' | undefined; + alignment?: + | 'start' + | 'center' + | 'end' + | 'space-between' + | 'space-around' + | undefined; +}; + +const TYPE = 'CollapsibleSection'; + +/** + * A collapsible section component, which is used to group multiple components + * together with a label. The section can be expanded or collapsed by the user. + * + * @param props - The props of the component. + * @param props.children - The children of the collapsible section. + * @param props.label - The label of the collapsible section. + * @param props.direction - The direction that the children are aligned. + * @param props.alignment - The alignment of the children (a justify-content value). + * @returns A collapsible section element. + * @example + * + * + *
+ * + * + *
+ * + * + * @category Components + */ +export const CollapsibleSection = createSnapComponent< + CollapsibleSectionProps, + typeof TYPE +>(TYPE); + +/** + * A collapsible section element. + * + * @see {@link CollapsibleSection} + * @category Elements + */ +export type CollapsibleSectionElement = ReturnType; diff --git a/packages/snaps-sdk/src/jsx/components/Section.ts b/packages/snaps-sdk/src/jsx/components/Section.ts index b21ae3819c..fa03e589ca 100644 --- a/packages/snaps-sdk/src/jsx/components/Section.ts +++ b/packages/snaps-sdk/src/jsx/components/Section.ts @@ -5,6 +5,8 @@ import { createSnapComponent } from '../component'; * The props of the {@link Section} component. * * @property children - The children of the section. + * @property direction - The direction to stack the components within the section. Defaults to `vertical`. + * @property alignment - The alignment mode to use within the section. Defaults to `start`. * @category Component Props */ export type SectionProps = { diff --git a/packages/snaps-sdk/src/jsx/components/index.ts b/packages/snaps-sdk/src/jsx/components/index.ts index 23d336570e..bdd486b1b1 100644 --- a/packages/snaps-sdk/src/jsx/components/index.ts +++ b/packages/snaps-sdk/src/jsx/components/index.ts @@ -3,6 +3,7 @@ import type { AvatarElement } from './Avatar'; import type { BannerElement } from './Banner'; import type { BoxElement } from './Box'; import type { CardElement } from './Card'; +import type { CollapsibleSectionElement } from './CollapsibleSection'; import type { ContainerElement } from './Container'; import type { CopyableElement } from './Copyable'; import type { DividerElement } from './Divider'; @@ -27,6 +28,7 @@ export * from './Address'; export * from './Avatar'; export * from './Box'; export * from './Card'; +export * from './CollapsibleSection'; export * from './Copyable'; export * from './Divider'; export * from './Value'; @@ -66,6 +68,7 @@ export type JSXElement = | LinkElement | RowElement | SectionElement + | CollapsibleSectionElement | SpinnerElement | TextElement | TooltipElement diff --git a/packages/snaps-sdk/src/jsx/validation.test.tsx b/packages/snaps-sdk/src/jsx/validation.test.tsx index d1f1a171ff..0925497925 100644 --- a/packages/snaps-sdk/src/jsx/validation.test.tsx +++ b/packages/snaps-sdk/src/jsx/validation.test.tsx @@ -39,6 +39,7 @@ import { AddressInput, AccountSelector, DateTimePicker, + CollapsibleSection, } from './components'; import { AddressStruct, @@ -82,6 +83,7 @@ import { AddressInputStruct, AccountSelectorStruct, DateTimePickerStruct, + CollapsibleSectionStruct, } from './validation'; describe('KeyStruct', () => { @@ -1666,6 +1668,77 @@ describe('SectionStruct', () => { }); }); +describe('CollapsibleSectionStruct', () => { + it.each([ + + + Hello world! + + , + + +
+ + +
+ + , + + foo + + alt + + , + + foo + + alt + + , + ])('validates a collapsible section element', (value) => { + expect(is(value, CollapsibleSectionStruct)).toBe(true); + }); + + it.each([ + 'foo', + 42, + null, + undefined, + {}, + [], + // @ts-expect-error - Invalid props. + , + // @ts-expect-error - Invalid props. + , + // @ts-expect-error - Invalid props. + , + // @ts-expect-error - Missing label. + + foo + , + foo, + + foo + , + // @ts-expect-error - Invalid props. + + + Hello world! + + , + ])('does not validate "%p"', (value) => { + expect(is(value, CollapsibleSectionStruct)).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 d7fcdfda40..6e1edfadbf 100644 --- a/packages/snaps-sdk/src/jsx/validation.ts +++ b/packages/snaps-sdk/src/jsx/validation.ts @@ -81,6 +81,7 @@ import type { SelectorOptionElement, BannerElement, DateTimePickerElement, + CollapsibleSectionElement, } from './components'; import { IconName } from './components'; import type { Describe } from '../internals'; @@ -768,6 +769,29 @@ export const SectionStruct: Describe = element('Section', { ), }); +/** + * A struct for the {@link CollapsibleSectionElement} type. + */ +export const CollapsibleSectionStruct: Describe = + element('CollapsibleSection', { + children: BoxChildrenStruct, + label: string(), + isLoading: optional(boolean()), + isExpanded: optional(boolean()), + direction: optional( + nullUnion([literal('horizontal'), literal('vertical')]), + ), + alignment: optional( + nullUnion([ + literal('start'), + literal('center'), + literal('end'), + literal('space-between'), + literal('space-around'), + ]), + ), + }); + /** * A subset of JSX elements that are allowed as children of the Footer component. * This set should include a single button or a tuple of two buttons. @@ -1020,6 +1044,7 @@ export const BoxChildStruct = typedUnion([ AvatarStruct, BannerStruct, SkeletonStruct, + CollapsibleSectionStruct, ]); /** @@ -1094,6 +1119,7 @@ export const JSXElementStruct: Describe = typedUnion([ AvatarStruct, BannerStruct, SkeletonStruct, + CollapsibleSectionStruct, ]); /**