Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
15b5fe7
feat: add ChainPanel component and integrate cross-chain selection
fairlighteth Nov 7, 2025
805ad39
refactor: enhance ChainPanel styling and structure
fairlighteth Nov 7, 2025
1ab99db
refactor: streamline token selection components and improve UI structure
fairlighteth Nov 7, 2025
67278e3
feat: replace active chain icon with SVG and update styling
fairlighteth Nov 7, 2025
6cd9ee0
feat: implement SelectTokenWidget with controller and view separation…
fairlighteth Nov 8, 2025
d04dd12
refactor: enhance token selection components by separating rendering …
fairlighteth Nov 8, 2025
d7a7799
chore: update @tanstack/react-virtual dependency to version 3.13.12 i…
fairlighteth Nov 8, 2025
23214a3
refactor: simplify token selection logic and enhance ChainPanel state…
fairlighteth Nov 8, 2025
05cd344
refactor: optimize token rendering logic in TokensVirtualList using u…
fairlighteth Nov 9, 2025
16be22a
refactor: enhance token search results rendering with virtualized lis…
fairlighteth Nov 9, 2025
d587f33
feat: add guide banner to TokenSearchContent for custom token addition
fairlighteth Nov 9, 2025
c091aad
feat: implement recent tokens feature in SelectTokenWidget
fairlighteth Nov 10, 2025
135becd
refactor: improve FavoriteTokensList component structure and styling
fairlighteth Nov 10, 2025
2ca306a
refactor: enhance styling and structure of token selection components
fairlighteth Nov 10, 2025
2c95cd7
refactor: improve ChainPanel functionality with scroll detection and …
fairlighteth Nov 10, 2025
83abee2
refactor: implement chain-specific accent colors and improve styling …
fairlighteth Nov 10, 2025
39d4e8f
refactor: streamline token selection and recent tokens handling in Se…
fairlighteth Nov 10, 2025
97514e7
refactor: enhance recent tokens management and integrate active chain…
fairlighteth Nov 10, 2025
beeeb1f
refactor: enhance SelectTokenWidget with mobile chain panel and impro…
fairlighteth Nov 10, 2025
0f4b0ea
refactor: optimize token sorting logic in TokensVirtualList for bette…
fairlighteth Nov 11, 2025
229fa86
refactor: modularize SelectTokenWidget dependencies and enhance recen…
fairlighteth Nov 11, 2025
f60c049
refactor: add controllerModalProps and controllerViewState for Select…
fairlighteth Nov 11, 2025
96df869
refactor: add accent color support for chains in ChainsSelector compo…
fairlighteth Nov 11, 2025
3b9249d
refactor: modularize SelectTokenWidget with new MobileChainPanelPortal
fairlighteth Nov 11, 2025
b483c0e
refactor: update MobileChainSelector styling and label handling for i…
fairlighteth Nov 11, 2025
64375b4
refactor: adjust styling and layout in ChainPanel and MobileChainSele…
fairlighteth Nov 11, 2025
19204ab
refactor: improve text color handling and font styles in ChainPanel
fairlighteth Nov 11, 2025
9b0ae61
refactor: implement dynamic visibility for chains in MobileChainSelec…
fairlighteth Nov 11, 2025
f6f6a8a
fix: correct displayed count of networks in MobileChainSelector
fairlighteth Nov 11, 2025
b0f5089
refactor: enhance Popover component with lazy portal mounting and imp…
fairlighteth Nov 11, 2025
34d2785
feat: implement useDeferredVisibility hook for improved lazy loading …
fairlighteth Nov 11, 2025
d23e094
refactor: optimize TokenListItem rendering by deferring balance forma…
fairlighteth Nov 11, 2025
fe81939
refactor: simplify TokenLogo component
fairlighteth Nov 11, 2025
53cc8a7
refactor: unify token selection handling across components with Token…
fairlighteth Nov 12, 2025
b818ca5
refactor: enhance SelectTokenWidget to support dynamic modal titles b…
fairlighteth Nov 12, 2025
db10011
feat: add chain sorting utility and tests for consistent chain order …
fairlighteth Nov 13, 2025
a1d5dbe
refactor: enhance MobileChainSelector with scrollable chain display a…
fairlighteth Nov 13, 2025
8b77893
Merge remote-tracking branch 'origin/develop' into feat/cross-token-s…
fairlighteth Nov 13, 2025
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
122 changes: 98 additions & 24 deletions apps/cowswap-frontend/src/common/pure/VirtualList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ReactNode, useCallback, useRef } from 'react'
import { ReactNode, useCallback, useLayoutEffect, useRef } from 'react'

import { useVirtualizer, VirtualItem } from '@tanstack/react-virtual'
import ms from 'ms.macro'
Expand All @@ -7,15 +7,72 @@ import { ListInner, ListScroller, ListWrapper, LoadingRows } from './styled'

const scrollDelay = ms`400ms`

