diff --git a/.gitignore b/.gitignore
index 41b097e960..063d6eb870 100644
--- a/.gitignore
+++ b/.gitignore
@@ -46,6 +46,7 @@ notes.txt
package-lock.json
+cypress/downloads
cypress/videos
cypress/screenshots
diff --git a/cypress/e2e/universal-search.test.ts b/cypress/e2e/universal-search.test.ts
index be7b902e3e..c5517cefeb 100644
--- a/cypress/e2e/universal-search.test.ts
+++ b/cypress/e2e/universal-search.test.ts
@@ -1,12 +1,21 @@
describe('Universal search bar', () => {
+ function openSearch() {
+ // can't just type "/" because on mobile it doesn't respond to that
+ cy.get('[data-cy="magnifying-icon"]').parent().eq(1).click()
+ }
+
beforeEach(() => {
cy.visit('/')
- cy.get('[data-cy="magnifying-icon"]').parent().eq(1).click()
+ openSearch()
})
+ function getSearchBar() {
+ return cy.get('[data-cy="search-bar-input"]').last()
+ }
+
it('should yield clickable result for regular token or nft collection search term', () => {
// Search for uni token by name.
- cy.get('[data-cy="search-bar-input"]').last().clear().type('uni')
+ getSearchBar().clear().type('uni')
cy.get('[data-cy="searchbar-token-row-UNI"]')
.should('contain.text', 'Uniswap')
.and('contain.text', 'UNI')
@@ -16,6 +25,32 @@ describe('Universal search bar', () => {
cy.location('hash').should('equal', '#/tokens/ethereum/0x1f9840a85d5af5bf1d1762f925bdaddc4201f984')
})
+ it('should go to the selected result when recent results are shown', () => {
+ // Search for uni token by name.
+ getSearchBar().type('uni')
+ cy.get('[data-cy="searchbar-token-row-UNI"]')
+
+ // Clear search
+ getSearchBar().clear()
+
+ // Close search
+ getSearchBar().type('{esc}')
+
+ openSearch()
+
+ // Search a different token by name.
+ getSearchBar().type('eth')
+
+ // Validate ETH result now exists.
+ cy.get('[data-cy="searchbar-token-row-ETH"]')
+
+ // Hit enter
+ getSearchBar().type('{enter}')
+
+ // Validate we went to ethereum address
+ cy.url().should('contain', 'tokens/ethereum/NATIVE')
+ })
+
it.skip('should show recent tokens and popular tokens with empty search term', () => {
cy.get('[data-cy="magnifying-icon"]')
.parent()
@@ -23,7 +58,7 @@ describe('Universal search bar', () => {
$navIcon.click()
})
// Recently searched UNI token should exist.
- cy.get('[data-cy="search-bar-input"]').last().clear()
+ getSearchBar().clear()
cy.get('[data-cy="searchbar-dropdown"]')
.contains('[data-cy="searchbar-dropdown"]', 'Recent searches')
.find('[data-cy="searchbar-token-row-UNI"]')
@@ -39,7 +74,7 @@ describe('Universal search bar', () => {
it.skip('should show blocked badge when blocked token is searched for', () => {
// Search for mTSLA, which is a blocked token.
- cy.get('[data-cy="search-bar-input"]').last().clear().type('mtsla')
+ getSearchBar().clear().type('mtsla')
cy.get('[data-cy="searchbar-token-row-mTSLA"]').find('[data-cy="blocked-icon"]').should('exist')
})
})
diff --git a/src/components/NavBar/SearchBarDropdown.tsx b/src/components/NavBar/SearchBarDropdown.tsx
index bcf6de5d70..30c2f5c95f 100644
--- a/src/components/NavBar/SearchBarDropdown.tsx
+++ b/src/components/NavBar/SearchBarDropdown.tsx
@@ -17,12 +17,14 @@ import { Box } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex'
import { subheadSmall } from 'nft/css/common.css'
import { GenieCollection, TrendingCollection } from 'nft/types'
-import { ReactNode, useEffect, useMemo, useState } from 'react'
+import { useEffect, useMemo, useState } from 'react'
import { useLocation } from 'react-router-dom'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
import { ClockIcon, TrendingArrow } from '../../nft/components/icons'
+import { SuspendConditionally } from '../Suspense/SuspendConditionally'
+import { SuspenseWithPreviousRenderAsFallback } from '../Suspense/SuspenseWithPreviousRenderAsFallback'
import { useRecentlySearchedAssets } from './RecentlySearchedAssets'
import * as styles from './SearchBar.css'
import { CollectionRow, SkeletonRow, TokenRow } from './SuggestionRow'
@@ -132,24 +134,46 @@ interface SearchBarDropdownProps {
isLoading: boolean
}
-export const SearchBarDropdown = ({
+export const SearchBarDropdown = (props: SearchBarDropdownProps) => {
+ const { isLoading } = props
+ const { chainId } = useWeb3React()
+ const showChainComingSoonBadge = chainId && BACKEND_NOT_YET_SUPPORTED_CHAIN_IDS.includes(chainId) && !isLoading
+ const logoUri = getChainInfo(chainId)?.logoUrl
+
+ return (
+
+
+
+
+
+
+
+ {showChainComingSoonBadge && (
+
+
+
+
+
+
+ )}
+
+
+ )
+}
+
+function SearchBarDropdownContents({
toggleOpen,
tokens,
collections,
queryText,
hasInput,
- isLoading,
-}: SearchBarDropdownProps) => {
+}: SearchBarDropdownProps): JSX.Element {
const [hoveredIndex, setHoveredIndex] = useState(0)
-
const { data: searchHistory } = useRecentlySearchedAssets()
const shortenedHistory = useMemo(() => searchHistory?.slice(0, 2) ?? [...Array(2)], [searchHistory])
-
const { pathname } = useLocation()
- const { chainId } = useWeb3React()
const isNFTPage = useIsNftPage()
const isTokenPage = pathname.includes('/tokens')
- const [resultsState, setResultsState] = useState()
const shouldDisableNFTRoutes = useDisableNFTRoutes()
const { data: trendingCollections, loading: trendingCollectionsAreLoading } = useTrendingCollections(
@@ -222,157 +246,112 @@ export const SearchBarDropdown = ({
const trace = JSON.stringify(useTrace({ section: InterfaceSectionName.NAVBAR_SEARCH }))
- useEffect(() => {
- const eventProperties = { total_suggestions: totalSuggestions, query_text: queryText, ...JSON.parse(trace) }
- if (!isLoading) {
- const tokenSearchResults =
- tokens.length > 0 ? (
- Tokens}
- />
- ) : (
-
- No tokens found.
-
- )
+ const eventProperties = { total_suggestions: totalSuggestions, query_text: queryText, ...JSON.parse(trace) }
- const collectionSearchResults =
- collections.length > 0 ? (
- NFT Collections}
- />
- ) : (
- No NFT collections found.
- )
-
- const currentState = () =>
- hasInput ? (
- // Empty or Up to 8 combined tokens and nfts
-
- {showCollectionsFirst ? (
- <>
- {collectionSearchResults}
- {tokenSearchResults}
- >
- ) : (
- <>
- {tokenSearchResults}
- {collectionSearchResults}
- >
- )}
-
- ) : (
- // Recent Searches, Trending Tokens, Trending Collections
-
- {shortenedHistory.length > 0 && (
- Recent searches}
- headerIcon={}
- isLoading={!searchHistory}
- />
- )}
- {!isNFTPage && (
- Popular tokens}
- headerIcon={}
- isLoading={!trendingTokenData}
- />
- )}
- {Boolean(!isTokenPage && !shouldDisableNFTRoutes) && (
- Popular NFT collections}
- headerIcon={}
- isLoading={trendingCollectionsAreLoading}
- />
- )}
-
- )
-
- setResultsState(currentState)
- }
- }, [
- isLoading,
- tokens,
- collections,
- formattedTrendingCollections,
- trendingTokens,
- trendingTokenData,
- hoveredIndex,
- toggleOpen,
- shortenedHistory,
- hasInput,
- isNFTPage,
- isTokenPage,
- showCollectionsFirst,
- queryText,
- totalSuggestions,
- trace,
- searchHistory,
- trendingCollectionsAreLoading,
- shouldDisableNFTRoutes,
- ])
-
- const showChainComingSoonBadge = chainId && BACKEND_NOT_YET_SUPPORTED_CHAIN_IDS.includes(chainId) && !isLoading
- const logoUri = getChainInfo(chainId)?.logoUrl
-
- return (
-
-
- {resultsState}
- {showChainComingSoonBadge && (
-
-
-
-
-
-
- )}
+ const tokenSearchResults =
+ tokens.length > 0 ? (
+ Tokens}
+ />
+ ) : (
+
+ No tokens found.
+ )
+
+ const collectionSearchResults =
+ collections.length > 0 ? (
+ NFT Collections}
+ />
+ ) : (
+ No NFT collections found.
+ )
+
+ return hasInput ? (
+ // Empty or Up to 8 combined tokens and nfts
+
+ {showCollectionsFirst ? (
+ <>
+ {collectionSearchResults}
+ {tokenSearchResults}
+ >
+ ) : (
+ <>
+ {tokenSearchResults}
+ {collectionSearchResults}
+ >
+ )}
+
+ ) : (
+ // Recent Searches, Trending Tokens, Trending Collections
+
+ {shortenedHistory.length > 0 && (
+ Recent searches}
+ headerIcon={}
+ isLoading={!searchHistory}
+ />
+ )}
+ {!isNFTPage && (
+ Popular tokens}
+ headerIcon={}
+ isLoading={!trendingTokenData}
+ />
+ )}
+ {Boolean(!isTokenPage && !shouldDisableNFTRoutes) && (
+ Popular NFT collections}
+ headerIcon={}
+ isLoading={trendingCollectionsAreLoading}
+ />
+ )}
)
}
diff --git a/src/components/Suspense/SuspendConditionally.tsx b/src/components/Suspense/SuspendConditionally.tsx
new file mode 100644
index 0000000000..297c512020
--- /dev/null
+++ b/src/components/Suspense/SuspendConditionally.tsx
@@ -0,0 +1,21 @@
+import React from 'react'
+import { useState } from 'react'
+
+export const SuspendConditionally = (props: { if: boolean; children: React.ReactNode }) => {
+ useSuspendIf(props.if)
+ return <>{props.children}>
+}
+
+function useSuspendIf(shouldSuspend = false) {
+ const [resolve, setResolve] = useState<((val?: unknown) => void) | undefined>()
+
+ if (!resolve && shouldSuspend) {
+ const promise = new Promise((res) => {
+ setResolve(res)
+ })
+ throw promise
+ } else if (resolve && !shouldSuspend) {
+ resolve()
+ setResolve(undefined)
+ }
+}
diff --git a/src/components/Suspense/SuspenseWithPreviousRenderAsFallback.tsx b/src/components/Suspense/SuspenseWithPreviousRenderAsFallback.tsx
new file mode 100644
index 0000000000..13743e1db8
--- /dev/null
+++ b/src/components/Suspense/SuspenseWithPreviousRenderAsFallback.tsx
@@ -0,0 +1,14 @@
+import usePrevious from 'hooks/usePrevious'
+import React, { Suspense } from 'react'
+
+/**
+ * This is useful for keeping the "last rendered" components on-screen while any suspense
+ * is triggered below this component.
+ *
+ * It stores a reference to the current children, and then returns them as the fallback.
+ */
+
+export const SuspenseWithPreviousRenderAsFallback = (props: { children: React.ReactNode }) => {
+ const previousContents = usePrevious(props.children)
+ return {props.children}
+}