Commit b152b115 authored by Zach Pomerantz's avatar Zach Pomerantz Committed by GitHub

fix: token select input handling (#3303)

parent 0f519911
import useTokenList, { DEFAULT_TOKEN_LIST } from 'lib/hooks/useTokenList' import DEFAULT_TOKEN_LIST from '@uniswap/default-token-list'
import useTokenList from 'lib/hooks/useTokenList'
import { Modal } from './Dialog' import { Modal } from './Dialog'
import { TokenSelectDialog } from './TokenSelect' import { TokenSelectDialog } from './TokenSelect'
export default function Fixture() { export default function Fixture() {
useTokenList(DEFAULT_TOKEN_LIST) useTokenList(DEFAULT_TOKEN_LIST.tokens)
return ( return (
<Modal color="module"> <Modal color="module">
......
...@@ -20,7 +20,6 @@ import { ...@@ -20,7 +20,6 @@ import {
} from 'react' } from 'react'
import AutoSizer from 'react-virtualized-auto-sizer' import AutoSizer from 'react-virtualized-auto-sizer'
import { areEqual, FixedSizeList, FixedSizeListProps } from 'react-window' import { areEqual, FixedSizeList, FixedSizeListProps } from 'react-window'
import invariant from 'tiny-invariant'
import { currencyId } from 'utils/currencyId' import { currencyId } from 'utils/currencyId'
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount' import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
...@@ -140,68 +139,73 @@ const TokenOptions = forwardRef<TokenOptionsHandle, TokenOptionsProps>(function ...@@ -140,68 +139,73 @@ const TokenOptions = forwardRef<TokenOptionsHandle, TokenOptionsProps>(function
ref ref
) { ) {
const [focused, setFocused] = useState(false) const [focused, setFocused] = useState(false)
const [hover, setHover] = useState(-1) const [hover, setHover] = useState<{ index: number; currency?: Currency }>({ index: -1 })
useEffect(() => setHover(-1), [tokens]) useEffect(() => {
setHover((hover) => {
const index = hover.currency ? tokens.indexOf(hover.currency) : -1
return { index, currency: tokens[index] }
})
}, [tokens])
const list = useRef<FixedSizeList>(null) const list = useRef<FixedSizeList>(null)
const [element, setElement] = useState<HTMLElement | null>(null)
const scrollTo = useCallback(
(index: number | undefined) => {
if (index === undefined) return
list.current?.scrollToItem(index)
if (focused) {
element?.querySelector<HTMLElement>(`[data-index='${index}']`)?.focus()
}
setHover({ index, currency: tokens[index] })
},
[element, focused, tokens]
)
const onKeyDown = useCallback( const onKeyDown = useCallback(
(e: KeyboardEvent) => { (e: KeyboardEvent) => {
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
if (e.key === 'ArrowDown' && hover < tokens.length - 1) { if (e.key === 'ArrowDown' && hover.index < tokens.length - 1) {
scrollTo(hover + 1) scrollTo(hover.index + 1)
} else if (e.key === 'ArrowUp' && hover > 0) { } else if (e.key === 'ArrowUp' && hover.index > 0) {
scrollTo(hover - 1) scrollTo(hover.index - 1)
} else if (e.key === 'ArrowUp' && hover === -1) { } else if (e.key === 'ArrowUp' && hover.index === -1) {
scrollTo(tokens.length - 1) scrollTo(tokens.length - 1)
} }
e.preventDefault() e.preventDefault()
} }
if (e.key === 'Enter' && hover) { if (e.key === 'Enter' && hover.index !== -1) {
onSelect(tokens[hover]) onSelect(tokens[hover.index])
}
function scrollTo(index: number) {
list.current?.scrollToItem(index)
setHover(index)
} }
}, },
[hover, onSelect, tokens] [hover.index, onSelect, scrollTo, tokens]
) )
const blur = useCallback(() => setHover(-1), []) const blur = useCallback(() => setHover({ index: -1 }), [])
useImperativeHandle(ref, () => ({ onKeyDown, blur }), [blur, onKeyDown]) useImperativeHandle(ref, () => ({ onKeyDown, blur }), [blur, onKeyDown])
const onClick = useCallback(({ token }: BubbledEvent) => token && onSelect(token), [onSelect]) const onClick = useCallback(({ token }: BubbledEvent) => token && onSelect(token), [onSelect])
const onFocus = useCallback(({ index }: BubbledEvent) => { const onFocus = useCallback(
if (index !== undefined) { ({ index }: BubbledEvent) => {
setHover(index)
setFocused(true) setFocused(true)
} scrollTo(index)
}, [])
const onBlur = useCallback(() => setFocused(false), [])
const onMouseMove = useCallback(
({ index, ref }: BubbledEvent) => {
if (index !== undefined) {
setHover(index)
if (focused) {
ref?.focus()
}
}
}, },
[focused] [scrollTo]
) )
const onBlur = useCallback(() => setFocused(false), [])
const onMouseMove = useCallback(({ index }: BubbledEvent) => scrollTo(index), [scrollTo])
const [element, setElement] = useState<HTMLElement | null>(null)
const scrollbar = useScrollbar(element, { padded: true }) const scrollbar = useScrollbar(element, { padded: true })
const onHover = useRef<HTMLDivElement>(null) const onHover = useRef<HTMLDivElement>(null)
// use native onscroll handler to capture Safari's bouncy overscroll effect // use native onscroll handler to capture Safari's bouncy overscroll effect
useNativeEvent(element, 'scroll', (e) => { useNativeEvent(
invariant(element) element,
if (onHover.current) { 'scroll',
// must be set synchronously to avoid jank (avoiding useState) useCallback(() => {
onHover.current.style.marginTop = `${-element.scrollTop}px` if (element && onHover.current) {
} // must be set synchronously to avoid jank (avoiding useState)
}) onHover.current.style.marginTop = `${-element.scrollTop}px`
}
}, [element])
)
return ( return (
<Column <Column
...@@ -215,11 +219,11 @@ const TokenOptions = forwardRef<TokenOptionsHandle, TokenOptionsProps>(function ...@@ -215,11 +219,11 @@ const TokenOptions = forwardRef<TokenOptionsHandle, TokenOptionsProps>(function
style={{ overflow: 'hidden' }} style={{ overflow: 'hidden' }}
> >
{/* OnHover is a workaround to Safari's incorrect (overflow: overlay) implementation */} {/* OnHover is a workaround to Safari's incorrect (overflow: overlay) implementation */}
<OnHover hover={hover} ref={onHover} /> <OnHover hover={hover.index} ref={onHover} />
<AutoSizer disableWidth> <AutoSizer disableWidth>
{({ height }) => ( {({ height }) => (
<TokenList <TokenList
hover={hover} hover={hover.index}
height={height} height={height}
width="100%" width="100%"
itemCount={tokens.length} itemCount={tokens.length}
......
import { useEffect } from 'react' import { useEffect } from 'react'
export default function useNativeEvent( export default function useNativeEvent<K extends keyof HTMLElementEventMap>(
element: HTMLElement | null, element: HTMLElement | null,
...eventListener: Parameters<HTMLElement['addEventListener']> type: K,
listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any,
options?: boolean | AddEventListenerOptions | undefined
) { ) {
useEffect(() => { useEffect(() => {
element?.addEventListener(...eventListener) element?.addEventListener(type, listener, options)
return () => element?.removeEventListener(...eventListener) return () => element?.removeEventListener(type, listener, options)
}, [element, eventListener]) }, [element, type, listener, options])
} }
import { css } from 'lib/theme' import { css } from 'lib/theme'
import { useEffect, useMemo, useState } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react'
import useNativeEvent from './useNativeEvent' import useNativeEvent from './useNativeEvent'
...@@ -52,7 +52,11 @@ export default function useScrollbar(element: HTMLElement | null, { padded = fal ...@@ -52,7 +52,11 @@ export default function useScrollbar(element: HTMLElement | null, { padded = fal
useEffect(() => { useEffect(() => {
setOverflow(hasOverflow(element)) setOverflow(hasOverflow(element))
}, [element]) }, [element])
useNativeEvent(element, 'transitionend', () => setOverflow(hasOverflow(element))) useNativeEvent(
element,
'transitionend',
useCallback(() => setOverflow(hasOverflow(element)), [element])
)
return useMemo(() => (overflow ? scrollbarCss(padded) : overflowCss), [overflow, padded]) return useMemo(() => (overflow ? scrollbarCss(padded) : overflowCss), [overflow, padded])
function hasOverflow(element: HTMLElement | null) { function hasOverflow(element: HTMLElement | null) {
......
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