1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-22 17:33:23 +01:00

UX: Multichain: Added TokenList Component (#17859)

* added redesign storybook

* updated token list

* updated css

* fixed lint error

* updated the new token list component

* fixed redesign folder error

* reverted changes in settings.json

* updated redesign to multichain

* added feature flag

* reverted settings.json

* added detect token banner

* added button componeny

* fixed lint errors

* removed settings

* fixed lint errors

* added stories for multichain

* updated no token found string

* updated lint error

* updated padding values

* updated padding values

* updated tabs with role button

* updated hover state

* updated components with multichain

* fixed lint errors

* updated multichain import token link

* updated a tag

* updated fixes

* updated onClick to handleClick

* updated setShowDetectedTokens proptype

* updated multichain tokenlist with item suffix

* updated tests

* updated tests

* updated token list css

* updated snapshot

* updated text

* reverted story

* added story for multichain token list

* updated story

* updated tooltip

* updated the new token list component

* fixed redesign folder error

* added feature flag

* reverted unused setting change

* removed token list

* fixed lint error

* updated status

* updated tooltip

* updated token-list-item changes

* updated actionbutton click for detect token banner

* updated snapshot

* updated symbol

* updated styles

* updated eth decimal and token url

* updated snapshot

* updated scripts

* updated snapshots
This commit is contained in:
Nidhi Kumari 2023-03-23 15:38:33 +05:30 committed by GitHub
parent 68f928c8a2
commit fcfb8a8938
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 919 additions and 58 deletions

View File

@ -12,21 +12,26 @@ import {
getNativeCurrencyImage,
getDetectedTokensInCurrentNetwork,
getIstokenDetectionInactiveOnNonMainnetSupportedNetwork,
getTokenList,
} from '../../../selectors';
import { getNativeCurrency } from '../../../ducks/metamask/metamask';
import { useCurrencyDisplay } from '../../../hooks/useCurrencyDisplay';
import Typography from '../../ui/typography/typography';
import Box from '../../ui/box/box';
import {
Color,
TypographyVariant,
FONT_WEIGHT,
JustifyContent,
TextVariant,
TEXT_ALIGN,
} from '../../../helpers/constants/design-system';
import { useI18nContext } from '../../../hooks/useI18nContext';
import { MetaMetricsContext } from '../../../contexts/metametrics';
import { EVENT, EVENT_NAMES } from '../../../../shared/constants/metametrics';
import DetectedToken from '../detected-token/detected-token';
import {
DetectedTokensBanner,
MultichainTokenListItem,
MultichainImportTokenLink,
} from '../../multichain';
import { Text } from '../../component-library';
import DetectedTokensLink from './detetcted-tokens-link/detected-tokens-link';
const AssetList = ({ onClickAsset }) => {
@ -69,20 +74,38 @@ const AssetList = ({ onClickAsset }) => {
const istokenDetectionInactiveOnNonMainnetSupportedNetwork = useSelector(
getIstokenDetectionInactiveOnNonMainnetSupportedNetwork,
);
const tokenList = useSelector(getTokenList);
const tokenData = Object.values(tokenList).find(
(token) => token.symbol === primaryCurrencyProperties.suffix,
);
const title = tokenData?.name || primaryCurrencyProperties.suffix;
return (
<>
<AssetListItem
onClick={() => onClickAsset(nativeCurrency)}
data-testid="wallet-balance"
primary={
primaryCurrencyProperties.value ?? secondaryCurrencyProperties.value
}
tokenSymbol={primaryCurrencyProperties.suffix}
secondary={showFiat ? secondaryCurrencyDisplay : undefined}
tokenImage={balanceIsLoading ? null : primaryTokenImage}
identiconBorder
/>
{process.env.MULTICHAIN ? (
<MultichainTokenListItem
onClick={() => onClickAsset(nativeCurrency)}
title={title}
primary={
primaryCurrencyProperties.value ?? secondaryCurrencyProperties.value
}
tokenSymbol={primaryCurrencyProperties.suffix}
secondary={showFiat ? secondaryCurrencyDisplay : undefined}
tokenImage={balanceIsLoading ? null : primaryTokenImage}
/>
) : (
<AssetListItem
onClick={() => onClickAsset(nativeCurrency)}
data-testid="wallet-balance"
primary={
primaryCurrencyProperties.value ?? secondaryCurrencyProperties.value
}
tokenSymbol={primaryCurrencyProperties.suffix}
secondary={showFiat ? secondaryCurrencyDisplay : undefined}
tokenImage={balanceIsLoading ? null : primaryTokenImage}
identiconBorder
/>
)}
<TokenList
onTokenClick={(tokenAddress) => {
onClickAsset(tokenAddress);
@ -98,19 +121,36 @@ const AssetList = ({ onClickAsset }) => {
/>
{detectedTokens.length > 0 &&
!istokenDetectionInactiveOnNonMainnetSupportedNetwork && (
<DetectedTokensLink setShowDetectedTokens={setShowDetectedTokens} />
<>
{process.env.MULTICHAIN ? (
<DetectedTokensBanner
actionButtonOnClick={() => setShowDetectedTokens(true)}
margin={4}
/>
) : (
<DetectedTokensLink
setShowDetectedTokens={setShowDetectedTokens}
/>
)}
</>
)}
<Box marginTop={detectedTokens.length > 0 ? 0 : 4}>
<Box justifyContent={JustifyContent.center}>
<Typography
color={Color.textAlternative}
variant={TypographyVariant.H6}
fontWeight={FONT_WEIGHT.NORMAL}
>
{t('missingToken')}
</Typography>
</Box>
<ImportTokenLink />
{process.env.MULTICHAIN ? (
<MultichainImportTokenLink margin={4} />
) : (
<>
<Text
color={Color.textAlternative}
variant={TextVariant.bodySm}
as="h6"
textAlign={TEXT_ALIGN.CENTER}
>
{t('missingToken')}
</Text>
<ImportTokenLink />
</>
)}
</Box>
{showDetectedTokens && (
<DetectedToken setShowDetectedTokens={setShowDetectedTokens} />

View File

@ -3,9 +3,12 @@ import PropTypes from 'prop-types';
import React from 'react';
import { useSelector } from 'react-redux';
import AssetListItem from '../asset-list-item';
import { getSelectedAddress } from '../../../selectors';
import { getSelectedAddress, getTokenList } from '../../../selectors';
import { useI18nContext } from '../../../hooks/useI18nContext';
import { useTokenFiatAmount } from '../../../hooks/useTokenFiatAmount';
import { MultichainTokenListItem } from '../../multichain';
import { ButtonLink, Text } from '../../component-library';
import { TextColor } from '../../../helpers/constants/design-system';
export default function TokenCell({
address,
@ -19,39 +22,58 @@ export default function TokenCell({
}) {
const userAddress = useSelector(getSelectedAddress);
const t = useI18nContext();
const tokenList = useSelector(getTokenList);
const tokenData = Object.values(tokenList).find(
(token) => token.symbol === symbol,
);
const title = tokenData?.name || symbol;
const tokenImage = tokenData?.iconUrl || image;
const formattedFiat = useTokenFiatAmount(address, string, symbol);
const warning = balanceError ? (
<span>
<Text as="span">
{t('troubleTokenBalances')}
<a
<ButtonLink
href={`https://ethplorer.io/address/${userAddress}`}
rel="noopener noreferrer"
target="_blank"
externalLink
onClick={(event) => event.stopPropagation()}
style={{ color: 'var(--color-warning-default)' }}
textProps={{
color: TextColor.warningDefault,
}}
>
{t('here')}
</a>
</span>
</ButtonLink>
</Text>
) : null;
return (
<AssetListItem
className={classnames('token-cell', {
'token-cell--outdated': Boolean(balanceError),
})}
iconClassName="token-cell__icon"
onClick={onClick.bind(null, address)}
tokenAddress={address}
tokenSymbol={symbol}
tokenDecimals={decimals}
tokenImage={image}
warning={warning}
primary={`${string || 0}`}
secondary={formattedFiat}
isERC721={isERC721}
/>
<>
{process.env.MULTICHAIN ? (
<MultichainTokenListItem
onClick={() => onClick(address)}
tokenSymbol={symbol}
tokenImage={tokenImage}
primary={`${string || 0}`}
secondary={formattedFiat}
title={title}
/>
) : (
<AssetListItem
className={classnames('token-cell', {
'token-cell--outdated': Boolean(balanceError),
})}
iconClassName="token-cell__icon"
onClick={() => onClick(address)}
tokenAddress={address}
tokenSymbol={symbol}
tokenDecimals={decimals}
tokenImage={image}
warning={warning}
primary={`${string || 0}`}
secondary={formattedFiat}
isERC721={isERC721}
/>
)}
</>
);
}

View File

@ -0,0 +1,27 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DetectedTokensBanner should render correctly 1`] = `
<div>
<div
class="box mm-banner-base mm-banner-alert mm-banner-alert--severity-info multichain-detected-token-banner box--padding-3 box--padding-left-2 box--display-flex box--gap-2 box--flex-direction-row box--background-color-primary-muted box--rounded-sm"
data-testid="detected-token-banner"
>
<span
class="box mm-icon mm-icon--size-lg box--display-inline-block box--flex-direction-row box--color-primary-default"
style="mask-image: url('./images/icons/info.svg');"
/>
<div>
<p
class="box mm-text mm-text--body-md mm-text--color-text-default box--flex-direction-row"
>
3 new tokens found in this account
</p>
<button
class="box mm-text mm-button-base mm-button-link mm-button-link--size-auto mm-text--body-md mm-text--color-primary-default box--display-inline-flex box--flex-direction-row box--justify-content-center box--align-items-center box--background-color-transparent"
>
Import tokens
</button>
</div>
</div>
</div>
`;

View File

@ -0,0 +1,60 @@
import React, { useContext } from 'react';
import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useI18nContext } from '../../../hooks/useI18nContext';
import { getDetectedTokensInCurrentNetwork } from '../../../selectors';
import { MetaMetricsContext } from '../../../contexts/metametrics';
import { EVENT, EVENT_NAMES } from '../../../../shared/constants/metametrics';
import { BannerAlert } from '../../component-library';
export const DetectedTokensBanner = ({
className,
actionButtonOnClick,
...props
}) => {
const t = useI18nContext();
const trackEvent = useContext(MetaMetricsContext);
const detectedTokens = useSelector(getDetectedTokensInCurrentNetwork);
const detectedTokensDetails = detectedTokens.map(
({ address, symbol }) => `${symbol} - ${address}`,
);
const handleOnClick = () => {
actionButtonOnClick();
trackEvent({
event: EVENT_NAMES.TOKEN_IMPORT_CLICKED,
category: EVENT.CATEGORIES.WALLET,
properties: {
source: EVENT.SOURCE.TOKEN.DETECTED,
tokens: detectedTokensDetails,
},
});
};
return (
<BannerAlert
className={classNames('multichain-detected-token-banner', className)}
actionButtonLabel={t('importTokensCamelCase')}
actionButtonOnClick={handleOnClick}
data-testid="detected-token-banner"
{...props}
>
{detectedTokens.length === 1
? t('numberOfNewTokensDetectedSingular')
: t('numberOfNewTokensDetectedPlural', [detectedTokens.length])}
</BannerAlert>
);
};
DetectedTokensBanner.propTypes = {
/**
* Handler to be passed to the DetectedTokenBanner component
*/
actionButtonOnClick: PropTypes.func.isRequired,
/**
* An additional className to the DetectedTokenBanner component
*/
className: PropTypes.string,
};

View File

@ -0,0 +1,14 @@
import React from 'react';
import { DetectedTokensBanner } from './detected-token-banner';
export default {
title: 'Components/Multichain/DetectedTokensBanner',
component: DetectedTokensBanner,
argTypes: {
actionButtonOnClick: { action: 'setShowDetectedTokens' },
},
};
export const DefaultStory = (args) => <DetectedTokensBanner {...args} />;
DefaultStory.storyName = 'Default';

View File

@ -0,0 +1,39 @@
import React from 'react';
import { renderWithProvider, screen, fireEvent } from '../../../../test/jest';
import configureStore from '../../../store/store';
import testData from '../../../../.storybook/test-data';
import { DetectedTokensBanner } from './detected-token-banner';
describe('DetectedTokensBanner', () => {
let setShowDetectedTokensSpy;
const args = {};
beforeEach(() => {
setShowDetectedTokensSpy = jest.fn();
args.actionButtonOnClick = setShowDetectedTokensSpy;
});
it('should render correctly', () => {
const store = configureStore(testData);
const { getByTestId, container } = renderWithProvider(
<DetectedTokensBanner {...args} />,
store,
);
expect(getByTestId('detected-token-banner')).toBeDefined();
expect(container).toMatchSnapshot();
});
it('should render number of tokens detected link', () => {
const store = configureStore(testData);
renderWithProvider(<DetectedTokensBanner {...args} />, store);
expect(
screen.getByText('3 new tokens found in this account'),
).toBeInTheDocument();
fireEvent.click(screen.getByText('Import tokens'));
expect(setShowDetectedTokensSpy).toHaveBeenCalled();
});
});

View File

@ -0,0 +1 @@
export { DetectedTokensBanner } from './detected-token-banner';

View File

@ -1,3 +1,6 @@
export { AccountListItem } from './account-list-item';
export { AccountListItemMenu } from './account-list-item-menu';
export { AccountListMenu } from './account-list-menu';
export { DetectedTokensBanner } from './detected-token-banner';
export { MultichainImportTokenLink } from './multichain-import-token-link';
export { MultichainTokenListItem } from './multichain-token-list-item';

View File

@ -1,2 +0,0 @@
@import 'account-list-item/index';
@import 'account-list-menu/index';

View File

@ -0,0 +1,9 @@
/**
* Please import your styles in order of atomicity.
* The most atomic styles should be imported first.
* This will help improve specificity and reduce the chance of
* unintended overrides.
**/
@import 'account-list-item/index';
@import 'account-list-menu/index';
@import 'multichain-token-list-item/multichain-token-list-item';

View File

@ -0,0 +1,75 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Import Token Link should match snapshot for goerli chainId 1`] = `
<div>
<div
class="box multichain-import-token-link box--flex-direction-row"
>
<div
class="box box--display-flex box--flex-direction-row box--align-items-center"
>
<button
class="box mm-text mm-button-base mm-button-base--size-md mm-button-link mm-text--body-md mm-text--color-primary-default box--display-inline-flex box--flex-direction-row box--justify-content-center box--align-items-center box--background-color-transparent"
data-testid="import-token-button"
>
<span
class="box mm-icon mm-icon--size-sm box--margin-inline-end-1 box--display-inline-block box--flex-direction-row box--color-inherit"
style="mask-image: url('./images/icons/add.svg');"
/>
Import tokens
</button>
</div>
<div
class="box box--padding-top-4 box--padding-bottom-4 box--display-flex box--flex-direction-row box--align-items-center"
>
<button
class="box mm-text mm-button-base mm-button-link mm-button-link--size-auto mm-text--body-md mm-text--color-primary-default box--display-inline-flex box--flex-direction-row box--justify-content-center box--align-items-center box--background-color-transparent"
data-testid="refresh-list-button"
>
<span
class="box mm-icon mm-icon--size-sm box--margin-inline-end-1 box--display-inline-block box--flex-direction-row box--color-inherit"
style="mask-image: url('./images/icons/refresh.svg');"
/>
Refresh list
</button>
</div>
</div>
</div>
`;
exports[`Import Token Link should match snapshot for mainnet chainId 1`] = `
<div>
<div
class="box multichain-import-token-link box--flex-direction-row"
>
<div
class="box box--display-flex box--flex-direction-row box--align-items-center"
>
<button
class="box mm-text mm-button-base mm-button-base--size-md mm-button-link mm-text--body-md mm-text--color-primary-default box--display-inline-flex box--flex-direction-row box--justify-content-center box--align-items-center box--background-color-transparent"
data-testid="import-token-button"
>
<span
class="box mm-icon mm-icon--size-sm box--margin-inline-end-1 box--display-inline-block box--flex-direction-row box--color-inherit"
style="mask-image: url('./images/icons/add.svg');"
/>
Import tokens
</button>
</div>
<div
class="box box--padding-top-4 box--padding-bottom-4 box--display-flex box--flex-direction-row box--align-items-center"
>
<button
class="box mm-text mm-button-base mm-button-link mm-button-link--size-auto mm-text--body-md mm-text--color-primary-default box--display-inline-flex box--flex-direction-row box--justify-content-center box--align-items-center box--background-color-transparent"
data-testid="refresh-list-button"
>
<span
class="box mm-icon mm-icon--size-sm box--margin-inline-end-1 box--display-inline-block box--flex-direction-row box--color-inherit"
style="mask-image: url('./images/icons/refresh.svg');"
/>
Refresh list
</button>
</div>
</div>
</div>
`;

View File

@ -0,0 +1 @@
export { MultichainImportTokenLink } from './multichain-import-token-link';

View File

@ -0,0 +1,87 @@
import React, { useContext } from 'react';
import { useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import Box from '../../ui/box/box';
import { ButtonLink, ICON_NAMES } from '../../component-library';
import {
AlignItems,
DISPLAY,
Size,
} from '../../../helpers/constants/design-system';
import { useI18nContext } from '../../../hooks/useI18nContext';
import { IMPORT_TOKEN_ROUTE } from '../../../helpers/constants/routes';
import { detectNewTokens } from '../../../store/actions';
import { MetaMetricsContext } from '../../../contexts/metametrics';
import { EVENT, EVENT_NAMES } from '../../../../shared/constants/metametrics';
import {
getIsTokenDetectionSupported,
getIsTokenDetectionInactiveOnMainnet,
} from '../../../selectors';
export const MultichainImportTokenLink = ({ className, ...props }) => {
const trackEvent = useContext(MetaMetricsContext);
const t = useI18nContext();
const history = useHistory();
const isTokenDetectionSupported = useSelector(getIsTokenDetectionSupported);
const isTokenDetectionInactiveOnMainnet = useSelector(
getIsTokenDetectionInactiveOnMainnet,
);
const isTokenDetectionAvailable =
isTokenDetectionSupported ||
isTokenDetectionInactiveOnMainnet ||
Boolean(process.env.IN_TEST);
return (
<Box
className={classnames('multichain-import-token-link', className)}
{...props}
>
<Box display={DISPLAY.FLEX} alignItems={AlignItems.center}>
<ButtonLink
size={Size.MD}
data-testid="import-token-button"
startIconName={ICON_NAMES.ADD}
onClick={() => {
history.push(IMPORT_TOKEN_ROUTE);
trackEvent({
event: EVENT_NAMES.TOKEN_IMPORT_BUTTON_CLICKED,
category: EVENT.CATEGORIES.NAVIGATION,
properties: {
location: 'Home',
},
});
}}
>
{isTokenDetectionAvailable
? t('importTokensCamelCase')
: t('importTokensCamelCase').charAt(0).toUpperCase() +
t('importTokensCamelCase').slice(1)}
</ButtonLink>
</Box>
<Box
display={DISPLAY.FLEX}
alignItems={AlignItems.center}
paddingBottom={4}
paddingTop={4}
>
<ButtonLink
startIconName={ICON_NAMES.REFRESH}
data-testid="refresh-list-button"
onClick={() => detectNewTokens()}
>
{t('refreshList')}
</ButtonLink>
</Box>
</Box>
);
};
MultichainImportTokenLink.propTypes = {
/**
* An additional className to apply to the TokenList.
*/
className: PropTypes.string,
};

View File

@ -0,0 +1,11 @@
import React from 'react';
import { MultichainImportTokenLink } from './multichain-import-token-link';
export default {
title: 'Components/Multichain/MultichainImportTokenLink',
component: MultichainImportTokenLink,
};
export const DefaultStory = () => <MultichainImportTokenLink />;
DefaultStory.storyName = 'Default';

View File

@ -0,0 +1,101 @@
import React from 'react';
import configureMockStore from 'redux-mock-store';
import { fireEvent, screen } from '@testing-library/react';
import { detectNewTokens } from '../../../store/actions';
import { renderWithProvider } from '../../../../test/lib/render-helpers';
import { MultichainImportTokenLink } from './multichain-import-token-link';
const mockPushHistory = jest.fn();
jest.mock('react-router-dom', () => {
const original = jest.requireActual('react-router-dom');
return {
...original,
useLocation: jest.fn(() => ({ search: '' })),
useHistory: () => ({
push: mockPushHistory,
}),
};
});
jest.mock('../../../store/actions.ts', () => ({
detectNewTokens: jest.fn(),
}));
describe('Import Token Link', () => {
it('should match snapshot for goerli chainId', () => {
const mockState = {
metamask: {
provider: {
chainId: '0x5',
},
},
};
const store = configureMockStore()(mockState);
const { container } = renderWithProvider(
<MultichainImportTokenLink />,
store,
);
expect(container).toMatchSnapshot();
});
it('should match snapshot for mainnet chainId', () => {
const mockState = {
metamask: {
provider: {
chainId: '0x1',
},
},
};
const store = configureMockStore()(mockState);
const { container } = renderWithProvider(
<MultichainImportTokenLink />,
store,
);
expect(container).toMatchSnapshot();
});
it('should detectNewTokens when clicking refresh', () => {
const mockState = {
metamask: {
provider: {
chainId: '0x5',
},
},
};
const store = configureMockStore()(mockState);
renderWithProvider(<MultichainImportTokenLink />, store);
const refreshList = screen.getByTestId('refresh-list-button');
fireEvent.click(refreshList);
expect(detectNewTokens).toHaveBeenCalled();
});
it('should push import token route', () => {
const mockState = {
metamask: {
provider: {
chainId: '0x5',
},
},
};
const store = configureMockStore()(mockState);
renderWithProvider(<MultichainImportTokenLink />, store);
const importToken = screen.getByTestId('import-token-button');
fireEvent.click(importToken);
expect(mockPushHistory).toHaveBeenCalledWith('/import-token');
});
});

View File

@ -0,0 +1,67 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`MultichainTokenListItem should render correctly 1`] = `
<div>
<div
class="box multichain-token-list-item box--display-flex box--gap-4 box--flex-direction-column"
data-testid="multichain-token-list-item"
>
<a
class="box multichain-token-list-item__container-cell box--padding-4 box--display-flex box--flex-direction-row"
data-testid="multichain-token-list-button"
href="#"
>
<div
class="box mm-badge-wrapper box--margin-right-3 box--display-inline-block box--flex-direction-row"
>
<div
class="box mm-text mm-avatar-base mm-avatar-base--size-md mm-avatar-token mm-avatar-token--with-halo mm-text--body-sm mm-text--text-transform-uppercase mm-text--color-text-default box--display-flex box--flex-direction-row box--justify-content-center box--align-items-center box--background-color-background-alternative box--rounded-full box--border-color-border-default box--border-style-solid box--border-width-1"
>
?
</div>
<div
class="box mm-badge-wrapper__badge-container mm-badge-wrapper__badge-container--circular-top-right box--flex-direction-row"
>
<div
class="box mm-text mm-avatar-base mm-avatar-base--size-xs mm-avatar-network mm-text--body-xs mm-text--text-transform-uppercase mm-text--color-text-default box--display-flex box--flex-direction-row box--justify-content-center box--align-items-center box--background-color-background-alternative box--rounded-full box--border-color-border-muted box--border-style-solid box--border-width-1"
>
<img
alt="undefined logo"
class="mm-avatar-network__network-image"
src="./images/eth_logo.svg"
/>
</div>
</div>
</div>
<div
class="box multichain-token-list-item__container-cell--text-container box--display-flex box--flex-direction-column box--width-full"
style="flex-grow: 1; overflow: hidden;"
>
<div
class="box box--display-flex box--gap-1 box--flex-direction-row box--justify-content-space-between"
>
<div
class="box box--flex-direction-row box--width-1/3"
>
<div>
<p
class="box mm-text mm-text--body-md mm-text--font-weight-medium mm-text--ellipsis mm-text--color-text-default box--flex-direction-row"
/>
</div>
</div>
<p
class="box mm-text mm-text--body-md mm-text--font-weight-medium mm-text--text-align-end mm-text--color-text-default box--flex-direction-row"
/>
</div>
<p
class="box mm-text mm-text--body-md mm-text--color-text-alternative box--flex-direction-row"
>
NaN
</p>
</div>
</a>
</div>
</div>
`;

View File

@ -0,0 +1 @@
export { MultichainTokenListItem } from './multichain-token-list-item';

View File

@ -0,0 +1,162 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import classnames from 'classnames';
import {
BLOCK_SIZES,
BorderColor,
DISPLAY,
FLEX_DIRECTION,
FONT_WEIGHT,
JustifyContent,
Size,
TextColor,
TextVariant,
TEXT_ALIGN,
} from '../../../helpers/constants/design-system';
import {
AvatarNetwork,
AvatarToken,
BadgeWrapper,
Text,
} from '../../component-library';
import Box from '../../ui/box/box';
import { getNativeCurrencyImage } from '../../../selectors';
import Tooltip from '../../ui/tooltip';
import { useI18nContext } from '../../../hooks/useI18nContext';
export const MultichainTokenListItem = ({
className,
onClick,
tokenSymbol,
tokenImage,
primary,
secondary,
title,
}) => {
const t = useI18nContext();
const primaryTokenImage = useSelector(getNativeCurrencyImage);
const dataTheme = document.documentElement.getAttribute('data-theme');
return (
<Box
className={classnames('multichain-token-list-item', className)}
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.COLUMN}
gap={4}
data-testid="multichain-token-list-item"
>
<Box
className="multichain-token-list-item__container-cell"
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.ROW}
padding={4}
as="a"
data-testid="multichain-token-list-button"
href="#"
onClick={(e) => {
e.preventDefault();
onClick();
}}
>
<BadgeWrapper
badge={
<AvatarNetwork
size={Size.XS}
name={tokenSymbol}
src={primaryTokenImage}
borderColor={
primaryTokenImage
? BorderColor.borderMuted
: BorderColor.borderDefault
}
/>
}
marginRight={3}
>
<AvatarToken
name={tokenSymbol}
src={tokenImage}
showHalo
borderColor={
tokenImage ? BorderColor.transparent : BorderColor.borderDefault
}
/>
</BadgeWrapper>
<Box
className="multichain-token-list-item__container-cell--text-container"
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.COLUMN}
width={BLOCK_SIZES.FULL}
style={{ flexGrow: 1, overflow: 'hidden' }}
>
<Box
display={DISPLAY.FLEX}
justifyContent={JustifyContent.spaceBetween}
gap={1}
>
<Box width={[BLOCK_SIZES.ONE_THIRD]}>
<Tooltip
position="bottom"
interactive
html={title}
disabled={title?.length < 12}
tooltipInnerClassName="multichain-token-list-item__tooltip"
theme={dataTheme === 'light' ? 'dark' : 'light'}
>
<Text
fontWeight={FONT_WEIGHT.MEDIUM}
variant={TextVariant.bodyMd}
ellipsis
>
{title === 'ETH' ? t('networkNameEthereum') : title}
</Text>
</Tooltip>
</Box>
<Text
fontWeight={FONT_WEIGHT.MEDIUM}
variant={TextVariant.bodyMd}
width={[BLOCK_SIZES.TWO_THIRD]}
textAlign={TEXT_ALIGN.END}
>
{secondary}
</Text>
</Box>
<Text color={TextColor.textAlternative}>
{Number(primary).toFixed(3)} {tokenSymbol}{' '}
</Text>
</Box>
</Box>
</Box>
);
};
MultichainTokenListItem.propTypes = {
/**
* An additional className to apply to the TokenList.
*/
className: PropTypes.string,
/**
* The onClick handler to be passed to the MultichainTokenListItem component
*/
onClick: PropTypes.func,
/**
* tokenSymbol represents the symbol of the Token
*/
tokenSymbol: PropTypes.string,
/**
* title represents the name of the token and if name is not available then Symbol
*/
title: PropTypes.string,
/**
* tokenImage represnts the image of the token icon
*/
tokenImage: PropTypes.string,
/**
* primary represents the balance
*/
primary: PropTypes.string,
/**
* secondary represents the balance in dollars
*/
secondary: PropTypes.string,
};

View File

@ -0,0 +1,8 @@
.multichain-token-list-item {
&__container-cell {
&:hover,
&:focus-within {
background-color: var(--color-background-default-hover);
}
}
}

View File

@ -0,0 +1,78 @@
import React from 'react';
import { Provider } from 'react-redux';
import testData from '../../../../.storybook/test-data';
import configureStore from '../../../store/store';
import { MultichainTokenListItem } from './multichain-token-list-item';
export default {
title: 'Components/Multichain/MultichainTokenListItem',
component: MultichainTokenListItem,
argTypes: {
tokenSymbol: {
control: 'text',
},
tokenImage: {
control: 'text',
},
primary: {
control: 'text',
},
secondary: {
control: 'text',
},
title: {
control: 'text',
},
onClick: {
action: 'onClick',
},
},
args: {
secondary: '$9.80 USD',
primary: '88.00687889',
tokenImage: './images/eth_logo.svg',
tokenSymbol: 'ETH',
title: 'Ethereum',
},
};
const customNetworkData = {
...testData,
metamask: { ...testData.metamask, nativeCurrency: '' },
};
const customNetworkStore = configureStore(customNetworkData);
const Template = (args) => {
return <MultichainTokenListItem {...args} />;
};
export const DefaultStory = Template.bind({});
export const ChaosStory = (args) => (
<div
style={{ width: '336px', border: '1px solid var(--color-border-muted)' }}
>
<MultichainTokenListItem {...args} />
</div>
);
ChaosStory.storyName = 'ChaosStory';
ChaosStory.args = {
title: 'Really long, long name',
secondary: '$94556756776.80 USD',
primary: '34449765768526.00',
};
export const NoImagesStory = Template.bind({});
NoImagesStory.decorators = [
(Story) => (
<Provider store={customNetworkStore}>
<Story />
</Provider>
),
];
NoImagesStory.args = {
tokenImage: '',
};

View File

@ -0,0 +1,57 @@
import React from 'react';
import configureMockStore from 'redux-mock-store';
import { fireEvent } from '@testing-library/react';
import { renderWithProvider } from '../../../../test/lib/render-helpers';
import { MultichainTokenListItem } from './multichain-token-list-item';
const state = {
metamask: {
provider: {
ticker: 'ETH',
nickname: '',
chainId: '0x1',
type: 'mainnet',
},
useTokenDetection: false,
nativeCurrency: 'ETH',
},
};
describe('MultichainTokenListItem', () => {
const props = {
onClick: jest.fn(),
};
it('should render correctly', () => {
const store = configureMockStore()(state);
const { getByTestId, container } = renderWithProvider(
<MultichainTokenListItem />,
store,
);
expect(getByTestId('multichain-token-list-item')).toBeDefined();
expect(container).toMatchSnapshot();
});
it('should render with custom className', () => {
const store = configureMockStore()(state);
const { getByTestId } = renderWithProvider(
<MultichainTokenListItem className="multichain-token-list-item-test" />,
store,
);
expect(getByTestId('multichain-token-list-item')).toHaveClass(
'multichain-token-list-item-test',
);
});
it('handles click action and fires onClick', () => {
const store = configureMockStore()(state);
const { queryByTestId } = renderWithProvider(
<MultichainTokenListItem {...props} />,
store,
);
fireEvent.click(queryByTestId('multichain-token-list-button'));
expect(props.onClick).toHaveBeenCalled();
});
});

View File

@ -26,19 +26,19 @@
}
}
&[x-placement^=top] .tippy-tooltip.tippy-tooltip--mm-custom-theme [x-arrow] {
&[x-placement^='top'] .tippy-tooltip.tippy-tooltip--mm-custom-theme [x-arrow] {
border-top-color: var(--color-background-default);
}
&[x-placement^=right] .tippy-tooltip.tippy-tooltip--mm-custom-theme [x-arrow] {
&[x-placement^='right'] .tippy-tooltip.tippy-tooltip--mm-custom-theme [x-arrow] {
border-right-color: var(--color-background-default);
}
&[x-placement^=left] .tippy-tooltip.tippy-tooltip--mm-custom-theme [x-arrow] {
&[x-placement^='left'] .tippy-tooltip.tippy-tooltip--mm-custom-theme [x-arrow] {
border-left-color: var(--color-background-default);
}
&[x-placement^=bottom] .tippy-tooltip.tippy-tooltip--mm-custom-theme [x-arrow] {
&[x-placement^='bottom'] .tippy-tooltip.tippy-tooltip--mm-custom-theme [x-arrow] {
border-bottom-color: var(--color-background-default);
}
}

View File

@ -10,8 +10,8 @@
@import './base-styles.scss';
@import '../components/component-library/component-library-components.scss';
@import '../components/app/app-components';
@import '../components/multichain/index.scss';
@import '../components/ui/ui-components';
@import '../components/multichain/multichain-components.scss';
@import '../pages/pages';
@import './errors.scss';
@import './loading.scss';