Skip to content

Commit f4dd3f0

Browse files
committed
add basic keyboard navigation
1 parent 9142be6 commit f4dd3f0

File tree

2 files changed

+126
-66
lines changed

2 files changed

+126
-66
lines changed

src/components/Search/index.js

Lines changed: 101 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import { useMedia } from 'react-use'
1313
import { useAllPairsInUniswap, useAllTokensInUniswap } from '../../contexts/GlobalData'
1414
import { OVERVIEW_TOKEN_BLACKLIST, OVERVIEW_PAIR_BLACKLIST } from '../../constants'
1515

16+
import { useKeyPress } from '../../hooks'
17+
1618
const Wrapper = styled.div`
1719
display: flex;
1820
position: relative;
@@ -68,7 +70,7 @@ const Menu = styled.div`
6870
width: 100%;
6971
top: 50px;
7072
max-height: 540px;
71-
overflow: scroll;
73+
overflow-y: scroll;
7274
left: 0;
7375
padding-bottom: 20px;
7476
background: white;
@@ -89,6 +91,10 @@ const MenuItem = styled(Row)`
8991
cursor: pointer;
9092
background-color: #f7f8fa;
9193
}
94+
:focus {
95+
background-color: #f7f8fa;
96+
outline: black auto 1px;
97+
}
9298
`
9399

94100
const Heading = styled(Row)`
@@ -119,7 +125,13 @@ export const Search = ({ small = false }) => {
119125
const allPairs = useAllPairsInUniswap()
120126
const allPairData = useAllPairData()
121127

122-
const [showMenu, toggleMenu] = useState(false)
128+
const [cursor, setCursor] = useState(null)
129+
const downKeyPressed = useKeyPress('ArrowDown')
130+
const escapeKeyPressed = useKeyPress('Escape')
131+
const tabKeyPressed = useKeyPress('Tab')
132+
const upKeyPressed = useKeyPress('ArrowUp')
133+
134+
const [showMenu, setShowMenu] = useState(false)
123135
const [value, setValue] = useState('')
124136
const [, toggleShadow] = useState(false)
125137
const [, toggleBottomShadow] = useState(false)
@@ -134,9 +146,9 @@ export const Search = ({ small = false }) => {
134146

135147
useEffect(() => {
136148
if (value !== '') {
137-
toggleMenu(true)
149+
setShowMenu(true)
138150
} else {
139-
toggleMenu(false)
151+
setShowMenu(false)
140152
}
141153
}, [value])
142154

@@ -278,10 +290,50 @@ export const Search = ({ small = false }) => {
278290
setPairsShown(Math.min(Object.keys(filteredPairList).length, 3))
279291
}, [filteredPairList])
280292

293+
useEffect(() => {
294+
if (pairsShown + tokensShown === 0) {
295+
setCursor(undefined)
296+
} else if (showMenu && downKeyPressed && cursor === undefined) {
297+
setCursor(0)
298+
} else if (showMenu && downKeyPressed && cursor < pairsShown + tokensShown) {
299+
setCursor(cursor + 1)
300+
}
301+
}, [showMenu, downKeyPressed, pairsShown, tokensShown, cursor])
302+
303+
useEffect(() => {
304+
if (pairsShown + tokensShown === 0) {
305+
setCursor(undefined)
306+
} else if (showMenu && upKeyPressed && cursor === undefined) {
307+
setCursor(pairsShown + tokensShown - 1)
308+
} else if (showMenu && upKeyPressed && cursor === 0) {
309+
setCursor(pairsShown + tokensShown - 1)
310+
} else if (showMenu && upKeyPressed && cursor > 0) {
311+
setCursor(cursor - 1)
312+
}
313+
}, [cursor, pairsShown, showMenu, tokensShown, upKeyPressed])
314+
315+
useEffect(() => {
316+
setCursor(undefined)
317+
}, [tabKeyPressed])
318+
319+
useEffect(() => {
320+
setCursor(undefined)
321+
setShowMenu(false)
322+
}, [escapeKeyPressed])
323+
324+
useEffect(() => {
325+
const canUseCursor = Boolean(
326+
Number.isInteger(cursor) && menuRef.current && menuRef.current.querySelectorAll('a')[cursor]
327+
)
328+
if (canUseCursor) {
329+
menuRef.current.querySelectorAll('a')[cursor].focus()
330+
}
331+
}, [cursor])
332+
281333
function onDismiss() {
282334
setPairsShown(3)
283335
setTokensShown(3)
284-
toggleMenu(false)
336+
setShowMenu(false)
285337
setValue('')
286338
}
287339

@@ -296,7 +348,7 @@ export const Search = ({ small = false }) => {
296348
) {
297349
setPairsShown(3)
298350
setTokensShown(3)
299-
toggleMenu(false)
351+
setShowMenu(false)
300352
}
301353
}
302354

@@ -338,7 +390,7 @@ export const Search = ({ small = false }) => {
338390
setValue(e.target.value)
339391
}}
340392
onFocus={() => {
341-
toggleMenu(true)
393+
setShowMenu(true)
342394
}}
343395
/>
344396
</Wrapper>
@@ -350,68 +402,51 @@ export const Search = ({ small = false }) => {
350402
<Heading>
351403
<Gray>Pairs</Gray>
352404
</Heading>
353-
<div>
354-
{filteredPairList && Object.keys(filteredPairList).length === 0 && <MenuItem>No results</MenuItem>}
355-
{filteredPairList &&
356-
filteredPairList.slice(0, pairsShown).map(pair => {
357-
if (pair?.token0?.id === '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2') {
358-
pair.token0.name = 'ETH (Wrapped)'
359-
pair.token0.symbol = 'ETH'
360-
}
361-
if (pair?.token1.id === '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2') {
362-
pair.token1.name = 'ETH (Wrapped)'
363-
pair.token1.symbol = 'ETH'
364-
}
365-
return (
366-
<BasicLink to={'/pair/' + pair.id} key={pair.id} onClick={onDismiss}>
367-
<MenuItem>
368-
<DoubleTokenLogo a0={pair?.token0?.id} a1={pair?.token1?.id} margin={true} />
369-
<span style={{ marginLeft: '10px' }}>{pair.token0.symbol + '-' + pair.token1.symbol} Pair</span>
370-
</MenuItem>
371-
</BasicLink>
372-
)
373-
})}
374-
<Heading
375-
hide={!(Object.keys(filteredPairList).length > 3 && Object.keys(filteredPairList).length >= pairsShown)}
376-
>
377-
<Blue
378-
onClick={() => {
379-
setPairsShown(pairsShown + 5)
380-
}}
381-
>
382-
See more...
383-
</Blue>
384-
</Heading>
385-
</div>
405+
{Object.keys(filteredPairList).length === 0 && <MenuItem>No results</MenuItem>}
406+
{filteredPairList.slice(0, pairsShown).map(pair => {
407+
if (pair?.token0?.id === '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2') {
408+
pair.token0.name = 'ETH (Wrapped)'
409+
pair.token0.symbol = 'ETH'
410+
}
411+
if (pair?.token1.id === '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2') {
412+
pair.token1.name = 'ETH (Wrapped)'
413+
pair.token1.symbol = 'ETH'
414+
}
415+
return (
416+
<BasicLink to={'/pair/' + pair.id} key={pair.id} onClick={onDismiss}>
417+
<MenuItem>
418+
<DoubleTokenLogo a0={pair?.token0?.id} a1={pair?.token1?.id} margin={true} />
419+
<span style={{ marginLeft: '10px' }}>{pair.token0.symbol + '-' + pair.token1.symbol} Pair</span>
420+
</MenuItem>
421+
</BasicLink>
422+
)
423+
})}
424+
<Heading
425+
hide={!(Object.keys(filteredPairList).length > 3 && Object.keys(filteredPairList).length >= pairsShown)}
426+
>
427+
<Blue onClick={() => setPairsShown(pairsShown + 5)}>See more...</Blue>
428+
</Heading>
386429
<Heading>
387430
<Gray>Tokens</Gray>
388431
</Heading>
389-
<div>
390-
{Object.keys(filteredTokenList).length === 0 && <MenuItem>No results</MenuItem>}
391-
{filteredTokenList.slice(0, tokensShown).map(token => {
392-
return (
393-
<BasicLink to={'/token/' + token.id} key={token.id} onClick={onDismiss}>
394-
<MenuItem>
395-
<TokenLogo address={token.id} style={{ marginRight: '10px' }} />
396-
<span>{token.name}</span>
397-
<span>({token.symbol})</span>
398-
</MenuItem>
399-
</BasicLink>
400-
)
401-
})}
402-
403-
<Heading
404-
hide={!(Object.keys(filteredTokenList).length > 3 && Object.keys(filteredTokenList).length >= tokensShown)}
405-
>
406-
<Blue
407-
onClick={() => {
408-
setTokensShown(tokensShown + 5)
409-
}}
410-
>
411-
See more...
412-
</Blue>
413-
</Heading>
414-
</div>
432+
{Object.keys(filteredTokenList).length === 0 && <MenuItem>No results</MenuItem>}
433+
{filteredTokenList.slice(0, tokensShown).map(token => {
434+
return (
435+
<BasicLink to={'/token/' + token.id} key={token.id} onClick={onDismiss}>
436+
<MenuItem>
437+
<TokenLogo address={token.id} style={{ marginRight: '10px' }} />
438+
<span>{token.name}</span>
439+
<span>({token.symbol})</span>
440+
</MenuItem>
441+
</BasicLink>
442+
)
443+
})}
444+
445+
<Heading
446+
hide={!(Object.keys(filteredTokenList).length > 3 && Object.keys(filteredTokenList).length >= tokensShown)}
447+
>
448+
<Blue onClick={() => setTokensShown(tokensShown + 5)}>See more...</Blue>
449+
</Heading>
415450
</Menu>
416451
</div>
417452
)

src/hooks/index.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,31 @@ export function useCopyClipboard(timeout = 500) {
5555
return [isCopied, staticCopy]
5656
}
5757

58+
export function useKeyPress(targetKey: string) {
59+
const [keyPressed, setKeyPressed] = useState(false)
60+
61+
const downHandler = useRef(undefined)
62+
downHandler.current = ({ key }) => {
63+
if (key === targetKey) setKeyPressed(true)
64+
}
65+
66+
const upHandler = useRef(undefined)
67+
upHandler.current = ({ key }) => {
68+
if (key === targetKey) setKeyPressed(false)
69+
}
70+
71+
useEffect(() => {
72+
window.addEventListener('keydown', downHandler.current)
73+
window.addEventListener('keyup', upHandler.current)
74+
return () => {
75+
window.removeEventListener('keydown', downHandler.current)
76+
window.removeEventListener('keyup', upHandler.current)
77+
}
78+
}, [])
79+
80+
return keyPressed
81+
}
82+
5883
export const useOutsideClick = (ref, ref2, callback) => {
5984
const handleClick = e => {
6085
if (ref.current && ref.current && !ref2.current) {

0 commit comments

Comments
 (0)