Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions development/build/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@ async function defineAndRunBuildTasks() {
'opr',
// for @popperjs/core and snap simple keyring site
'devicePixelRatio',
// for @tanstack/react-virtual
'ResizeObserver',
'setTimeout',
'clearTimeout',
];

if (
Expand Down
1 change: 1 addition & 0 deletions development/webpack/utils/plugins/LavamoatPlugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export const lavamoatPlugin = (args: Args) =>
'Date',
'setTimeout',
'clearTimeout',
'ResizeObserver',
// globals sentry needs to function
'__SENTRY__',
'appState',
Expand Down
18 changes: 18 additions & 0 deletions lavamoat/browserify/beta/policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -2598,6 +2598,24 @@
"browserify>browser-resolve": true
}
},
"@tanstack/react-virtual": {
"globals": {
"document": true,
"scrollY": true
},
"packages": {
"@tanstack/react-virtual>@tanstack/virtual-core": true,
"react": true,
"react-dom": true
}
},
"@tanstack/react-virtual>@tanstack/virtual-core": {
"globals": {
"console.info": true,
"console.warn": true,
"requestAnimationFrame": true
}
},
"@toruslabs/eccrypto": {
"globals": {
"crypto": true,
Expand Down
18 changes: 18 additions & 0 deletions lavamoat/browserify/experimental/policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -2598,6 +2598,24 @@
"browserify>browser-resolve": true
}
},
"@tanstack/react-virtual": {
"globals": {
"document": true,
"scrollY": true
},
"packages": {
"@tanstack/react-virtual>@tanstack/virtual-core": true,
"react": true,
"react-dom": true
}
},
"@tanstack/react-virtual>@tanstack/virtual-core": {
"globals": {
"console.info": true,
"console.warn": true,
"requestAnimationFrame": true
}
},
"@toruslabs/eccrypto": {
"globals": {
"crypto": true,
Expand Down
18 changes: 18 additions & 0 deletions lavamoat/browserify/flask/policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -2598,6 +2598,24 @@
"browserify>browser-resolve": true
}
},
"@tanstack/react-virtual": {
"globals": {
"document": true,
"scrollY": true
},
"packages": {
"@tanstack/react-virtual>@tanstack/virtual-core": true,
"react": true,
"react-dom": true
}
},
"@tanstack/react-virtual>@tanstack/virtual-core": {
"globals": {
"console.info": true,
"console.warn": true,
"requestAnimationFrame": true
}
},
"@toruslabs/eccrypto": {
"globals": {
"crypto": true,
Expand Down
18 changes: 18 additions & 0 deletions lavamoat/browserify/main/policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -2598,6 +2598,24 @@
"browserify>browser-resolve": true
}
},
"@tanstack/react-virtual": {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding virtualisation library. To investigate the document global property usage.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From light inspection, seems to be used in the libraries examples. Uses document.getElementById.
https://github.com/search?q=repo%3ATanStack%2Fvirtual%20document.&type=code

"globals": {
"document": true,
"scrollY": true
},
"packages": {
"@tanstack/react-virtual>@tanstack/virtual-core": true,
"react": true,
"react-dom": true
}
},
"@tanstack/react-virtual>@tanstack/virtual-core": {
"globals": {
"console.info": true,
"console.warn": true,
"requestAnimationFrame": true
}
},
"@toruslabs/eccrypto": {
"globals": {
"crypto": true,
Expand Down
18 changes: 18 additions & 0 deletions lavamoat/webpack/policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -2684,6 +2684,24 @@
"@metamask/profile-sync-controller>siwe>@stablelib/random>@stablelib/wipe": true
}
},
"@tanstack/react-virtual": {
"globals": {
"document": true,
"scrollY": true
},
"packages": {
"@tanstack/react-virtual>@tanstack/virtual-core": true,
"react": true,
"react-dom": true
}
},
"@tanstack/react-virtual>@tanstack/virtual-core": {
"globals": {
"console.info": true,
"console.warn": true,
"requestAnimationFrame": true
}
},
"@toruslabs/eccrypto": {
"globals": {
"Buffer.alloc": true,
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,7 @@
"@sentry/utils": "^8.33.1",
"@solana/addresses": "2.0.0-rc.4",
"@swc/core": "^1.13.2",
"@tanstack/react-virtual": "^3.10.8",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice addition. I remember doing a PoC with the windowVirtualiser for our modals/popups with this (e.g. accounts list)
Cannot wait to start using this!

"@trezor/connect-web": "~9.6.0",
"@zxing/browser": "^0.1.5",
"@zxing/library": "0.21.3",
Expand Down
23 changes: 0 additions & 23 deletions ui/components/app/assets/asset-list/cells/asset-title.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React from 'react';
import {
Display,
FontWeight,
TextVariant,
} from '../../../../../helpers/constants/design-system';
Expand All @@ -10,7 +9,6 @@ import {
networkTitleOverrides,
} from '../../util/networkTitleOverrides';
import { useI18nContext } from '../../../../../hooks/useI18nContext';
import Tooltip from '../../../../ui/tooltip';

type AssetCellTitleProps = {
title: string;
Expand All @@ -19,27 +17,6 @@ type AssetCellTitleProps = {
export const AssetCellTitle = ({ title }: AssetCellTitleProps) => {
const t = useI18nContext();

if (title && title.length > 12) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are not using a tooltip for long titles anymore?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Can potentially use the title attribute for default browser experience)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are issues with this tooltip and the list. Morever, with the upcoming Side Panel mode where the Extension width can be adjusted to any size, basing it an arbitrary "length > 12" doesn't make sense. Truncation with ellipsis via CSS is more flexible.

return (
<Tooltip
position="bottom"
html={title}
wrapperClassName="token-cell-title--ellipsis"
>
<Text
as="span"
data-testid="multichain-token-list-item-token-name"
fontWeight={FontWeight.Medium}
variant={TextVariant.bodyMd}
display={Display.Block}
ellipsis
>
{networkTitleOverrides(t as TranslateFunction, { title })}
</Text>
</Tooltip>
);
}

// non-ellipsized title
return (
<Text
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
BlockSize,
JustifyContent,
} from '../../../../../helpers/constants/design-system';
import { ASSET_CELL_HEIGHT } from '../../constants';

type GenericAssetCellLayoutProps = {
onClick?: () => void;
Expand Down Expand Up @@ -54,7 +55,7 @@ export default function GenericAssetCellLayout({
paddingRight={4}
width={BlockSize.Full}
style={{
height: 62,
height: ASSET_CELL_HEIGHT,
cursor: onClick ? 'pointer' : 'auto',
backgroundColor:
!disableHover && isHovered
Expand Down
2 changes: 2 additions & 0 deletions ui/components/app/assets/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ export const ACCOUNT_TYPE_LABELS: Partial<Record<KeyringAccountType, string>> =
[BtcAccountType.P2wpkh]: 'Native SegWit',
[BtcAccountType.P2tr]: 'Taproot',
};

export const ASSET_CELL_HEIGHT = 62;
44 changes: 35 additions & 9 deletions ui/components/app/assets/token-list/token-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import React, { useContext, useEffect, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { type CaipChainId, type Hex } from '@metamask/utils';
import { NON_EVM_TESTNET_IDS } from '@metamask/multichain-network-controller';
import { useVirtualizer } from '@tanstack/react-virtual';
import TokenCell from '../token-cell';
import { ASSET_CELL_HEIGHT } from '../constants';
import {
getEnabledNetworksByNamespace,
getIsMultichainAccountsState2Enabled,
Expand Down Expand Up @@ -36,6 +38,7 @@ import { SafeChain } from '../../../../pages/settings/networks-tab/networks-form
import { isGlobalNetworkSelectorRemoved } from '../../../../selectors/selectors';
import { isEvmChainId } from '../../../../../shared/lib/asset-utils';
import { sortAssetsWithPriority } from '../util/sortAssetsWithPriority';
import { useScrollContainer } from '../../../../contexts/scroll-container';

type TokenListProps = {
onTokenClick: (chainId: string, address: string) => void;
Expand All @@ -45,6 +48,7 @@ type TokenListProps = {
// TODO: Fix in https://github.com/MetaMask/metamask-extension/issues/31860
// eslint-disable-next-line @typescript-eslint/naming-convention
function TokenList({ onTokenClick, safeChains }: TokenListProps) {
const scrollContainerRef = useScrollContainer();
Copy link
Contributor Author

@n3ps n3ps Nov 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Get the scrolling container

const isEvm = useSelector(getIsEvmMultichainNetworkSelected);
const newTokensImported = useSelector(getNewTokensImported);
const currentNetwork = useSelector(getSelectedMultichainNetworkConfiguration);
Expand Down Expand Up @@ -145,6 +149,13 @@ function TokenList({ onTokenClick, safeChains }: TokenListProps) {
allEnabledNetworksForAllNamespaces,
]);

const virtualizer = useVirtualizer({
count: sortedFilteredTokens.length,
getScrollElement: () => scrollContainerRef?.current || null,
estimateSize: () => ASSET_CELL_HEIGHT,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does this fair with responsive design (increase/decrease window size/font size)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

virtualized.mov

overscan: 5,
});

useEffect(() => {
if (sortedFilteredTokens) {
endTrace({ name: TraceName.AccountOverviewAssetListTab });
Expand Down Expand Up @@ -179,24 +190,39 @@ function TokenList({ onTokenClick, safeChains }: TokenListProps) {
});
};

const virtualItems = virtualizer.getVirtualItems();

return (
<>
{sortedFilteredTokens.map((token: TokenWithFiatAmount) => {
<div
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. The list container

Copy link
Contributor

@Prithpal-Sooriya Prithpal-Sooriya Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we might want to reuse tanstack virtual for other areas, can we create a custom component that abstracts away some of these steps?

const VirtualScrollView = (...) => {
  // Step 2...
  const virtualiser = useVirtualiser({ ... })
  
  // Step 3...
  return (
    <div ...>
      {/* Step 4... */}
      virtualItems.map(virtualItem => {
        return (
          <div ...>
              {renderItem({ item, virtualItem })}
          </div>
        )
      })
    </div>
  )
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eventually, we can abstract this once we get to see usage patterns from other tabs, so that the API and props match our needs.

className="relative w-full"
style={{
height: `${virtualizer.getTotalSize()}px`,
}}
>
{virtualItems.map((virtualItem) => {
const token = sortedFilteredTokens[virtualItem.index];
const isNonEvmTestnet = NON_EVM_TESTNET_IDS.includes(
token.chainId as CaipChainId,
);

return (
<TokenCell
<div
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. The virtualized elements

key={`${token.chainId}-${token.symbol}-${token.address}`}
token={token}
privacyMode={privacyMode}
onClick={isNonEvmTestnet ? undefined : handleTokenClick(token)}
safeChains={safeChains}
/>
className="absolute top-0 left-0 w-full"
style={{
transform: `translateY(${virtualItem.start}px)`,
}}
>
<TokenCell
token={token}
privacyMode={privacyMode}
onClick={isNonEvmTestnet ? undefined : handleTokenClick(token)}
safeChains={safeChains}
/>
</div>
);
})}
</>
</div>
);
}

Expand Down
35 changes: 35 additions & 0 deletions ui/contexts/scroll-container.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React, { createContext, useContext, useRef } from 'react';

const ScrollContainerContext =
createContext<React.RefObject<HTMLDivElement> | null>(null);

/**
* Provides a ref to this container element for its child components
*
* @param props - HTML div attributes
* @param props.children - Child components to render inside the container
* @returns A div element with a ref accessible via useScrollContainer
*/
export const ScrollContainer = ({
children,
...props
}: React.HTMLAttributes<HTMLDivElement>) => {
const scrollRef = useRef<HTMLDivElement>(null);

return (
<ScrollContainerContext.Provider value={scrollRef}>
<div ref={scrollRef} {...props}>
{children}
</div>
</ScrollContainerContext.Provider>
);
};

/**
* Hook to access the scroll container ref from any child component
*
* @returns Ref to the scroll container
*/
export const useScrollContainer = () => {
return useContext(ScrollContainerContext);
};
Original file line number Diff line number Diff line change
Expand Up @@ -134,25 +134,12 @@ exports[`DeFiDetailsPage renders defi asset page 1`] = `
<div
class="flex flex-row gap-2 min-w-0"
>
<div
class="token-cell-title--ellipsis"
<p
class="mm-box mm-text mm-text--body-md mm-text--font-weight-medium mm-text--ellipsis mm-box--color-text-default"
data-testid="multichain-token-list-item-token-name"
>
<div
aria-describedby="tippy-tooltip-1"
class=""
data-original-title="null"
data-tooltipped=""
style="display: inline;"
tabindex="0"
>
<span
class="mm-box mm-text mm-text--body-md mm-text--font-weight-medium mm-text--ellipsis mm-box--display-block mm-box--color-text-default"
data-testid="multichain-token-list-item-token-name"
>
Liquid staked Ether 2.0
</span>
</div>
</div>
Liquid staked Ether 2.0
</p>
</div>
</div>
<p
Expand Down
5 changes: 3 additions & 2 deletions ui/pages/home/home.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import ConnectedSites from '../connected-sites';
import ConnectedAccounts from '../connected-accounts';
import { isMv3ButOffscreenDocIsMissing } from '../../../shared/modules/mv3.utils';
import ActionableMessage from '../../components/ui/actionable-message/actionable-message';
import { ScrollContainer } from '../../contexts/scroll-container';

import {
FontWeight,
Expand Down Expand Up @@ -887,7 +888,7 @@ export default class Home extends PureComponent {
!isSocialLoginFlow;

return (
<div className="main-container main-container--has-shadow">
<ScrollContainer className="main-container main-container--has-shadow">
Copy link
Contributor Author

@n3ps n3ps Nov 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Share a ref of the scrolling container

<Route path={CONNECTED_ROUTE} component={ConnectedSites} exact />
<Route
path={CONNECTED_ACCOUNTS_ROUTE}
Expand Down Expand Up @@ -945,7 +946,7 @@ export default class Home extends PureComponent {
</div>
{this.renderNotifications()}
</div>
</div>
</ScrollContainer>
);
}
}
Loading
Loading