Skip to content

Commit

Permalink
fix: search input
Browse files Browse the repository at this point in the history
  • Loading branch information
TateB committed Apr 24, 2024
1 parent 18aeb07 commit abb80b1
Show file tree
Hide file tree
Showing 10 changed files with 378 additions and 259 deletions.
117 changes: 82 additions & 35 deletions src/components/@molecules/SearchInput/SearchInput.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import debounce from 'lodash/debounce'
import {
Dispatch,
RefObject,
Expand All @@ -8,7 +9,7 @@ import {
useRef,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import { TFunction, useTranslation } from 'react-i18next'
import useTransition, { TransitionState } from 'react-transition-state'
import styled, { css } from 'styled-components'
import { isAddress } from 'viem'
Expand All @@ -24,7 +25,7 @@ import { thread } from '@app/utils/utils'

import { FakeSearchInputBox, SearchInputBox } from './SearchInputBox'
import { SearchResult } from './SearchResult'
import { HistoryItem, SearchHandler, SearchItem } from './types'
import { AnyItem, HistoryItem, SearchHandler, SearchItem } from './types'

const Container = styled.div<{ $size: 'medium' | 'extraLarge' }>(
({ $size }) => css`
Expand Down Expand Up @@ -179,14 +180,18 @@ const createSearchHandler =
({
router,
setHistory,
dropdownItems,
}: {
router: ReturnType<typeof useRouterWithHistory>
setHistory: Dispatch<SetStateAction<HistoryItem[]>>
dropdownItems: SearchItem[]
}): SearchHandler =>
(searchItem: SearchItem) => {
(index: number) => {
if (index === -1) return
const searchItem = dropdownItems[index]
if (!searchItem?.text) return
const { text, nameType } = searchItem
if (nameType === 'error') return
if (nameType === 'error' || nameType === 'text') return
setHistory((prev: HistoryItem[]) => [
...prev.filter((item) => !(item.text === text && item.nameType === nameType)),
{ lastAccessed: Date.now(), nameType, text, isValid: searchItem.isValid },
Expand Down Expand Up @@ -235,7 +240,7 @@ const handleKeyDown =
}) =>
(e: KeyboardEvent) => {
if (e.key === 'Enter') {
handleSearch(dropdownItems[selected])
handleSearch(selected)
return
}
if (e.key === 'ArrowUp') {
Expand Down Expand Up @@ -268,7 +273,7 @@ const useSelectionManager = ({

useEffect(() => {
if (state === 'unmounted') {
setSelected(0)
setSelected(-1)
}
}, [state, setSelected])
}
Expand All @@ -281,7 +286,7 @@ const formatEthText = ({ name, isETH }: { name: string; isETH: boolean | undefin
}
const addEthDropdownItem =
({ name, isETH }: { name: string; isETH: boolean | undefined }) =>
(dropdownItems: SearchItem[]) => {
(dropdownItems: AnyItem[]): AnyItem[] => {
const formattedEthName = formatEthText({ name, isETH })
if (formattedEthName === '') return dropdownItems
return [
Expand Down Expand Up @@ -317,7 +322,7 @@ const formatBoxText = (name: string) => {
}
const addBoxDropdownItem =
({ name, isValid }: { name: string; isValid: boolean | undefined }) =>
(dropdownItems: SearchItem[]) => {
(dropdownItems: AnyItem[]): AnyItem[] => {
const formattedBoxName = formatBoxText(name)
if (!formattedBoxName) return dropdownItems
return [
Expand All @@ -337,7 +342,7 @@ const formatTldText = (name: string) => {
}
const addTldDropdownItem =
({ name }: { name: string }) =>
(dropdownItems: SearchItem[]) => {
(dropdownItems: AnyItem[]): AnyItem[] => {
const formattedTld = formatTldText(name)
if (!formattedTld) return dropdownItems
return [
Expand All @@ -351,7 +356,7 @@ const addTldDropdownItem =

const addAddressItem =
({ name, inputIsAddress }: { name: string; inputIsAddress: boolean }) =>
(dropdownItems: SearchItem[]) => {
(dropdownItems: AnyItem[]): AnyItem[] => {
if (!inputIsAddress) return dropdownItems
return [
{
Expand All @@ -364,20 +369,27 @@ const addAddressItem =

const MAX_DROPDOWN_ITEMS = 6
const addHistoryDropdownItems =
({ history }: { history: HistoryItem[] }) =>
(dropdownItems: SearchItem[]) => {
({ name, history }: { name: string; history: HistoryItem[] }) =>
(dropdownItems: AnyItem[]): AnyItem[] => {
const historyItemDrawCount = MAX_DROPDOWN_ITEMS - dropdownItems.length

if (historyItemDrawCount > 0) {
const filteredHistoryItems = history.filter(
(historyItem: HistoryItem) =>
dropdownItems.findIndex(
(dropdownItem) =>
dropdownItem.nameType === historyItem.nameType &&
dropdownItem.text === historyItem.text,
) === -1,
)
const historyItems = filteredHistoryItems?.slice(0, historyItemDrawCount)
const filteredHistoryItems = history
.filter(
(historyItem: HistoryItem) =>
historyItem.text.includes(name) &&
dropdownItems.findIndex(
(dropdownItem) =>
dropdownItem.nameType === historyItem.nameType &&
dropdownItem.text === historyItem.text,
) === -1,
)
.sort((a, b) => b.lastAccessed - a.lastAccessed)
const historyItems = filteredHistoryItems?.slice(0, historyItemDrawCount).map((item) => ({
nameType: item.nameType,
text: item.text,
isHistory: true,
}))
return [...dropdownItems, ...historyItems]
}

Expand All @@ -393,7 +405,7 @@ const formatDnsText = ({ name, isETH }: { name: string; isETH: boolean | undefin
}
const addDnsDropdownItem =
({ name, isETH }: { name: string; isETH: boolean | undefined }) =>
(dropdownItems: SearchItem[]) => {
(dropdownItems: AnyItem[]): AnyItem[] => {
const formattedDnsName = formatDnsText({ name, isETH })
if (!formattedDnsName) return dropdownItems
return [
Expand All @@ -407,7 +419,7 @@ const addDnsDropdownItem =

const addErrorDropdownItem =
({ name, isValid }: { name: string; isValid: boolean | undefined }) =>
(dropdownItems: SearchItem[]) => {
(dropdownItems: AnyItem[]): AnyItem[] => {
if (isValid || name === '') return dropdownItems
return [
{
Expand All @@ -417,26 +429,47 @@ const addErrorDropdownItem =
]
}

const addInfoDropdownItem =
({ t }: { t: TFunction }) =>
(dropdownItems: AnyItem[]): AnyItem[] => {
if (dropdownItems.length) return dropdownItems
return [
{
text: t('search.emptyText'),
nameType: 'text',
} as const,
]
}

const useBuildDropdownItems = (inputVal: string, history: HistoryItem[]) => {
const { t } = useTranslation('common')

const inputIsAddress = useMemo(() => isAddress(inputVal), [inputVal])

const { isValid, isETH, name } = useValidate({
input: inputVal,
enabled: !inputIsAddress && !inputVal,
})

return thread(
[],
addEthDropdownItem({ name, isETH }),
addBoxDropdownItem({ name, isValid }),
addDnsDropdownItem({ name, isETH }),
addAddressItem({ name, inputIsAddress }),
addTldDropdownItem({ name }),
addHistoryDropdownItems({ history }),
addErrorDropdownItem({ name, isValid }),
return useMemo(
() =>
thread(
[],
addEthDropdownItem({ name, isETH }),
addBoxDropdownItem({ name, isValid }),
addDnsDropdownItem({ name, isETH }),
addAddressItem({ name, inputIsAddress }),
addTldDropdownItem({ name }),
addHistoryDropdownItems({ name, history }),
addErrorDropdownItem({ name, isValid }),
addInfoDropdownItem({ t }),
),
[inputIsAddress, name, isETH, isValid, history, t],
)
}

const debouncer = debounce((setFunc: () => void) => setFunc(), 250)

export const SearchInput = ({ size = 'extraLarge' }: { size?: 'medium' | 'extraLarge' }) => {
const router = useRouterWithHistory()
const breakpoints = useBreakpoint()
Expand All @@ -461,6 +494,7 @@ export const SearchInput = ({ size = 'extraLarge' }: { size?: 'medium' | 'extraL
const { width } = useElementSize(searchInputContainerRef.current)

const [selected, setSelected] = useState(0)
const [usingPlaceholder, setUsingPlaceholder] = useState(false)

const [history, setHistory] = useLocalStorage<HistoryItem[]>('search-history-v2', [])

Expand All @@ -470,9 +504,10 @@ export const SearchInput = ({ size = 'extraLarge' }: { size?: 'medium' | 'extraL
const dropdownItems = useBuildDropdownItems(inputVal, history)

// eslint-disable-next-line react-hooks/exhaustive-deps
const handleSearch = useCallback(createSearchHandler({ router, setHistory }), [
const handleSearch = useCallback(createSearchHandler({ router, setHistory, dropdownItems }), [
router,
setHistory,
dropdownItems,
])

// eslint-disable-next-line react-hooks/exhaustive-deps
Expand All @@ -490,12 +525,18 @@ export const SearchInput = ({ size = 'extraLarge' }: { size?: 'medium' | 'extraL

useSelectionManager({ inputVal, setSelected, state })

const setInput = (val: string) => {
setInputVal(val)
setUsingPlaceholder(true)
debouncer(() => setUsingPlaceholder(false))
}

const SearchInputElement = (
<SearchInputBox
containerRef={searchInputContainerRef}
ref={searchInputRef}
input={inputVal}
setInput={setInputVal}
setInput={setInput}
size={size}
/>
)
Expand All @@ -505,7 +546,7 @@ export const SearchInput = ({ size = 'extraLarge' }: { size?: 'medium' | 'extraL
style={{
width: width === Infinity ? undefined : width,
}}
onMouseLeave={() => inputVal === '' && setSelected(0)}
onMouseLeave={() => inputVal === '' && setSelected(-1)}
$state={state}
data-testid="search-input-results"
// data-error={!isValid && !inputIsAddress && inputVal !== ''}
Expand All @@ -517,6 +558,12 @@ export const SearchInput = ({ size = 'extraLarge' }: { size?: 'medium' | 'extraL
index={index}
selected={index === selected}
searchItem={searchItem}
key={
searchItem.isHistory
? `${searchItem.nameType}-${searchItem.text}`
: `${searchItem.nameType}`
}
usingPlaceholder={searchItem.isHistory ? false : usingPlaceholder}
/>
))}
</SearchResultsContainer>
Expand Down
30 changes: 4 additions & 26 deletions src/components/@molecules/SearchInput/SearchInputBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */

/* eslint-disable jsx-a11y/interactive-supports-focus */
import { Dispatch, ForwardedRef, forwardRef, MouseEvent, SetStateAction } from 'react'
import { ForwardedRef, forwardRef, MouseEvent } from 'react'
import { useTranslation } from 'react-i18next'
import styled, { css } from 'styled-components'

Expand Down Expand Up @@ -83,25 +83,12 @@ const MagnifyingGlassIcon = styled.svg(
type SearchInputBoxProps = {
size?: 'medium' | 'extraLarge'
input: string
setInput: Dispatch<SetStateAction<string>>
setInput: (value: string) => void
containerRef: ForwardedRef<HTMLDivElement>
}

export const SearchInputBox = forwardRef<HTMLInputElement, SearchInputBoxProps>(
(
{
size = 'extraLarge',
input,
setInput,
containerRef,
}: {
size?: 'medium' | 'extraLarge'
input: string
setInput: Dispatch<SetStateAction<string>>
containerRef: ForwardedRef<HTMLDivElement>
},
ref,
) => {
({ size = 'extraLarge', input, setInput, containerRef }, ref) => {
const { t } = useTranslation('common')
return (
<SearchInputWrapper ref={containerRef} $size={size}>
Expand Down Expand Up @@ -132,16 +119,7 @@ type FakeSearchInputBoxProps = {
}

export const FakeSearchInputBox = forwardRef<HTMLInputElement, FakeSearchInputBoxProps>(
(
{
size = 'extraLarge',
onClick,
}: {
size?: 'medium' | 'extraLarge'
onClick: (e: MouseEvent<HTMLInputElement>) => void
},
ref,
) => {
({ size = 'extraLarge', onClick }, ref) => {
const { t } = useTranslation('common')
return (
<SearchInputWrapper $size={size}>
Expand Down
Loading

0 comments on commit abb80b1

Please sign in to comment.