Commit 1c504601 authored by Nate Wienert's avatar Nate Wienert Committed by GitHub

fix: using search hotkey enter navigates to the wrong result (#6735)

* fix: fix SearchBarDropdown selecting invalid result on enter after initial search when recent searches are filled

* add test

* Update cypress/e2e/universal-search.test.ts
Co-authored-by: default avatarZach Pomerantz <zzmp@uniswap.org>

* Update cypress/e2e/universal-search.test.ts
Co-authored-by: default avatarZach Pomerantz <zzmp@uniswap.org>

* Update cypress/e2e/universal-search.test.ts
Co-authored-by: default avatarZach Pomerantz <zzmp@uniswap.org>

* use searhc hotkey

* Revert ""

This reverts commit 7b04d5d575e8f776e8127e9b43c3fff6f74d8919.

* chore: gitignore cypress/downloads.html

---------
Co-authored-by: default avatarZach Pomerantz <zzmp@uniswap.org>
parent 2ef9e9b6
...@@ -46,6 +46,7 @@ notes.txt ...@@ -46,6 +46,7 @@ notes.txt
package-lock.json package-lock.json
cypress/downloads
cypress/videos cypress/videos
cypress/screenshots cypress/screenshots
......
describe('Universal search bar', () => { 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(() => { beforeEach(() => {
cy.visit('/') 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', () => { it('should yield clickable result for regular token or nft collection search term', () => {
// Search for uni token by name. // 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"]') cy.get('[data-cy="searchbar-token-row-UNI"]')
.should('contain.text', 'Uniswap') .should('contain.text', 'Uniswap')
.and('contain.text', 'UNI') .and('contain.text', 'UNI')
...@@ -16,6 +25,32 @@ describe('Universal search bar', () => { ...@@ -16,6 +25,32 @@ describe('Universal search bar', () => {
cy.location('hash').should('equal', '#/tokens/ethereum/0x1f9840a85d5af5bf1d1762f925bdaddc4201f984') 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', () => { it.skip('should show recent tokens and popular tokens with empty search term', () => {
cy.get('[data-cy="magnifying-icon"]') cy.get('[data-cy="magnifying-icon"]')
.parent() .parent()
...@@ -23,7 +58,7 @@ describe('Universal search bar', () => { ...@@ -23,7 +58,7 @@ describe('Universal search bar', () => {
$navIcon.click() $navIcon.click()
}) })
// Recently searched UNI token should exist. // Recently searched UNI token should exist.
cy.get('[data-cy="search-bar-input"]').last().clear() getSearchBar().clear()
cy.get('[data-cy="searchbar-dropdown"]') cy.get('[data-cy="searchbar-dropdown"]')
.contains('[data-cy="searchbar-dropdown"]', 'Recent searches') .contains('[data-cy="searchbar-dropdown"]', 'Recent searches')
.find('[data-cy="searchbar-token-row-UNI"]') .find('[data-cy="searchbar-token-row-UNI"]')
...@@ -39,7 +74,7 @@ describe('Universal search bar', () => { ...@@ -39,7 +74,7 @@ describe('Universal search bar', () => {
it.skip('should show blocked badge when blocked token is searched for', () => { it.skip('should show blocked badge when blocked token is searched for', () => {
// Search for mTSLA, which is a blocked token. // 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') cy.get('[data-cy="searchbar-token-row-mTSLA"]').find('[data-cy="blocked-icon"]').should('exist')
}) })
}) })
...@@ -17,12 +17,14 @@ import { Box } from 'nft/components/Box' ...@@ -17,12 +17,14 @@ import { Box } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex' import { Column, Row } from 'nft/components/Flex'
import { subheadSmall } from 'nft/css/common.css' import { subheadSmall } from 'nft/css/common.css'
import { GenieCollection, TrendingCollection } from 'nft/types' 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 { useLocation } from 'react-router-dom'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import { ThemedText } from 'theme' import { ThemedText } from 'theme'
import { ClockIcon, TrendingArrow } from '../../nft/components/icons' import { ClockIcon, TrendingArrow } from '../../nft/components/icons'
import { SuspendConditionally } from '../Suspense/SuspendConditionally'
import { SuspenseWithPreviousRenderAsFallback } from '../Suspense/SuspenseWithPreviousRenderAsFallback'
import { useRecentlySearchedAssets } from './RecentlySearchedAssets' import { useRecentlySearchedAssets } from './RecentlySearchedAssets'
import * as styles from './SearchBar.css' import * as styles from './SearchBar.css'
import { CollectionRow, SkeletonRow, TokenRow } from './SuggestionRow' import { CollectionRow, SkeletonRow, TokenRow } from './SuggestionRow'
...@@ -132,24 +134,46 @@ interface SearchBarDropdownProps { ...@@ -132,24 +134,46 @@ interface SearchBarDropdownProps {
isLoading: boolean 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 (
<Column overflow="hidden" className={clsx(styles.searchBarDropdownNft, styles.searchBarScrollable)}>
<Box opacity={isLoading ? '0.3' : '1'} transition="125">
<SuspenseWithPreviousRenderAsFallback>
<SuspendConditionally if={isLoading}>
<SearchBarDropdownContents {...props} />
</SuspendConditionally>
</SuspenseWithPreviousRenderAsFallback>
{showChainComingSoonBadge && (
<ChainComingSoonBadge>
<ChainLogo src={logoUri} />
<ThemedText.BodySmall color="textSecondary" fontSize="14px" fontWeight="400" lineHeight="20px">
<ComingSoonText chainId={chainId} />
</ThemedText.BodySmall>
</ChainComingSoonBadge>
)}
</Box>
</Column>
)
}
function SearchBarDropdownContents({
toggleOpen, toggleOpen,
tokens, tokens,
collections, collections,
queryText, queryText,
hasInput, hasInput,
isLoading, }: SearchBarDropdownProps): JSX.Element {
}: SearchBarDropdownProps) => {
const [hoveredIndex, setHoveredIndex] = useState<number | undefined>(0) const [hoveredIndex, setHoveredIndex] = useState<number | undefined>(0)
const { data: searchHistory } = useRecentlySearchedAssets() const { data: searchHistory } = useRecentlySearchedAssets()
const shortenedHistory = useMemo(() => searchHistory?.slice(0, 2) ?? [...Array<SearchToken>(2)], [searchHistory]) const shortenedHistory = useMemo(() => searchHistory?.slice(0, 2) ?? [...Array<SearchToken>(2)], [searchHistory])
const { pathname } = useLocation() const { pathname } = useLocation()
const { chainId } = useWeb3React()
const isNFTPage = useIsNftPage() const isNFTPage = useIsNftPage()
const isTokenPage = pathname.includes('/tokens') const isTokenPage = pathname.includes('/tokens')
const [resultsState, setResultsState] = useState<ReactNode>()
const shouldDisableNFTRoutes = useDisableNFTRoutes() const shouldDisableNFTRoutes = useDisableNFTRoutes()
const { data: trendingCollections, loading: trendingCollectionsAreLoading } = useTrendingCollections( const { data: trendingCollections, loading: trendingCollectionsAreLoading } = useTrendingCollections(
...@@ -222,157 +246,112 @@ export const SearchBarDropdown = ({ ...@@ -222,157 +246,112 @@ export const SearchBarDropdown = ({
const trace = JSON.stringify(useTrace({ section: InterfaceSectionName.NAVBAR_SEARCH })) const trace = JSON.stringify(useTrace({ section: InterfaceSectionName.NAVBAR_SEARCH }))
useEffect(() => { const eventProperties = { total_suggestions: totalSuggestions, query_text: queryText, ...JSON.parse(trace) }
const eventProperties = { total_suggestions: totalSuggestions, query_text: queryText, ...JSON.parse(trace) }
if (!isLoading) {
const tokenSearchResults =
tokens.length > 0 ? (
<SearchBarDropdownSection
hoveredIndex={hoveredIndex}
startingIndex={showCollectionsFirst ? collections.length : 0}
setHoveredIndex={setHoveredIndex}
toggleOpen={toggleOpen}
suggestions={tokens}
eventProperties={{
suggestion_type: NavBarSearchTypes.TOKEN_SUGGESTION,
...eventProperties,
}}
header={<Trans>Tokens</Trans>}
/>
) : (
<Box className={styles.notFoundContainer}>
<Trans>No tokens found.</Trans>
</Box>
)
const collectionSearchResults =
collections.length > 0 ? (
<SearchBarDropdownSection
hoveredIndex={hoveredIndex}
startingIndex={showCollectionsFirst ? 0 : tokens.length}
setHoveredIndex={setHoveredIndex}
toggleOpen={toggleOpen}
suggestions={collections}
eventProperties={{
suggestion_type: NavBarSearchTypes.COLLECTION_SUGGESTION,
...eventProperties,
}}
header={<Trans>NFT Collections</Trans>}
/>
) : (
<Box className={styles.notFoundContainer}>No NFT collections found.</Box>
)
const currentState = () =>
hasInput ? (
// Empty or Up to 8 combined tokens and nfts
<Column gap="20">
{showCollectionsFirst ? (
<>
{collectionSearchResults}
{tokenSearchResults}
</>
) : (
<>
{tokenSearchResults}
{collectionSearchResults}
</>
)}
</Column>
) : (
// Recent Searches, Trending Tokens, Trending Collections
<Column gap="20">
{shortenedHistory.length > 0 && (
<SearchBarDropdownSection
hoveredIndex={hoveredIndex}
startingIndex={0}
setHoveredIndex={setHoveredIndex}
toggleOpen={toggleOpen}
suggestions={shortenedHistory}
eventProperties={{
suggestion_type: NavBarSearchTypes.RECENT_SEARCH,
...eventProperties,
}}
header={<Trans>Recent searches</Trans>}
headerIcon={<ClockIcon />}
isLoading={!searchHistory}
/>
)}
{!isNFTPage && (
<SearchBarDropdownSection
hoveredIndex={hoveredIndex}
startingIndex={shortenedHistory.length}
setHoveredIndex={setHoveredIndex}
toggleOpen={toggleOpen}
suggestions={trendingTokens}
eventProperties={{
suggestion_type: NavBarSearchTypes.TOKEN_TRENDING,
...eventProperties,
}}
header={<Trans>Popular tokens</Trans>}
headerIcon={<TrendingArrow />}
isLoading={!trendingTokenData}
/>
)}
{Boolean(!isTokenPage && !shouldDisableNFTRoutes) && (
<SearchBarDropdownSection
hoveredIndex={hoveredIndex}
startingIndex={shortenedHistory.length + (isNFTPage ? 0 : trendingTokens?.length ?? 0)}
setHoveredIndex={setHoveredIndex}
toggleOpen={toggleOpen}
suggestions={formattedTrendingCollections as unknown as GenieCollection[]}
eventProperties={{
suggestion_type: NavBarSearchTypes.COLLECTION_TRENDING,
...eventProperties,
}}
header={<Trans>Popular NFT collections</Trans>}
headerIcon={<TrendingArrow />}
isLoading={trendingCollectionsAreLoading}
/>
)}
</Column>
)
setResultsState(currentState) const tokenSearchResults =
} tokens.length > 0 ? (
}, [ <SearchBarDropdownSection
isLoading, hoveredIndex={hoveredIndex}
tokens, startingIndex={showCollectionsFirst ? collections.length : 0}
collections, setHoveredIndex={setHoveredIndex}
formattedTrendingCollections, toggleOpen={toggleOpen}
trendingTokens, suggestions={tokens}
trendingTokenData, eventProperties={{
hoveredIndex, suggestion_type: NavBarSearchTypes.TOKEN_SUGGESTION,
toggleOpen, ...eventProperties,
shortenedHistory, }}
hasInput, header={<Trans>Tokens</Trans>}
isNFTPage, />
isTokenPage, ) : (
showCollectionsFirst, <Box className={styles.notFoundContainer}>
queryText, <Trans>No tokens found.</Trans>
totalSuggestions, </Box>
trace, )
searchHistory,
trendingCollectionsAreLoading,
shouldDisableNFTRoutes,
])
const showChainComingSoonBadge = chainId && BACKEND_NOT_YET_SUPPORTED_CHAIN_IDS.includes(chainId) && !isLoading const collectionSearchResults =
const logoUri = getChainInfo(chainId)?.logoUrl collections.length > 0 ? (
<SearchBarDropdownSection
hoveredIndex={hoveredIndex}
startingIndex={showCollectionsFirst ? 0 : tokens.length}
setHoveredIndex={setHoveredIndex}
toggleOpen={toggleOpen}
suggestions={collections}
eventProperties={{
suggestion_type: NavBarSearchTypes.COLLECTION_SUGGESTION,
...eventProperties,
}}
header={<Trans>NFT Collections</Trans>}
/>
) : (
<Box className={styles.notFoundContainer}>No NFT collections found.</Box>
)
return ( return hasInput ? (
<Column overflow="hidden" className={clsx(styles.searchBarDropdownNft, styles.searchBarScrollable)}> // Empty or Up to 8 combined tokens and nfts
<Box opacity={isLoading ? '0.3' : '1'} transition="125"> <Column gap="20">
{resultsState} {showCollectionsFirst ? (
{showChainComingSoonBadge && ( <>
<ChainComingSoonBadge> {collectionSearchResults}
<ChainLogo src={logoUri} /> {tokenSearchResults}
<ThemedText.BodySmall color="textSecondary" fontSize="14px" fontWeight="400" lineHeight="20px"> </>
<ComingSoonText chainId={chainId} /> ) : (
</ThemedText.BodySmall> <>
</ChainComingSoonBadge> {tokenSearchResults}
)} {collectionSearchResults}
</Box> </>
)}
</Column>
) : (
// Recent Searches, Trending Tokens, Trending Collections
<Column gap="20">
{shortenedHistory.length > 0 && (
<SearchBarDropdownSection
hoveredIndex={hoveredIndex}
startingIndex={0}
setHoveredIndex={setHoveredIndex}
toggleOpen={toggleOpen}
suggestions={shortenedHistory}
eventProperties={{
suggestion_type: NavBarSearchTypes.RECENT_SEARCH,
...eventProperties,
}}
header={<Trans>Recent searches</Trans>}
headerIcon={<ClockIcon />}
isLoading={!searchHistory}
/>
)}
{!isNFTPage && (
<SearchBarDropdownSection
hoveredIndex={hoveredIndex}
startingIndex={shortenedHistory.length}
setHoveredIndex={setHoveredIndex}
toggleOpen={toggleOpen}
suggestions={trendingTokens}
eventProperties={{
suggestion_type: NavBarSearchTypes.TOKEN_TRENDING,
...eventProperties,
}}
header={<Trans>Popular tokens</Trans>}
headerIcon={<TrendingArrow />}
isLoading={!trendingTokenData}
/>
)}
{Boolean(!isTokenPage && !shouldDisableNFTRoutes) && (
<SearchBarDropdownSection
hoveredIndex={hoveredIndex}
startingIndex={shortenedHistory.length + (isNFTPage ? 0 : trendingTokens?.length ?? 0)}
setHoveredIndex={setHoveredIndex}
toggleOpen={toggleOpen}
suggestions={formattedTrendingCollections as unknown as GenieCollection[]}
eventProperties={{
suggestion_type: NavBarSearchTypes.COLLECTION_TRENDING,
...eventProperties,
}}
header={<Trans>Popular NFT collections</Trans>}
headerIcon={<TrendingArrow />}
isLoading={trendingCollectionsAreLoading}
/>
)}
</Column> </Column>
) )
} }
......
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)
}
}
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 <Suspense fallback={previousContents ?? null}>{props.children}</Suspense>
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment