From f4dd3f02eed17660b4a49aadd9fdc29453d69f6c Mon Sep 17 00:00:00 2001 From: Jordan Frankfurt Date: Tue, 14 Jul 2020 20:58:04 -0400 Subject: [PATCH] add basic keyboard navigation --- src/components/Search/index.js | 167 ++++++++++++++++++++------------- src/hooks/index.ts | 25 +++++ 2 files changed, 126 insertions(+), 66 deletions(-) diff --git a/src/components/Search/index.js b/src/components/Search/index.js index e56bc0358..5a58a7517 100644 --- a/src/components/Search/index.js +++ b/src/components/Search/index.js @@ -13,6 +13,8 @@ import { useMedia } from 'react-use' import { useAllPairsInUniswap, useAllTokensInUniswap } from '../../contexts/GlobalData' import { OVERVIEW_TOKEN_BLACKLIST, OVERVIEW_PAIR_BLACKLIST } from '../../constants' +import { useKeyPress } from '../../hooks' + const Wrapper = styled.div` display: flex; position: relative; @@ -68,7 +70,7 @@ const Menu = styled.div` width: 100%; top: 50px; max-height: 540px; - overflow: scroll; + overflow-y: scroll; left: 0; padding-bottom: 20px; background: white; @@ -89,6 +91,10 @@ const MenuItem = styled(Row)` cursor: pointer; background-color: #f7f8fa; } + :focus { + background-color: #f7f8fa; + outline: black auto 1px; + } ` const Heading = styled(Row)` @@ -119,7 +125,13 @@ export const Search = ({ small = false }) => { const allPairs = useAllPairsInUniswap() const allPairData = useAllPairData() - const [showMenu, toggleMenu] = useState(false) + const [cursor, setCursor] = useState(null) + const downKeyPressed = useKeyPress('ArrowDown') + const escapeKeyPressed = useKeyPress('Escape') + const tabKeyPressed = useKeyPress('Tab') + const upKeyPressed = useKeyPress('ArrowUp') + + const [showMenu, setShowMenu] = useState(false) const [value, setValue] = useState('') const [, toggleShadow] = useState(false) const [, toggleBottomShadow] = useState(false) @@ -134,9 +146,9 @@ export const Search = ({ small = false }) => { useEffect(() => { if (value !== '') { - toggleMenu(true) + setShowMenu(true) } else { - toggleMenu(false) + setShowMenu(false) } }, [value]) @@ -278,10 +290,50 @@ export const Search = ({ small = false }) => { setPairsShown(Math.min(Object.keys(filteredPairList).length, 3)) }, [filteredPairList]) + useEffect(() => { + if (pairsShown + tokensShown === 0) { + setCursor(undefined) + } else if (showMenu && downKeyPressed && cursor === undefined) { + setCursor(0) + } else if (showMenu && downKeyPressed && cursor < pairsShown + tokensShown) { + setCursor(cursor + 1) + } + }, [showMenu, downKeyPressed, pairsShown, tokensShown, cursor]) + + useEffect(() => { + if (pairsShown + tokensShown === 0) { + setCursor(undefined) + } else if (showMenu && upKeyPressed && cursor === undefined) { + setCursor(pairsShown + tokensShown - 1) + } else if (showMenu && upKeyPressed && cursor === 0) { + setCursor(pairsShown + tokensShown - 1) + } else if (showMenu && upKeyPressed && cursor > 0) { + setCursor(cursor - 1) + } + }, [cursor, pairsShown, showMenu, tokensShown, upKeyPressed]) + + useEffect(() => { + setCursor(undefined) + }, [tabKeyPressed]) + + useEffect(() => { + setCursor(undefined) + setShowMenu(false) + }, [escapeKeyPressed]) + + useEffect(() => { + const canUseCursor = Boolean( + Number.isInteger(cursor) && menuRef.current && menuRef.current.querySelectorAll('a')[cursor] + ) + if (canUseCursor) { + menuRef.current.querySelectorAll('a')[cursor].focus() + } + }, [cursor]) + function onDismiss() { setPairsShown(3) setTokensShown(3) - toggleMenu(false) + setShowMenu(false) setValue('') } @@ -296,7 +348,7 @@ export const Search = ({ small = false }) => { ) { setPairsShown(3) setTokensShown(3) - toggleMenu(false) + setShowMenu(false) } } @@ -338,7 +390,7 @@ export const Search = ({ small = false }) => { setValue(e.target.value) }} onFocus={() => { - toggleMenu(true) + setShowMenu(true) }} /> @@ -350,68 +402,51 @@ export const Search = ({ small = false }) => { Pairs -
- {filteredPairList && Object.keys(filteredPairList).length === 0 && No results} - {filteredPairList && - filteredPairList.slice(0, pairsShown).map(pair => { - if (pair?.token0?.id === '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2') { - pair.token0.name = 'ETH (Wrapped)' - pair.token0.symbol = 'ETH' - } - if (pair?.token1.id === '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2') { - pair.token1.name = 'ETH (Wrapped)' - pair.token1.symbol = 'ETH' - } - return ( - - - - {pair.token0.symbol + '-' + pair.token1.symbol} Pair - - - ) - })} - 3 && Object.keys(filteredPairList).length >= pairsShown)} - > - { - setPairsShown(pairsShown + 5) - }} - > - See more... - - -
+ {Object.keys(filteredPairList).length === 0 && No results} + {filteredPairList.slice(0, pairsShown).map(pair => { + if (pair?.token0?.id === '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2') { + pair.token0.name = 'ETH (Wrapped)' + pair.token0.symbol = 'ETH' + } + if (pair?.token1.id === '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2') { + pair.token1.name = 'ETH (Wrapped)' + pair.token1.symbol = 'ETH' + } + return ( + + + + {pair.token0.symbol + '-' + pair.token1.symbol} Pair + + + ) + })} + 3 && Object.keys(filteredPairList).length >= pairsShown)} + > + setPairsShown(pairsShown + 5)}>See more... + Tokens -
- {Object.keys(filteredTokenList).length === 0 && No results} - {filteredTokenList.slice(0, tokensShown).map(token => { - return ( - - - - {token.name} - ({token.symbol}) - - - ) - })} - - 3 && Object.keys(filteredTokenList).length >= tokensShown)} - > - { - setTokensShown(tokensShown + 5) - }} - > - See more... - - -
+ {Object.keys(filteredTokenList).length === 0 && No results} + {filteredTokenList.slice(0, tokensShown).map(token => { + return ( + + + + {token.name} + ({token.symbol}) + + + ) + })} + + 3 && Object.keys(filteredTokenList).length >= tokensShown)} + > + setTokensShown(tokensShown + 5)}>See more... + ) diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 5082f6f6a..51a8193f3 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -55,6 +55,31 @@ export function useCopyClipboard(timeout = 500) { return [isCopied, staticCopy] } +export function useKeyPress(targetKey: string) { + const [keyPressed, setKeyPressed] = useState(false) + + const downHandler = useRef(undefined) + downHandler.current = ({ key }) => { + if (key === targetKey) setKeyPressed(true) + } + + const upHandler = useRef(undefined) + upHandler.current = ({ key }) => { + if (key === targetKey) setKeyPressed(false) + } + + useEffect(() => { + window.addEventListener('keydown', downHandler.current) + window.addEventListener('keyup', upHandler.current) + return () => { + window.removeEventListener('keydown', downHandler.current) + window.removeEventListener('keyup', upHandler.current) + } + }, []) + + return keyPressed +} + export const useOutsideClick = (ref, ref2, callback) => { const handleClick = e => { if (ref.current && ref.current && !ref2.current) {