// TODO: Add proper return type annotation
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
const threeDivs = () => (
<>
<div />
<div />
<div />
</>
)
const LoadingPlaceholder: () => ReactNode = () => {
return (
<>
<div />
<div />
<div />
</>
)
}

interface VirtualListRowProps<T> {
item: VirtualItem
loading?: boolean
items: T[]
getItemView(items: T[], item: VirtualItem): ReactNode
measureElement(element: Element | null): void
}

function VirtualListRow<T>({ item, loading, items, getItemView, measureElement }: VirtualListRowProps<T>): ReactNode {
if (loading) {
return (
<LoadingRows>
<LoadingPlaceholder />
</LoadingRows>
)
}

return (
<div data-index={item.index} ref={measureElement}>
{getItemView(items, item)}
</div>
)
}

interface VirtualListRowsProps<T> {
virtualItems: VirtualItem[]
loading?: boolean
items: T[]
getItemView(items: T[], item: VirtualItem): ReactNode
measureElement(element: Element | null): void
}

function renderVirtualListRows<T>({
virtualItems,
loading,
items,
getItemView,
measureElement,
}: VirtualListRowsProps<T>): ReactNode[] {
const elements: ReactNode[] = []

for (const item of virtualItems) {
elements.push(
<VirtualListRow
key={item.key}
item={item}
loading={loading}
items={items}
getItemView={getItemView}
measureElement={measureElement}
/>,
)
}

return elements
}

interface VirtualListProps<T> {
id?: string
Expand All @@ -26,18 +83,18 @@ interface VirtualListProps<T> {
loading?: boolean
estimateSize?: () => number
children?: ReactNode
scrollResetKey?: string | number | boolean
}

// TODO: Add proper return type annotation
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function VirtualList<T>({
id,
items,
loading,
getItemView,
children,
estimateSize = () => 56,
}: VirtualListProps<T>) {
scrollResetKey,
}: VirtualListProps<T>): ReactNode {
const parentRef = useRef<HTMLDivElement>(null)
const wrapperRef = useRef<HTMLDivElement>(null)
const scrollTimeoutRef = useRef<NodeJS.Timeout>(undefined)
Expand All @@ -53,31 +110,48 @@ export function VirtualList<T>({
}, scrollDelay)
}, [])

// eslint-disable-next-line react-hooks/incompatible-library
const virtualizer = useVirtualizer({
getScrollElement: () => parentRef.current,
count: items.length,
estimateSize,
overscan: 5,
})

useLayoutEffect(() => {
if (scrollResetKey === undefined) {
return
}

const scrollContainer = parentRef.current

if (scrollContainer) {
scrollContainer.scrollTop = 0
scrollContainer.scrollLeft = 0

if (typeof scrollContainer.scrollTo === 'function') {
scrollContainer.scrollTo({ top: 0, left: 0, behavior: 'auto' })
}
}

virtualizer.scrollToOffset(0, { align: 'start' })
}, [scrollResetKey, virtualizer])

const virtualItems = virtualizer.getVirtualItems()
const virtualRows = renderVirtualListRows({
virtualItems,
loading,
items,
getItemView,
measureElement: virtualizer.measureElement,
})

return (
<ListWrapper id={id} ref={parentRef} onScroll={onScroll}>
<ListInner ref={wrapperRef} style={{ height: virtualizer.getTotalSize() }}>
<ListScroller style={{ transform: `translateY(${virtualItems[0]?.start ?? 0}px)` }}>
{children}
{virtualItems.map((item) => {
if (loading) {
return <LoadingRows key={item.key}>{threeDivs()}</LoadingRows>
}

return (
<div key={item.key} data-index={item.index} ref={virtualizer.measureElement}>
{getItemView(items, item)}
</div>
)
})}
{virtualRows}
</ListScroller>
</ListInner>
</ListWrapper>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { ReactNode, useMemo } from 'react'

import { useTokensBalances } from '@cowprotocol/balances-and-allowances'
import { TokenWithLogo } from '@cowprotocol/common-const'
import {
getTokenSearchFilter,
LP_TOKEN_LIST_CATEGORIES,
Expand All @@ -21,12 +20,14 @@ import { TabButton, TabsContainer } from './styled'
import { LpTokenLists } from '../../pure/LpTokenLists'
import { tokensListSorter } from '../../utils/tokensListSorter'

import type { TokenSelectionHandler } from '../../types'

interface LpTokenListsProps<T = TokenListCategory[] | null> {
account: string | undefined
children: ReactNode
search: string
disableErc20?: boolean
onSelectToken(token: TokenWithLogo): void
onSelectToken: TokenSelectionHandler
openPoolPage(poolAddress: string): void
tokenListCategoryState: [T, (category: T) => void]
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { ReactNode } from 'react'

import { TokenWithLogo } from '@cowprotocol/common-const'
import { ExplorerDataType, getExplorerLink, shortenAddress } from '@cowprotocol/common-utils'
import { TokenLogo, useTokensByAddressMap } from '@cowprotocol/tokens'
import { ExternalLink, ModalHeader, TokenSymbol } from '@cowprotocol/ui'
Expand All @@ -20,6 +19,8 @@ import {
Wrapper,
} from './styled'

import type { TokenSelectionHandler } from '../../types'

function renderValue<T>(value: T | undefined, template: (v: T) => string, defaultValue?: string): string | undefined {
return value ? template(value) : defaultValue
}
Expand All @@ -31,7 +32,7 @@ interface LpTokenPageProps {

onDismiss(): void

onSelectToken(token: TokenWithLogo): void
onSelectToken: TokenSelectionHandler
}

// eslint-disable-next-line max-lines-per-function
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { MouseEvent, ReactNode } from 'react'

import { createPortal } from 'react-dom'

import { MobileChainPanelCard, MobileChainPanelOverlay } from './styled'

import { ChainPanel } from '../../pure/ChainPanel'

import type { SelectTokenWidgetViewProps } from './controllerProps'

interface MobileChainPanelPortalProps {
chainsPanelTitle: string
chainsToSelect: SelectTokenWidgetViewProps['chainsToSelect']
onSelectChain: SelectTokenWidgetViewProps['onSelectChain']
onClose(): void
}

export function MobileChainPanelPortal({
chainsPanelTitle,
chainsToSelect,
onSelectChain,
onClose,
}: MobileChainPanelPortalProps): ReactNode {
if (typeof document === 'undefined') {
return null
}

return createPortal(
<MobileChainPanelOverlay onClick={onClose}>
<MobileChainPanelCard onClick={(event: MouseEvent<HTMLDivElement>) => event.stopPropagation()}>
<ChainPanel
title={chainsPanelTitle}
chainsState={chainsToSelect}
onSelectChain={(chain) => {
onSelectChain(chain)
onClose()
}}
variant="fullscreen"
onClose={onClose}
/>
</MobileChainPanelCard>
</MobileChainPanelOverlay>,
document.body,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { useIsBridgingEnabled } from '@cowprotocol/common-hooks'
import { useWalletInfo } from '@cowprotocol/wallet'

import { Field } from 'legacy/state/types'

import { useLpTokensWithBalances } from 'modules/yield/shared'

import { SelectTokenWidgetViewProps } from './controllerProps'
import {
useManageWidgetVisibility,
useTokenAdminActions,
useTokenDataSources,
useWidgetMetadata,
} from './controllerState'
import { useSelectTokenWidgetViewState } from './controllerViewState'

import { useChainsToSelect } from '../../hooks/useChainsToSelect'
import { useCloseTokenSelectWidget } from '../../hooks/useCloseTokenSelectWidget'
import { useOnSelectChain } from '../../hooks/useOnSelectChain'
import { useOnTokenListAddingError } from '../../hooks/useOnTokenListAddingError'
import { useSelectTokenWidgetState } from '../../hooks/useSelectTokenWidgetState'
import { useUpdateSelectTokenWidgetState } from '../../hooks/useUpdateSelectTokenWidgetState'

export interface SelectTokenWidgetProps {
displayLpTokenLists?: boolean
standalone?: boolean
}

export interface SelectTokenWidgetController {
shouldRender: boolean
hasChainPanel: boolean
viewProps: SelectTokenWidgetViewProps
}

export function useSelectTokenWidgetController({
displayLpTokenLists,
standalone,
}: SelectTokenWidgetProps): SelectTokenWidgetController {
const widgetState = useSelectTokenWidgetState()
const { count: lpTokensWithBalancesCount } = useLpTokensWithBalances()
const resolvedField = widgetState.field ?? Field.INPUT
const chainsToSelect = useChainsToSelect()
const onSelectChain = useOnSelectChain()
const isBridgeFeatureEnabled = useIsBridgingEnabled()
const manageWidget = useManageWidgetVisibility()
const updateSelectTokenWidget = useUpdateSelectTokenWidgetState()
const { account, chainId: walletChainId } = useWalletInfo()
const closeTokenSelectWidget = useCloseTokenSelectWidget()
const tokenData = useTokenDataSources()
const onTokenListAddingError = useOnTokenListAddingError()
const tokenAdminActions = useTokenAdminActions()
const widgetMetadata = useWidgetMetadata(
resolvedField,
widgetState.tradeType,
displayLpTokenLists,
widgetState.oppositeToken,
lpTokensWithBalancesCount,
)

const { isChainPanelEnabled, viewProps } = useSelectTokenWidgetViewState({
displayLpTokenLists,
standalone,
widgetState,
chainsToSelect,
onSelectChain,
manageWidget,
updateSelectTokenWidget,
account,
closeTokenSelectWidget,
tokenData,
onTokenListAddingError,
tokenAdminActions,
widgetMetadata,
walletChainId,
isBridgeFeatureEnabled,
})

return {
shouldRender: Boolean(widgetState.onSelectToken && (widgetState.open || widgetState.forceOpen)),
hasChainPanel: isChainPanelEnabled,
viewProps,
}
}

export type { SelectTokenWidgetViewProps } from './controllerProps'
Loading