-
Notifications
You must be signed in to change notification settings - Fork 5.3k
feat: Add logic for new requireScroll
prop for Snap Footer
component
#29863
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 17 commits
fb1ef7c
6689d13
22be388
328b3ac
da08c07
3692446
063b985
1f3d3fb
38efda3
5ac14f9
513d77d
6aa2a73
45ada75
e6ed619
d659a38
470c38e
f4f3dc9
3f352b3
dafe6aa
6bfd930
46ab45e
ffa89c8
995774c
89ac089
fb755a3
2f4f7d1
f7bac01
d414b6d
21c8e10
4f240ed
272d239
2b8faa3
7166bec
1697674
dde70f1
172e802
3b460ca
91f83f7
f6ba4c5
74bd5b1
355fe1e
8e0b3d7
49c45bc
0191696
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { JSXElement } from '@metamask/snaps-sdk/jsx'; | ||
import { UIComponentFactory } from './types'; | ||
|
||
export const avatarIcon: UIComponentFactory<JSXElement> = ({ element }) => ({ | ||
element: 'AvatarIcon', | ||
props: { | ||
// Define props once we expose this component to snap developers | ||
...element.props, | ||
}, | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
import React, { memo, useMemo } from 'react'; | ||
import React, { memo, useMemo, useCallback } from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import { useSelector } from 'react-redux'; | ||
import { Container } from '@metamask/snaps-sdk/jsx'; | ||
|
@@ -7,7 +7,7 @@ import { isEqual } from 'lodash'; | |
import MetaMaskTemplateRenderer from '../../metamask-template-renderer/metamask-template-renderer'; | ||
import { SnapDelineator } from '../snap-delineator'; | ||
import { getSnapMetadata, getMemoizedInterface } from '../../../../selectors'; | ||
import { Box } from '../../../component-library'; | ||
import { Box, IconName } from '../../../component-library'; | ||
import { DelineatorType } from '../../../../helpers/constants/snaps'; | ||
|
||
import { SnapInterfaceContextProvider } from '../../../../contexts/snaps'; | ||
|
@@ -17,11 +17,80 @@ import { | |
BackgroundColor, | ||
BlockSize, | ||
Display, | ||
IconColor, | ||
JustifyContent, | ||
} from '../../../../helpers/constants/design-system'; | ||
import { useI18nContext } from '../../../../hooks/useI18nContext'; | ||
import { useScrollRequired } from '../../../../hooks/useScrollRequired'; | ||
import { useScrollHandling } from '../../../../hooks/useScrollHandling'; | ||
import { mapToExtensionCompatibleColor, mapToTemplate } from './utils'; | ||
|
||
/** | ||
* This hook is used to process the content of the Snap UI Renderer. | ||
* It adds the scroll arrow and the footer to the content if required. | ||
* | ||
* @param {object} rawContent - The raw content of the Snap UI Renderer. | ||
* @param {object} scrollState - The state of the scroll handling. | ||
* @param {object} scrollArrow - The scroll arrow component. | ||
* @param {boolean} requireScroll - Whether the content requires scrolling. | ||
* @returns The processed content. | ||
*/ | ||
const useProcessedContent = ( | ||
rawContent, | ||
hmalik88 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
scrollState, | ||
scrollArrow, | ||
requireScroll, | ||
) => { | ||
return useMemo(() => { | ||
const baseContent = | ||
rawContent?.type === 'Container' || !rawContent | ||
? rawContent | ||
: Container({ children: rawContent }); | ||
|
||
if (requireScroll) { | ||
const children = [...baseContent.props.children]; | ||
const footer = { | ||
...children[children.length - 1], | ||
props: { | ||
...children[children.length - 1].props, | ||
isScrolledToBottom: scrollState.buttonsEnabled, | ||
requireScroll: true, | ||
}, | ||
}; | ||
|
||
children[children.length - 1] = footer; | ||
|
||
if (scrollState.showArrow) { | ||
children.splice(-1, 0, scrollArrow); | ||
} | ||
|
||
return { | ||
...baseContent, | ||
props: { | ||
...baseContent.props, | ||
children, | ||
}, | ||
}; | ||
} | ||
|
||
return baseContent; | ||
}, [rawContent, scrollState, scrollArrow, requireScroll]); | ||
}; | ||
|
||
const LoadingSpinner = memo(function LoadingSpinner() { | ||
return ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this moved out? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For readability There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. and memoizing because it's static no need to re-create on every render |
||
<Box | ||
display={Display.Flex} | ||
justifyContent={JustifyContent.center} | ||
alignItems={AlignItems.center} | ||
height={BlockSize.Full} | ||
width={BlockSize.Full} | ||
> | ||
<PulseLoader /> | ||
</Box> | ||
); | ||
}); | ||
|
||
// Component that maps Snaps UI JSON format to MetaMask Template Renderer format | ||
const SnapUIRendererComponent = ({ | ||
snapId, | ||
|
@@ -44,21 +113,76 @@ const SnapUIRendererComponent = ({ | |
}) => { | ||
const t = useI18nContext(); | ||
|
||
const { name: snapName } = useSelector((state) => | ||
getSnapMetadata(state, snapId), | ||
); | ||
|
||
const interfaceState = useSelector( | ||
(state) => getMemoizedInterface(state, interfaceId), | ||
// We only want to update the state if the content has changed. | ||
// We do this to avoid useless re-renders. | ||
(oldState, newState) => isEqual(oldState.content, newState.content), | ||
); | ||
const rawContent = interfaceState?.content; | ||
const content = | ||
rawContent?.type === 'Container' || !rawContent | ||
? rawContent | ||
: Container({ children: rawContent }); | ||
|
||
const requireScroll = | ||
interfaceState?.content?.props?.children?.[1]?.props?.requireScroll; | ||
|
||
const { isScrollable, scrollToBottom, onScroll, ref } = useScrollRequired( | ||
[], | ||
// Only enable hook if the content requires scrolling | ||
{ enabled: requireScroll }, | ||
); | ||
|
||
const { name: snapName } = useSelector((state) => | ||
getSnapMetadata(state, snapId), | ||
); | ||
|
||
const { scrollState, handleScroll, handleScrollToBottom } = useScrollHandling( | ||
requireScroll, | ||
isScrollable, | ||
); | ||
|
||
const wrappedOnScroll = useCallback( | ||
(e) => { | ||
handleScroll(e, onScroll); | ||
}, | ||
[handleScroll, onScroll], | ||
); | ||
|
||
const handleScrollToBottomClick = useCallback(() => { | ||
handleScrollToBottom(scrollToBottom); | ||
}, [handleScrollToBottom, scrollToBottom]); | ||
|
||
const scrollArrow = useMemo(() => { | ||
if (!requireScroll || !scrollState.showArrow) { | ||
return null; | ||
} | ||
|
||
return { | ||
type: 'AvatarIcon', | ||
key: 'snap-ui-renderer__scroll-arrow', | ||
props: { | ||
iconName: IconName.Arrow2Down, | ||
backgroundColor: BackgroundColor.infoDefault, | ||
color: IconColor.primaryInverse, | ||
className: 'snap-ui-renderer__scroll-button', | ||
onClick: handleScrollToBottomClick, | ||
style: { | ||
cursor: 'pointer', | ||
position: 'absolute', | ||
left: 0, | ||
right: 0, | ||
marginLeft: 'auto', | ||
marginRight: 'auto', | ||
zIndex: 'auto', | ||
bottom: '84px', | ||
}, | ||
}, | ||
}; | ||
}, [handleScrollToBottomClick, requireScroll, scrollState.showArrow]); | ||
|
||
const processedContent = useProcessedContent( | ||
interfaceState?.content, | ||
scrollState, | ||
scrollArrow, | ||
requireScroll, | ||
); | ||
|
||
const promptLegacyProps = useMemo( | ||
() => | ||
|
@@ -72,44 +196,47 @@ const SnapUIRendererComponent = ({ | |
|
||
const backgroundColor = | ||
contentBackgroundColor ?? | ||
mapToExtensionCompatibleColor(content?.props?.backgroundColor) ?? | ||
mapToExtensionCompatibleColor(processedContent?.props?.backgroundColor) ?? | ||
BackgroundColor.backgroundAlternative; | ||
|
||
// The renderer should only have a footer if there is a default cancel action | ||
// or if the footer component has been used. | ||
const hasFooter = | ||
onCancel || processedContent?.props?.children?.[1] !== undefined; | ||
|
||
const sections = useMemo( | ||
() => | ||
content && | ||
processedContent && | ||
mapToTemplate({ | ||
map: {}, | ||
element: content, | ||
element: processedContent, | ||
onCancel, | ||
useFooter, | ||
promptLegacyProps, | ||
t, | ||
contentBackgroundColor: backgroundColor, | ||
// We have to pass the props at the top level to ensure the children components get the correct props. | ||
requireScroll, | ||
isScrolledToBottom: scrollState.buttonsEnabled, | ||
}), | ||
hmalik88 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
[content, onCancel, useFooter, promptLegacyProps, t, backgroundColor], | ||
[ | ||
processedContent, | ||
onCancel, | ||
hmalik88 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
useFooter, | ||
promptLegacyProps, | ||
t, | ||
backgroundColor, | ||
scrollState.buttonsEnabled, | ||
requireScroll, | ||
], | ||
); | ||
|
||
if (isLoading || !content) { | ||
return ( | ||
<Box | ||
display={Display.Flex} | ||
justifyContent={JustifyContent.center} | ||
alignItems={AlignItems.center} | ||
height={BlockSize.Full} | ||
width={BlockSize.Full} | ||
> | ||
<PulseLoader /> | ||
</Box> | ||
); | ||
if (isLoading || !processedContent) { | ||
return <LoadingSpinner />; | ||
} | ||
|
||
const { state: initialState, context } = interfaceState; | ||
|
||
// The renderer should only have a footer if there is a default cancel action | ||
// or if the footer component has been used. | ||
const hasFooter = onCancel || content?.props?.children?.[1] !== undefined; | ||
|
||
return useDelineator ? ( | ||
<SnapDelineator | ||
snapName={snapName} | ||
|
@@ -140,6 +267,8 @@ const SnapUIRendererComponent = ({ | |
> | ||
<Box | ||
className="snap-ui-renderer__content" | ||
ref={ref} | ||
onScroll={wrappedOnScroll} | ||
height={BlockSize.Full} | ||
backgroundColor={backgroundColor} | ||
style={{ | ||
|
Uh oh!
There was an error while loading. Please reload this page.