Skip to content

Commit 26d495c

Browse files
authored
chore: fix progress spinner jank in v3 asset picker (#3781)
1 parent 6117514 commit 26d495c

4 files changed

Lines changed: 113 additions & 117 deletions

File tree

src/components/TokenBalance.tsx

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react';
2-
import { Stack, Typography, CircularProgress } from '@mui/material';
2+
import { Box, Skeleton, Stack, Typography } from '@mui/material';
33
import { amount as sdkAmount } from '@wormhole-foundation/sdk';
44
import { useTheme } from '@mui/material/styles';
55

@@ -13,33 +13,45 @@ const TokenBalance = ({
1313
price?: string | null;
1414
}) => {
1515
const theme = useTheme();
16+
const shouldHideBalance = !balance || sdkAmount.display(balance) === '0';
17+
const containerWidth = 120; // Reserve space to prevent layout shift
1618

17-
if (isFetching && balance === null) {
19+
if (isFetching) {
1820
return (
19-
<Stack alignItems="flex-end">
20-
<Typography fontSize={14}>
21-
<CircularProgress size={24} />
22-
</Typography>
23-
</Stack>
21+
<Box sx={{ width: containerWidth }}>
22+
<Stack alignItems="flex-end">
23+
<Skeleton
24+
variant="rounded"
25+
height={14}
26+
width="80%"
27+
sx={{ borderRadius: '8px' }}
28+
/>
29+
<Skeleton
30+
variant="rounded"
31+
height={14}
32+
width="60%"
33+
sx={{ borderRadius: '8px', mt: 0.5 }}
34+
/>
35+
</Stack>
36+
</Box>
2437
);
2538
}
2639

27-
// Hide 0 / $0 for both source and destination lists
28-
const shouldHideBalance = !balance || sdkAmount.display(balance) === '0';
29-
3040
return (
31-
<Stack alignItems="flex-end">
32-
<Typography fontSize={14}>
33-
{balance && !shouldHideBalance
34-
? sdkAmount.display(sdkAmount.truncate(balance, 6))
35-
: ''}
36-
</Typography>
37-
{price && !shouldHideBalance && (
38-
<Typography color={theme.palette.text.secondary} fontSize="10px">
39-
{price}
41+
<Box sx={{ width: containerWidth }}>
42+
<Stack alignItems="flex-end">
43+
<Typography fontSize={14}>
44+
{balance && !shouldHideBalance
45+
? sdkAmount.display(sdkAmount.truncate(balance, 6))
46+
: ''}
4047
</Typography>
41-
)}
42-
</Stack>
48+
{price && !shouldHideBalance && (
49+
<Typography color={theme.palette.text.secondary} fontSize="10px">
50+
{price}
51+
</Typography>
52+
)}
53+
</Stack>
54+
</Box>
4355
);
4456
};
4557

src/views/v3/Bridge/AssetPicker/SearchableList/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ function SearchableList<T>(props: SearchableListProps<T>): ReactNode {
3434
padding: 0,
3535
},
3636
searchList: {
37-
marginTop: '12px',
37+
marginTop: 0,
3838
overflow: 'auto',
3939
},
4040
};
@@ -62,6 +62,7 @@ function SearchableList<T>(props: SearchableListProps<T>): ReactNode {
6262
placeholder={props.searchPlaceholder}
6363
/>
6464
<List
65+
disablePadding
6566
sx={{ ...styles.searchList, ...scrollbarStyles }}
6667
data-testid={props.dataTestId}
6768
>

src/views/v3/Bridge/AssetPicker/TokenList.tsx

Lines changed: 46 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,13 @@
11
import React, { Fragment, useMemo } from 'react';
2-
import { Box, Card, CardContent, Skeleton, useTheme } from '@mui/material';
3-
import CircularProgress from '@mui/material/CircularProgress';
4-
import ListItemButton from '@mui/material/ListItemButton';
2+
import { Card, CardContent, useTheme } from '@mui/material';
53
import Typography from '@mui/material/Typography';
6-
74
import type { ChainConfig } from 'config/types';
85
import type { Token } from 'config/tokens';
96
import type { WalletData } from 'store/wallet';
107
import SearchableList from 'views/v3/Bridge/AssetPicker/SearchableList';
118
import TokenItem from 'views/v3/Bridge/AssetPicker/TokenItem';
129
import { getUSDFormat, calculateUSDPriceRaw } from 'utils';
1310
import config from 'config';
14-
import { useTokens } from 'contexts/TokensContext';
1511
import type { Balances } from 'utils/wallet/types';
1612
import { useTokenListWithSearch } from 'hooks/useTokenListWithSearch';
1713
import TokenSectionHeader from './TokenSectionHeader';
@@ -38,7 +34,6 @@ type Props = {
3834
const TokenList = (props: Props) => {
3935
const theme = useTheme();
4036
const tokenPastingIsEnabled = config.ui.disableUserInputtedTokens !== true;
41-
const { isFetchingToken } = useTokens();
4237

4338
const { sortedTokens, tokenPrices } = useTokenListWithSearch({
4439
baseTokenList: props.tokenList,
@@ -87,10 +82,10 @@ const TokenList = (props: Props) => {
8782
fontSize: 14,
8883
marginBottom: '8px',
8984
},
90-
tokenLoader: {
91-
padding: 0,
85+
tokenLoaderRow: {
9286
display: 'flex',
9387
justifyContent: 'space-between',
88+
padding: '8px 16px',
9489
},
9590
tokenList: {
9691
maxHeight: '360px',
@@ -118,9 +113,6 @@ const TokenList = (props: Props) => {
118113
return 'ready';
119114
}, [props.isFetching, sortedTokens.length]);
120115

121-
const shouldShowLoadingState = listState === 'loading';
122-
const shouldShowEmptyMessage = listState === 'empty';
123-
124116
// Build sectioned list for source picker when not searching
125117
const isGroupingEnabled = props.isSource && !props.searchQuery;
126118
const isWalletConnected = Boolean(props.wallet?.address);
@@ -132,76 +124,52 @@ const TokenList = (props: Props) => {
132124
balances: props.balances,
133125
});
134126

135-
const searchList = (
136-
<SearchableList<Token>
137-
searchPlaceholder={placeholder}
138-
sx={styles.tokenList}
139-
dataTestId="token-search-list"
140-
searchQuery={props.searchQuery}
141-
listTitle={shouldShowEmptyMessage ? emptyMessage : ''}
142-
loading={
143-
shouldShowLoadingState &&
144-
[1, 2, 3].map((x) => (
145-
<ListItemButton sx={styles.tokenLoader} dense key={x}>
146-
<Box padding="8px 16px">
147-
<Skeleton variant="circular" width="36px" height="36px" />
148-
</Box>
149-
</ListItemButton>
150-
))
151-
}
152-
items={listItems}
153-
onQueryChange={(query) => {
154-
props.onSearchQueryChange(query);
155-
}}
156-
renderFn={(token: Token, index: number) => {
157-
const balance = props.balances?.[token.key]?.balance;
158-
const tokenPrice = tokenPrices.get(token.key);
159-
const price =
160-
balance && tokenPrice !== undefined
161-
? getUSDFormat(calculateUSDPriceRaw(tokenPrice, balance, token))
162-
: null;
163-
164-
// Do not dim when no wallet is connected
165-
const isRestSection =
166-
isGroupingEnabled && isWalletConnected && index >= ownedCount;
167-
168-
return (
169-
<Fragment key={token.key}>
170-
<TokenSectionHeader
171-
index={index}
172-
ownedCount={ownedCount}
173-
isGroupingEnabled={isGroupingEnabled}
174-
/>
175-
<TokenItem
176-
key={token.key}
177-
token={token}
178-
chain={props.selectedChainConfig.sdkName}
179-
onClick={() => props.onSelectToken(token)}
180-
isSource={props.isSource}
181-
balance={balance}
182-
price={price}
183-
isSelected={token.key === props.selectedToken?.key}
184-
isFetchingBalance={props.isFetchingBalances}
185-
isDimmed={isRestSection}
186-
/>
187-
</Fragment>
188-
);
189-
}}
190-
/>
191-
);
192-
193127
return (
194128
<Card sx={styles.card} variant="elevation">
195129
<CardContent sx={styles.tokenListContainer}>
196-
<Box sx={{ display: 'flex', padding: '0 16px' }}>
197-
{isFetchingToken || props.isFetchingBalances ? (
198-
<CircularProgress
199-
sx={{ alignSelf: 'flex-end', marginBottom: '12px' }}
200-
size={14}
201-
/>
202-
) : null}
203-
</Box>
204-
{searchList}
130+
<SearchableList<Token>
131+
searchPlaceholder={placeholder}
132+
sx={styles.tokenList}
133+
dataTestId="token-search-list"
134+
searchQuery={props.searchQuery}
135+
listTitle={listState === 'empty' ? emptyMessage : ''}
136+
items={listItems}
137+
onQueryChange={props.onSearchQueryChange}
138+
renderFn={(token: Token, index: number) => {
139+
const balance = props.balances?.[token.key]?.balance;
140+
const tokenPrice = tokenPrices.get(token.key);
141+
const price =
142+
balance && tokenPrice !== undefined
143+
? getUSDFormat(calculateUSDPriceRaw(tokenPrice, balance, token))
144+
: null;
145+
146+
// Do not dim when no wallet is connected
147+
const isRestSection =
148+
isGroupingEnabled && isWalletConnected && index >= ownedCount;
149+
150+
return (
151+
<Fragment key={token.key}>
152+
<TokenSectionHeader
153+
index={index}
154+
ownedCount={ownedCount}
155+
isGroupingEnabled={isGroupingEnabled}
156+
/>
157+
<TokenItem
158+
key={token.key}
159+
token={token}
160+
chain={props.selectedChainConfig.sdkName}
161+
onClick={() => props.onSelectToken(token)}
162+
isSource={props.isSource}
163+
balance={balance}
164+
price={price}
165+
isSelected={token.key === props.selectedToken?.key}
166+
isFetchingBalance={props.isFetchingBalances}
167+
isDimmed={isRestSection}
168+
/>
169+
</Fragment>
170+
);
171+
}}
172+
/>
205173
</CardContent>
206174
</Card>
207175
);

src/views/v3/Bridge/AssetPicker/TokenSectionHeader.tsx

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { Box, useTheme } from '@mui/material';
2-
32
import { Typography } from '@mui/material';
43
import React from 'react';
54

@@ -14,23 +13,39 @@ const TokenSectionHeader = ({
1413
}) => {
1514
const theme = useTheme();
1615

16+
if (!isGroupingEnabled) {
17+
return null;
18+
}
19+
20+
let label: string | null = null;
21+
22+
if (index === 0 && ownedCount > 0) {
23+
label = 'Your tokens';
24+
}
25+
26+
if (index === ownedCount) {
27+
label = 'All tokens';
28+
}
29+
30+
if (!label) {
31+
return null;
32+
}
33+
1734
return (
18-
<>
19-
{isGroupingEnabled && index === 0 && ownedCount > 0 && (
20-
<Box sx={{ padding: '4px 16px' }}>
21-
<Typography fontSize={14} color={theme.palette.text.secondary}>
22-
Your tokens
23-
</Typography>
24-
</Box>
25-
)}
26-
{isGroupingEnabled && index === ownedCount && (
27-
<Box sx={{ padding: '4px 16px' }}>
28-
<Typography fontSize={14} color={theme.palette.text.secondary}>
29-
All tokens
30-
</Typography>
31-
</Box>
32-
)}
33-
</>
35+
<Box
36+
sx={{
37+
position: 'sticky',
38+
top: 0,
39+
zIndex: 2,
40+
padding: `${theme.spacing(1)} 16px 4px 16px`,
41+
background: theme.palette.input.background,
42+
transition: 'background-color 150ms ease',
43+
}}
44+
>
45+
<Typography fontSize={14} color={theme.palette.text.secondary}>
46+
{label}
47+
</Typography>
48+
</Box>
3449
);
3550
};
3651

0 commit comments

Comments
 (0)