From d64466189800f251686e4ea8a4f55a8d4aac711c Mon Sep 17 00:00:00 2001 From: david0xd Date: Mon, 29 Jul 2024 11:31:09 +0200 Subject: [PATCH 1/4] Add Snap UI Radio component --- .../safe-component-list.js | 2 + .../app/snaps/snap-ui-radio-group/index.ts | 1 + .../snap-ui-radio-group.tsx | 91 +++++++++++++++++++ .../snap-ui-renderer/components/field.ts | 20 ++++ .../snap-ui-renderer/components/index.ts | 2 + .../snap-ui-renderer/components/radioGroup.ts | 26 ++++++ 6 files changed, 142 insertions(+) create mode 100644 ui/components/app/snaps/snap-ui-radio-group/index.ts create mode 100644 ui/components/app/snaps/snap-ui-radio-group/snap-ui-radio-group.tsx create mode 100644 ui/components/app/snaps/snap-ui-renderer/components/radioGroup.ts diff --git a/ui/components/app/metamask-template-renderer/safe-component-list.js b/ui/components/app/metamask-template-renderer/safe-component-list.js index 5c306e0c17d2..f224017b6280 100644 --- a/ui/components/app/metamask-template-renderer/safe-component-list.js +++ b/ui/components/app/metamask-template-renderer/safe-component-list.js @@ -32,6 +32,7 @@ import { SnapUIInput } from '../snaps/snap-ui-input'; import { SnapUIForm } from '../snaps/snap-ui-form'; import { SnapUIButton } from '../snaps/snap-ui-button'; import { SnapUIDropdown } from '../snaps/snap-ui-dropdown'; +import { SnapUIRadioGroup } from '../snaps/snap-ui-radio-group'; import { SnapUICheckbox } from '../snaps/snap-ui-checkbox'; import { SnapUITooltip } from '../snaps/snap-ui-tooltip'; ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) @@ -87,6 +88,7 @@ export const safeComponentList = { SnapUIButton, SnapUIForm, SnapUIDropdown, + SnapUIRadioGroup, SnapUICheckbox, SnapUITooltip, ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) diff --git a/ui/components/app/snaps/snap-ui-radio-group/index.ts b/ui/components/app/snaps/snap-ui-radio-group/index.ts new file mode 100644 index 000000000000..973dd83fc50f --- /dev/null +++ b/ui/components/app/snaps/snap-ui-radio-group/index.ts @@ -0,0 +1 @@ +export * from './snap-ui-radio-group'; diff --git a/ui/components/app/snaps/snap-ui-radio-group/snap-ui-radio-group.tsx b/ui/components/app/snaps/snap-ui-radio-group/snap-ui-radio-group.tsx new file mode 100644 index 000000000000..d0ac306d0bc4 --- /dev/null +++ b/ui/components/app/snaps/snap-ui-radio-group/snap-ui-radio-group.tsx @@ -0,0 +1,91 @@ +import React, { FunctionComponent, useEffect, useState } from 'react'; +import { useSnapInterfaceContext } from '../../../../contexts/snaps'; +import { + AlignItems, + Display, + FlexDirection, + TextVariant, +} from '../../../../helpers/constants/design-system'; +import { + Box, + HelpText, + HelpTextSeverity, + Label, + Text, +} from '../../../component-library'; + +export type SnapUIRadioProps = { + name: string; + label?: string; + error?: string; + options: { name: string; value: string }[]; + form?: string; +}; + +export const SnapUIRadioGroup: FunctionComponent = ({ + name, + label, + error, + form, + ...props +}) => { + const { handleInputChange, getValue } = useSnapInterfaceContext(); + + const initialValue = getValue(name, form); + + const [value, setValue] = useState(initialValue ?? ''); + + type RadioOptions = [{ value: string; name: string }]; + + useEffect(() => { + if (initialValue) { + setValue(initialValue); + } + }, [initialValue]); + + const handleChange = (newValue: string) => { + setValue(newValue); + handleInputChange(name, newValue, form); + }; + + const displayRadioOptions = (options: RadioOptions) => { + return options.map((option: { value: string; name: string }) => { + return ( + + handleChange(option.value)} + /> + + {option.name} + + + ); + }); + }; + + return ( + + {label && } + {displayRadioOptions(props.options as RadioOptions)} + {error && ( + + {error} + + )} + + ); +}; diff --git a/ui/components/app/snaps/snap-ui-renderer/components/field.ts b/ui/components/app/snaps/snap-ui-renderer/components/field.ts index eb5b0b0e2256..61a7cab9466a 100644 --- a/ui/components/app/snaps/snap-ui-renderer/components/field.ts +++ b/ui/components/app/snaps/snap-ui-renderer/components/field.ts @@ -4,11 +4,13 @@ import { ButtonElement, JSXElement, DropdownElement, + RadioGroupElement, CheckboxElement, } from '@metamask/snaps-sdk/jsx'; import { getJsxChildren } from '@metamask/snaps-utils'; import { button as buttonFn } from './button'; import { dropdown as dropdownFn } from './dropdown'; +import { radioGroup as radioGroupFn } from './radioGroup'; import { checkbox as checkboxFn } from './checkbox'; import { UIComponentFactory, UIComponentParams } from './types'; @@ -84,6 +86,24 @@ export const field: UIComponentFactory = ({ element, form }) => { }; } + case 'RadioGroup': { + const radioGroup = child as RadioGroupElement; + const radioGroupMapped = radioGroupFn({ + element: radioGroup, + } as UIComponentParams); + return { + element: 'SnapUIRadioGroup', + props: { + ...radioGroupMapped.props, + id: radioGroup.props.name, + label: element.props.label, + name: radioGroup.props.name, + form, + error: element.props.error, + }, + }; + } + case 'Checkbox': { const checkbox = child as CheckboxElement; const checkboxMapped = checkboxFn({ diff --git a/ui/components/app/snaps/snap-ui-renderer/components/index.ts b/ui/components/app/snaps/snap-ui-renderer/components/index.ts index 3a84e5a67a40..544a4f0a85f4 100644 --- a/ui/components/app/snaps/snap-ui-renderer/components/index.ts +++ b/ui/components/app/snaps/snap-ui-renderer/components/index.ts @@ -16,6 +16,7 @@ import { italic } from './italic'; import { link } from './link'; import { field } from './field'; import { dropdown } from './dropdown'; +import { radioGroup } from './radioGroup'; import { value } from './value'; import { checkbox } from './checkbox'; import { tooltip } from './tooltip'; @@ -40,6 +41,7 @@ export const COMPONENT_MAPPING = { Link: link, Field: field, Dropdown: dropdown, + Radio: radioGroup, Value: value, Checkbox: checkbox, Tooltip: tooltip, diff --git a/ui/components/app/snaps/snap-ui-renderer/components/radioGroup.ts b/ui/components/app/snaps/snap-ui-renderer/components/radioGroup.ts new file mode 100644 index 000000000000..78677e60ec35 --- /dev/null +++ b/ui/components/app/snaps/snap-ui-renderer/components/radioGroup.ts @@ -0,0 +1,26 @@ +import { RadioGroupElement, RadioElement } from '@metamask/snaps-sdk/jsx'; +import { getJsxChildren } from '@metamask/snaps-utils'; + +import { UIComponentFactory } from './types'; + +export const radioGroup: UIComponentFactory = ({ + element, + form, +}) => { + const children = getJsxChildren(element) as RadioElement[]; + + const options = children.map((child) => ({ + value: child.props.value, + name: child.props.children, + })); + + return { + element: 'SnapUIRadioGroup', + props: { + id: element.props.name, + name: element.props.name, + form, + options, + }, + }; +}; From 2253021151cec176d8ae353b8ef74248a73da787 Mon Sep 17 00:00:00 2001 From: david0xd Date: Mon, 29 Jul 2024 16:32:01 +0200 Subject: [PATCH 2/4] Refactor set init value logic --- .../app/snaps/snap-ui-radio-group/snap-ui-radio-group.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/components/app/snaps/snap-ui-radio-group/snap-ui-radio-group.tsx b/ui/components/app/snaps/snap-ui-radio-group/snap-ui-radio-group.tsx index d0ac306d0bc4..7828a1f7a355 100644 --- a/ui/components/app/snaps/snap-ui-radio-group/snap-ui-radio-group.tsx +++ b/ui/components/app/snaps/snap-ui-radio-group/snap-ui-radio-group.tsx @@ -38,7 +38,7 @@ export const SnapUIRadioGroup: FunctionComponent = ({ type RadioOptions = [{ value: string; name: string }]; useEffect(() => { - if (initialValue) { + if (initialValue && value !== initialValue) { setValue(initialValue); } }, [initialValue]); From 8c95dac22533c939a49b1343ee3c2856abca319a Mon Sep 17 00:00:00 2001 From: david0xd Date: Mon, 29 Jul 2024 16:44:37 +0200 Subject: [PATCH 3/4] Refactor radio group types --- .../snap-ui-radio-group/snap-ui-radio-group.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ui/components/app/snaps/snap-ui-radio-group/snap-ui-radio-group.tsx b/ui/components/app/snaps/snap-ui-radio-group/snap-ui-radio-group.tsx index 7828a1f7a355..914c72db7cc3 100644 --- a/ui/components/app/snaps/snap-ui-radio-group/snap-ui-radio-group.tsx +++ b/ui/components/app/snaps/snap-ui-radio-group/snap-ui-radio-group.tsx @@ -14,11 +14,13 @@ import { Text, } from '../../../component-library'; +export type SnapUIRadioOption = { value: string; name: string }; + export type SnapUIRadioProps = { name: string; label?: string; error?: string; - options: { name: string; value: string }[]; + options: SnapUIRadioOption[]; form?: string; }; @@ -35,8 +37,6 @@ export const SnapUIRadioGroup: FunctionComponent = ({ const [value, setValue] = useState(initialValue ?? ''); - type RadioOptions = [{ value: string; name: string }]; - useEffect(() => { if (initialValue && value !== initialValue) { setValue(initialValue); @@ -48,8 +48,8 @@ export const SnapUIRadioGroup: FunctionComponent = ({ handleInputChange(name, newValue, form); }; - const displayRadioOptions = (options: RadioOptions) => { - return options.map((option: { value: string; name: string }) => { + const displayRadioOptions = (options: SnapUIRadioOption[]) => { + return options.map((option: SnapUIRadioOption) => { return ( = ({ flexDirection={FlexDirection.Column} > {label && } - {displayRadioOptions(props.options as RadioOptions)} + {displayRadioOptions(props.options)} {error && ( {error} From 86b61ebe711b45329478a54badddcb53d38aa6d4 Mon Sep 17 00:00:00 2001 From: david0xd Date: Thu, 1 Aug 2024 13:09:05 +0200 Subject: [PATCH 4/4] Add some refactoring changes --- .../app/snaps/snap-ui-radio-group/snap-ui-radio-group.tsx | 6 +++--- .../app/snaps/snap-ui-renderer/components/index.ts | 2 +- .../app/snaps/snap-ui-renderer/snap-ui-renderer.test.js | 2 +- ui/components/app/snaps/snap-ui-renderer/utils.ts | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ui/components/app/snaps/snap-ui-radio-group/snap-ui-radio-group.tsx b/ui/components/app/snaps/snap-ui-radio-group/snap-ui-radio-group.tsx index 914c72db7cc3..4563fbc02fe3 100644 --- a/ui/components/app/snaps/snap-ui-radio-group/snap-ui-radio-group.tsx +++ b/ui/components/app/snaps/snap-ui-radio-group/snap-ui-radio-group.tsx @@ -16,7 +16,7 @@ import { export type SnapUIRadioOption = { value: string; name: string }; -export type SnapUIRadioProps = { +export type SnapUIRadioGroupProps = { name: string; label?: string; error?: string; @@ -24,7 +24,7 @@ export type SnapUIRadioProps = { form?: string; }; -export const SnapUIRadioGroup: FunctionComponent = ({ +export const SnapUIRadioGroup: FunctionComponent = ({ name, label, error, @@ -33,7 +33,7 @@ export const SnapUIRadioGroup: FunctionComponent = ({ }) => { const { handleInputChange, getValue } = useSnapInterfaceContext(); - const initialValue = getValue(name, form); + const initialValue = getValue(name, form) as string; const [value, setValue] = useState(initialValue ?? ''); diff --git a/ui/components/app/snaps/snap-ui-renderer/components/index.ts b/ui/components/app/snaps/snap-ui-renderer/components/index.ts index 544a4f0a85f4..51f625998a4a 100644 --- a/ui/components/app/snaps/snap-ui-renderer/components/index.ts +++ b/ui/components/app/snaps/snap-ui-renderer/components/index.ts @@ -41,7 +41,7 @@ export const COMPONENT_MAPPING = { Link: link, Field: field, Dropdown: dropdown, - Radio: radioGroup, + RadioGroup: radioGroup, Value: value, Checkbox: checkbox, Tooltip: tooltip, diff --git a/ui/components/app/snaps/snap-ui-renderer/snap-ui-renderer.test.js b/ui/components/app/snaps/snap-ui-renderer/snap-ui-renderer.test.js index 6b6f5b659c54..510d7466e22c 100644 --- a/ui/components/app/snaps/snap-ui-renderer/snap-ui-renderer.test.js +++ b/ui/components/app/snaps/snap-ui-renderer/snap-ui-renderer.test.js @@ -1,7 +1,7 @@ import { JSXElementStruct } from '@metamask/snaps-sdk/jsx'; import { COMPONENT_MAPPING } from './components'; -const EXCLUDED_COMPONENTS = ['Option']; +const EXCLUDED_COMPONENTS = ['Option', 'Radio']; describe('Snap UI mapping', () => { it('supports all exposed components', () => { diff --git a/ui/components/app/snaps/snap-ui-renderer/utils.ts b/ui/components/app/snaps/snap-ui-renderer/utils.ts index 4ad950d7c415..86e97bee12af 100644 --- a/ui/components/app/snaps/snap-ui-renderer/utils.ts +++ b/ui/components/app/snaps/snap-ui-renderer/utils.ts @@ -89,7 +89,7 @@ export const mapToTemplate = (params: MapToTemplateParams): UIComponent => { const { type, key } = params.element; const elementKey = key ?? generateKey(params.map, params.element); const mapped = COMPONENT_MAPPING[ - type as Exclude + type as Exclude // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any ](params as any);