-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Implement tooltip component on mobile (#13860)
## **Description** Implementation of the Tooltip component for Snaps UI Renderer. ## **Related issues** Fixes: MetaMask/snaps#3177 ## **Manual testing steps** 1. Go to the webview for snaps testing (https://metamask.github.io/snaps/test-snaps/latest/) 2. Scroll down and install JSX Snap. Then press 'Show JSX Snap' 3. Press the Tooltip to see a bottom sheet pop over on the screen with populated information ## **Screenshots/Recordings** https://github.com/user-attachments/assets/51b2e503-6cf9-48e7-adf4-0bc6a3495669 ## **Pre-merge author checklist** - [x] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.
- Loading branch information
1 parent
74dd739
commit 49d8909
Showing
7 changed files
with
381 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
205 changes: 205 additions & 0 deletions
205
app/components/Snaps/SnapUIRenderer/components/tooltip.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,205 @@ | ||
import { Text, SnapElement, TooltipProps } from '@metamask/snaps-sdk/jsx'; | ||
import { tooltip } from './tooltip'; | ||
import { mockTheme } from '../../../../util/theme'; | ||
|
||
describe('tooltip component', () => { | ||
const defaultParams = { | ||
map: {}, | ||
useFooter: false, | ||
onCancel: jest.fn(), | ||
t: jest.fn(), | ||
theme: mockTheme, | ||
}; | ||
|
||
it('should render tooltip with string content', () => { | ||
const e = { | ||
type: 'Tooltip' as const, | ||
props: { | ||
content: 'Tooltip content', | ||
children: [Text({ children: 'Hover me' })], | ||
}, | ||
key: null, | ||
}; | ||
|
||
const result = tooltip({ | ||
element: e as unknown as SnapElement<TooltipProps, 'Tooltip'>, | ||
...defaultParams, | ||
}); | ||
|
||
expect(result).toEqual({ | ||
element: 'SnapUITooltip', | ||
children: [ | ||
{ | ||
element: 'Text', | ||
children: [ | ||
{ | ||
element: 'RNText', | ||
children: 'Hover me', | ||
props: { | ||
color: 'inherit', | ||
}, | ||
key: expect.any(String), | ||
}, | ||
], | ||
props: { | ||
color: 'inherit', | ||
fontWeight: 'normal', | ||
textAlign: 'left', | ||
variant: 'sBodyMD', | ||
}, | ||
key: expect.any(String), | ||
}, | ||
], | ||
propComponents: { | ||
content: { | ||
element: 'Text', | ||
children: [ | ||
{ | ||
element: 'RNText', | ||
children: 'Tooltip content', | ||
props: { | ||
color: 'inherit', | ||
}, | ||
key: expect.any(String), | ||
}, | ||
], | ||
props: { | ||
color: 'inherit', | ||
fontWeight: 'normal', | ||
textAlign: 'left', | ||
variant: 'sBodyMD', | ||
}, | ||
key: expect.any(String), | ||
}, | ||
}, | ||
}); | ||
}); | ||
|
||
it('should render tooltip with complex content', () => { | ||
const e = { | ||
type: 'Tooltip' as const, | ||
props: { | ||
content: Text({ children: 'Complex content' }), | ||
children: [Text({ children: 'Hover me' })], | ||
}, | ||
key: null, | ||
}; | ||
|
||
const result = tooltip({ | ||
element: e as unknown as SnapElement<TooltipProps, 'Tooltip'>, | ||
...defaultParams, | ||
}); | ||
|
||
expect(result).toEqual({ | ||
element: 'SnapUITooltip', | ||
children: [ | ||
{ | ||
element: 'Text', | ||
children: [ | ||
{ | ||
element: 'RNText', | ||
children: 'Hover me', | ||
props: { | ||
color: 'inherit', | ||
}, | ||
key: expect.any(String), | ||
}, | ||
], | ||
props: { | ||
color: 'inherit', | ||
fontWeight: 'normal', | ||
textAlign: 'left', | ||
variant: 'sBodyMD', | ||
}, | ||
key: expect.any(String), | ||
}, | ||
], | ||
propComponents: { | ||
content: { | ||
element: 'Text', | ||
children: [ | ||
{ | ||
element: 'RNText', | ||
children: 'Complex content', | ||
props: { | ||
color: 'inherit', | ||
}, | ||
key: expect.any(String), | ||
}, | ||
], | ||
props: { | ||
color: 'inherit', | ||
fontWeight: 'normal', | ||
textAlign: 'left', | ||
variant: 'sBodyMD', | ||
}, | ||
key: expect.any(String), | ||
}, | ||
}, | ||
}); | ||
}); | ||
|
||
it('should handle nested children', () => { | ||
const e = { | ||
type: 'Tooltip' as const, | ||
props: { | ||
content: 'Tooltip content', | ||
children: [Text({ children: 'Nested text' })], | ||
}, | ||
key: null, | ||
}; | ||
|
||
const result = tooltip({ | ||
element: e as unknown as SnapElement<TooltipProps, 'Tooltip'>, | ||
...defaultParams, | ||
}); | ||
|
||
expect(result).toEqual({ | ||
element: 'SnapUITooltip', | ||
children: [ | ||
{ | ||
element: 'Text', | ||
children: [ | ||
{ | ||
element: 'RNText', | ||
children: 'Nested text', | ||
props: { | ||
color: 'inherit', | ||
}, | ||
key: expect.any(String), | ||
}, | ||
], | ||
props: { | ||
color: 'inherit', | ||
fontWeight: 'normal', | ||
textAlign: 'left', | ||
variant: 'sBodyMD', | ||
}, | ||
key: expect.any(String), | ||
}, | ||
], | ||
propComponents: { | ||
content: { | ||
element: 'Text', | ||
children: [ | ||
{ | ||
element: 'RNText', | ||
children: 'Tooltip content', | ||
props: { | ||
color: 'inherit', | ||
}, | ||
key: expect.any(String), | ||
}, | ||
], | ||
props: { | ||
color: 'inherit', | ||
fontWeight: 'normal', | ||
textAlign: 'left', | ||
variant: 'sBodyMD', | ||
}, | ||
key: expect.any(String), | ||
}, | ||
}, | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { JSXElement, Text, TooltipElement } from '@metamask/snaps-sdk/jsx'; | ||
import { getJsxChildren } from '@metamask/snaps-utils'; | ||
import { mapToTemplate } from '../utils'; | ||
import { UIComponentFactory } from './types'; | ||
|
||
export const tooltip: UIComponentFactory<TooltipElement> = ({ | ||
element: e, | ||
...params | ||
}) => ({ | ||
element: 'SnapUITooltip', | ||
children: getJsxChildren(e).map((children) => | ||
mapToTemplate({ element: children as JSXElement, ...params }), | ||
), | ||
propComponents: { | ||
content: mapToTemplate({ | ||
element: | ||
typeof e.props.content === 'string' | ||
? Text({ children: e.props.content }) | ||
: e.props.content, | ||
...params, | ||
}), | ||
}, | ||
}); |
28 changes: 28 additions & 0 deletions
28
app/components/Snaps/SnapUITooltip/SnapUITooltip.styles.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { StyleSheet } from 'react-native'; | ||
import { Theme } from '../../../util/theme/models'; | ||
import Device from '../../../util/device'; | ||
/** | ||
* | ||
* @param params Style sheet params. | ||
* @param params.theme App theme from ThemeContext. | ||
* @returns StyleSheet object. | ||
*/ | ||
const styleSheet = (params: { theme: Theme }) => { | ||
const { theme } = params; | ||
const { colors } = theme; | ||
return StyleSheet.create({ | ||
modal: { | ||
backgroundColor: colors.background.default, | ||
borderTopLeftRadius: 24, | ||
borderTopRightRadius: 24, | ||
minHeight: '50%', | ||
paddingBottom: Device.isIphoneX() ? 20 : 0, | ||
overflow: 'hidden', | ||
}, | ||
content: { | ||
paddingHorizontal: 16, | ||
}, | ||
}); | ||
}; | ||
|
||
export default styleSheet; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import React from 'react'; | ||
import { fireEvent, render } from '@testing-library/react-native'; | ||
import { SnapUITooltip } from './SnapUITooltip'; | ||
import { Text, TouchableOpacity } from 'react-native'; | ||
import ApprovalModal from '../../Approvals/ApprovalModal'; | ||
|
||
jest.mock( | ||
'../../../component-library/components/BottomSheets/BottomSheetHeader', | ||
() => ({ | ||
__esModule: true, | ||
default: function BottomSheetHeader({ onBack }: { onBack: () => void }) { | ||
setTimeout(onBack, 0); | ||
return null; | ||
}, | ||
}), | ||
); | ||
|
||
describe('SnapUITooltip', () => { | ||
it('should render tooltip with content and children', () => { | ||
const content = 'Test content'; | ||
const children = 'Click me'; | ||
const { getByText } = render( | ||
<SnapUITooltip content={<Text>{content}</Text>}> | ||
<Text>{children}</Text> | ||
</SnapUITooltip>, | ||
); | ||
|
||
expect(getByText(children)).toBeTruthy(); | ||
}); | ||
|
||
it('should open modal on press', () => { | ||
const content = 'Test content'; | ||
const children = 'Click me'; | ||
const { getByText, UNSAFE_getByType } = render( | ||
<SnapUITooltip content={<Text>{content}</Text>}> | ||
<Text>{children}</Text> | ||
</SnapUITooltip>, | ||
); | ||
|
||
const touchable = getByText(children).parent as TouchableOpacity; | ||
fireEvent.press(touchable); | ||
|
||
const modal = UNSAFE_getByType(ApprovalModal); | ||
expect(modal.props.isVisible).toBe(true); | ||
}); | ||
|
||
it('should close modal when back action is triggered', async () => { | ||
const content = 'Test content'; | ||
const children = 'Click me'; | ||
const { getByText, UNSAFE_getByType } = render( | ||
<SnapUITooltip content={<Text>{content}</Text>}> | ||
<Text>{children}</Text> | ||
</SnapUITooltip>, | ||
); | ||
|
||
const touchable = getByText(children).parent as TouchableOpacity; | ||
fireEvent.press(touchable); | ||
|
||
await new Promise((resolve) => setTimeout(resolve, 0)); | ||
|
||
const modal = UNSAFE_getByType(ApprovalModal); | ||
expect(modal.props.isVisible).toBe(false); | ||
}); | ||
|
||
it('should render complex content in modal', () => { | ||
const content = <Text>Complex content</Text>; | ||
const children = 'Click me'; | ||
const { getByText, UNSAFE_getByType } = render( | ||
<SnapUITooltip content={content}> | ||
<Text>{children}</Text> | ||
</SnapUITooltip>, | ||
); | ||
|
||
const touchable = getByText(children).parent as TouchableOpacity; | ||
fireEvent.press(touchable); | ||
|
||
const modal = UNSAFE_getByType(ApprovalModal); | ||
expect(modal).toBeTruthy(); | ||
expect(getByText('Complex content')).toBeTruthy(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import React, { FunctionComponent, ReactNode, useState } from 'react'; | ||
import ApprovalModal from '../../Approvals/ApprovalModal'; | ||
import { ScrollView, TouchableOpacity, View } from 'react-native'; | ||
import { useStyles } from '../../../component-library/hooks/useStyles'; | ||
import stylesheet from './SnapUITooltip.styles'; | ||
import BottomSheetHeader from '../../../component-library/components/BottomSheets/BottomSheetHeader'; | ||
|
||
export interface SnapUITooltipProps { | ||
content: ReactNode; | ||
children: ReactNode; | ||
} | ||
|
||
export const SnapUITooltip: FunctionComponent<SnapUITooltipProps> = ({ | ||
content, | ||
children, | ||
}) => { | ||
const { styles } = useStyles(stylesheet, {}); | ||
|
||
const [isOpen, setIsOpen] = useState(false); | ||
|
||
const handleOnOpen = () => { | ||
setIsOpen(true); | ||
}; | ||
|
||
const handleOnCancel = () => { | ||
setIsOpen(false); | ||
}; | ||
|
||
return ( | ||
<> | ||
<TouchableOpacity onPress={handleOnOpen}>{children}</TouchableOpacity> | ||
<ApprovalModal isVisible={isOpen} onCancel={handleOnCancel}> | ||
<View style={styles.modal}> | ||
<BottomSheetHeader onBack={handleOnCancel} /> | ||
<ScrollView style={styles.content}>{content}</ScrollView> | ||
</View> | ||
</ApprovalModal> | ||
</> | ||
); | ||
}; |
Oops, something went wrong.