Skip to content

Commit

Permalink
feat(onTokenSelectorClick): add onTokenSelectorClick event handler (#129
Browse files Browse the repository at this point in the history
)

* feat(onTokenSelectorClick): add event handler and optional handler setting hook

* style(lint): lint action with ESLint

* use handler

* style(lint): lint action with ESLint

* tests

* style(lint): lint action with ESLint

* tests!

* style(lint): lint action with ESLint

* add missing useCallback dep

* test: expand test utils

* update some test util stuff

* style(lint): lint action with ESLint

* useSyncEventHandlers

* drop assert (node.js core lib not available in frontend env)

* test cleanup

* style(lint): lint action with ESLint

* add Field arg to handler

* style(lint): lint action with ESLint

* add field hook dep

Co-authored-by: Lint Action <lint-action@uniswap.org>
Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
  • Loading branch information
3 people authored Aug 11, 2022
1 parent 1094e01 commit d88541b
Show file tree
Hide file tree
Showing 15 changed files with 272 additions and 103 deletions.
26 changes: 12 additions & 14 deletions src/components/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,20 +69,18 @@ interface HeaderProps {
export function Header({ title, children, ruled }: HeaderProps) {
const onClose = useContext(OnCloseContext)
return (
<>
<Column>
<HeaderRow iconSize={1.2}>
<TextButton color="primary" onClick={onClose}>
<Row justify="flex-start" gap={0.5}>
<ChevronLeft />
<Row gap={0.5}>{title && <ThemedText.Subhead1>{title}</ThemedText.Subhead1>}</Row>
</Row>
</TextButton>
{children}
</HeaderRow>
{ruled && <Rule padded />}
</Column>
</>
<Column data-testid="dialog-header">
<HeaderRow iconSize={1.2}>
<TextButton color="primary" onClick={onClose}>
<Row justify="flex-start" gap={0.5}>
<ChevronLeft />
<Row gap={0.5}>{title && <ThemedText.Subhead1>{title}</ThemedText.Subhead1>}</Row>
</Row>
</TextButton>
{children}
</HeaderRow>
{ruled && <Rule padded />}
</Column>
)
}

Expand Down
5 changes: 3 additions & 2 deletions src/components/Swap/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,11 @@ export default function Input({ disabled, focused }: InputProps) {
return (
<InputColumn gap={0.5} approved={mockApproved}>
<TokenInput
currency={inputCurrency}
amount={amount}
max={max}
currency={inputCurrency}
disabled={disabled}
field={Field.INPUT}
max={max}
onChangeInput={updateInputAmount}
onChangeCurrency={updateInputCurrency}
loading={isLoading}
Expand Down
3 changes: 2 additions & 1 deletion src/components/Swap/Output.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,10 @@ export default function Output({ disabled, focused, children }: PropsWithChildre
</ThemedText.Subhead1>
</Row>
<TokenInput
currency={swapOutputCurrency}
amount={amount}
currency={swapOutputCurrency}
disabled={disabled}
field={Field.OUTPUT}
onChangeInput={updateSwapOutputAmount}
onChangeCurrency={updateSwapOutputCurrency}
loading={isLoading}
Expand Down
11 changes: 9 additions & 2 deletions src/components/Swap/Settings/MaxSlippageSelect.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import userEvent from '@testing-library/user-event'
import { MAX_VALID_SLIPPAGE } from 'hooks/useSlippage'
import { act, render, RenderResult, user } from 'test/utils'
import { act, renderComponent, RenderResult } from 'test'

import MaxSlippageSelect from './MaxSlippageSelect'

Expand All @@ -10,7 +11,7 @@ describe('MaxSlippageSelect', () => {
let input: HTMLInputElement

beforeEach(async () => {
el = render(<MaxSlippageSelect />)
el = renderComponent(<MaxSlippageSelect />)
auto = (await el.findByTestId('auto')) as HTMLOptionElement
custom = (await el.findByTestId('custom')) as HTMLOptionElement
input = (await el.findByTestId('input')) as HTMLInputElement
Expand All @@ -19,18 +20,24 @@ describe('MaxSlippageSelect', () => {
})

it('accepts integral input', async () => {
const user = userEvent.setup()

await act(async () => user.type(input, '1'))
expect(custom.selected).toBeTruthy()
expect(input.value).toBe('1')
})

it('accepts decimal input', async () => {
const user = userEvent.setup()

await act(async () => user.type(input, '1.5'))
expect(custom.selected).toBeTruthy()
expect(input.value).toBe('1.5')
})

it('selects auto slippage when input is invalid', async () => {
const user = userEvent.setup()

const INVALID_SLIPPAGE = '51'
expect(Number(INVALID_SLIPPAGE)).toBeGreaterThan(Number(MAX_VALID_SLIPPAGE.toFixed()))

Expand Down
18 changes: 10 additions & 8 deletions src/components/Swap/TokenInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import 'setimmediate'
import { Trans } from '@lingui/macro'
import { Currency } from '@uniswap/sdk-core'
import { loadingTransitionCss } from 'css/loading'
import { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { PropsWithChildren, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Field } from 'state/swap'
import styled, { keyframes } from 'styled-components/macro'
import { ThemedText } from 'theme'

Expand Down Expand Up @@ -52,26 +53,27 @@ const MaxButton = styled(Button)`
`

interface TokenInputProps {
currency?: Currency
amount: string
max?: string
currency?: Currency
disabled?: boolean
field: Field
max?: string
onChangeInput: (input: string) => void
onChangeCurrency: (currency: Currency) => void
loading?: boolean
children: ReactNode
}

export default function TokenInput({
currency,
amount,
max,
currency,
disabled,
field,
max,
onChangeInput,
onChangeCurrency,
loading,
children,
}: TokenInputProps) {
}: PropsWithChildren<TokenInputProps>) {
const input = useRef<HTMLInputElement>(null)
const onSelect = useCallback(
(currency: Currency) => {
Expand Down Expand Up @@ -122,7 +124,7 @@ export default function TokenInput({
</ThemedText.ButtonMedium>
</MaxButton>
)}
<TokenSelect value={currency} collapsed={showMax} disabled={disabled} onSelect={onSelect} />
<TokenSelect value={currency} collapsed={showMax} disabled={disabled} onSelect={onSelect} field={field} />
</TokenInputRow>
{children}
</Column>
Expand Down
22 changes: 5 additions & 17 deletions src/components/Swap/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import useSyncTokenDefaults, { TokenDefaults } from 'hooks/swap/useSyncTokenDefa
import { usePendingTransactions } from 'hooks/transactions'
import useHasFocus from 'hooks/useHasFocus'
import useOnSupportedNetwork from 'hooks/useOnSupportedNetwork'
import useSyncEventHandlers from 'hooks/useSyncEventHandlers'
import { useAtom } from 'jotai'
import { useEffect, useState } from 'react'
import { displayTxHashAtom, onReviewSwapClickAtom } from 'state/swap'
import { useState } from 'react'
import { displayTxHashAtom } from 'state/swap'
import { SwapTransactionInfo, Transaction, TransactionType, WrapTransactionInfo } from 'state/transactions'
import { onConnectWalletClickAtom } from 'state/wallet'

import Dialog from '../Dialog'
import Header from '../Header'
Expand Down Expand Up @@ -51,26 +51,14 @@ export interface SwapProps extends TokenDefaults, FeeOptions {
routerUrl?: string
onConnectWalletClick?: () => void | Promise<boolean>
onReviewSwapClick?: () => void | Promise<boolean>
onTokenSelectorClick?: () => void | Promise<boolean>
}

export default function Swap(props: SwapProps) {
useValidate(props)
useSyncConvenienceFee(props)
useSyncTokenDefaults(props)

const [onReviewSwapClick, setOnReviewSwapClick] = useAtom(onReviewSwapClickAtom)
useEffect(() => {
if (props.onReviewSwapClick !== onReviewSwapClick) {
setOnReviewSwapClick((old: (() => void | Promise<boolean>) | undefined) => (old = props.onReviewSwapClick))
}
}, [props.onReviewSwapClick, onReviewSwapClick, setOnReviewSwapClick])

const [onConnectWalletClick, setOnConnectWalletClick] = useAtom(onConnectWalletClickAtom)
useEffect(() => {
if (props.onConnectWalletClick !== onConnectWalletClick) {
setOnConnectWalletClick((old: (() => void | Promise<boolean>) | undefined) => (old = props.onConnectWalletClick))
}
}, [props.onConnectWalletClick, onConnectWalletClick, setOnConnectWalletClick])
useSyncEventHandlers(props)

const { isActive } = useWeb3React()
const [wrapper, setWrapper] = useState<HTMLDivElement | null>(null)
Expand Down
74 changes: 74 additions & 0 deletions src/components/TokenSelect/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* @jest-environment hardhat/dist/jsdom
*/

import '@ethersproject/providers'
import 'jest-environment-hardhat'

import { Field, onTokenSelectorClickAtom } from '../../state/swap'
import { renderWidget, userEvent } from '../../test'
import TokenSelect from './'

describe('TokenSelect.tsx', () => {
describe('onTokenSelectorClick', () => {
it('should fire the onTokenSelectorClick handler when it exists', async () => {
const user = userEvent.setup()
const onTokenSelectorClick = jest.fn()

const component = renderWidget(
<TokenSelect field={Field.INPUT} value={undefined} collapsed disabled={false} onSelect={jest.fn()} />,
{
initialAtomValues: [[onTokenSelectorClickAtom, onTokenSelectorClick]],
}
)

await user.click(component.getByRole('button'))
expect(onTokenSelectorClick).toHaveBeenCalledWith(Field.INPUT)
})
it('should continue if the handler promise resolves to true', async () => {
const user = userEvent.setup()

const onTokenSelectorClick = jest.fn().mockResolvedValueOnce(true)

const component = renderWidget(
<TokenSelect field={Field.INPUT} value={undefined} collapsed={false} disabled={false} onSelect={jest.fn()} />,
{
initialAtomValues: [[onTokenSelectorClickAtom, onTokenSelectorClick]],
}
)

await user.click(component.getByRole('button'))
expect(component.getByTestId('dialog-header').textContent).toBe('Select a token')
})
it('should halt if the handler promise resolves to false', async () => {
const user = userEvent.setup()

const onTokenSelectorClick = jest.fn().mockResolvedValueOnce(false)

const component = renderWidget(
<TokenSelect field={Field.INPUT} value={undefined} collapsed={false} disabled={false} onSelect={jest.fn()} />,
{
initialAtomValues: [[onTokenSelectorClickAtom, onTokenSelectorClick]],
}
)

await user.click(component.getByRole('button'))
expect(() => component.getByTestId('dialog-header')).toThrow()
})
it('should halt if the handler promise rejects', async () => {
const user = userEvent.setup()

const onTokenSelectorClick = jest.fn().mockRejectedValueOnce(false)

const component = renderWidget(
<TokenSelect field={Field.INPUT} value={undefined} collapsed={false} disabled={false} onSelect={jest.fn()} />,
{
initialAtomValues: [[onTokenSelectorClickAtom, onTokenSelectorClick]],
}
)

await user.click(component.getByRole('button'))
expect(() => component.getByTestId('dialog-header')).toThrow()
})
})
})
22 changes: 19 additions & 3 deletions src/components/TokenSelect/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import { useWeb3React } from '@web3-react/core'
import { useCurrencyBalances } from 'hooks/useCurrencyBalance'
import useNativeCurrency from 'hooks/useNativeCurrency'
import useTokenList, { useIsTokenListLoaded, useQueryTokens } from 'hooks/useTokenList'
import { useAtomValue } from 'jotai/utils'
import { ElementRef, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Field, onTokenSelectorClickAtom } from 'state/swap'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'

Expand Down Expand Up @@ -119,17 +121,31 @@ export function TokenSelectDialog({ value, onSelect, onClose }: TokenSelectDialo
}

interface TokenSelectProps {
value?: Currency
collapsed: boolean
disabled?: boolean
field: Field
onSelect: (value: Currency) => void
value?: Currency
}

export default memo(function TokenSelect({ value, collapsed, disabled, onSelect }: TokenSelectProps) {
export default memo(function TokenSelect({ collapsed, disabled, field, onSelect, value }: TokenSelectProps) {
usePrefetchBalances()

const [open, setOpen] = useState(false)
const onOpen = useCallback(() => setOpen(true), [])
const onTokenSelectorClick = useAtomValue(onTokenSelectorClickAtom)
const onOpen = useCallback(() => {
const promise = onTokenSelectorClick?.(field)
if (promise) {
return promise
.then((open) => {
setOpen(open)
})
.catch(() => {
setOpen(false)
})
}
return setOpen(true)
}, [field, onTokenSelectorClick])
const selectAndClose = useCallback(
(value: Currency) => {
onSelect(value)
Expand Down
Loading

1 comment on commit d88541b

@vercel
Copy link

@vercel vercel bot commented on d88541b Aug 11, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

widgets – ./

widgets-uniswap.vercel.app
widgets-seven-tau.vercel.app
widgets-git-main-uniswap.vercel.app

Please sign in to comment.