Skip to content

Commit

Permalink
feat: refactor dialogs and bottom sheets (#538)
Browse files Browse the repository at this point in the history
* feat: refactor dialogs and bottom sheets

* test: add test

* fix: update snapshot
  • Loading branch information
just-toby authored Mar 7, 2023
1 parent 15bdb22 commit ba4aec1
Show file tree
Hide file tree
Showing 8 changed files with 222 additions and 79 deletions.
17 changes: 12 additions & 5 deletions src/components/BottomSheetModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const BottomSheetModalBackdrop = styled.div<{ className?: string }>`
z-index: ${({ theme }) => theme.zIndex.modal - 1};
`

const Wrapper = styled.div<{ open: boolean }>`
const Wrapper = styled.div`
border-radius: 0;
bottom: 0;
left: 0;
Expand Down Expand Up @@ -85,7 +85,7 @@ export function BottomSheetModal({ children, onClose, open, title }: BottomSheet

return (
<>
<RootElement ref={setRootElement} open={open} />
<RootElement ref={setRootElement} open={open} onClose={onClose} />
<DialogProvider value={rootElement}>
{open && (
<Dialog color="dialog" onClose={onClose} forceContain>
Expand All @@ -102,17 +102,24 @@ export function BottomSheetModal({ children, onClose, open, title }: BottomSheet

type RootElementProps = PropsWithChildren<{
open: boolean
onClose: () => void
}>

const RootElement = forwardRef<HTMLDivElement, RootElementProps>(function RootWrapper(
{ children, open }: RootElementProps,
{ children, open, onClose }: RootElementProps,
ref
) {
return createPortal(
<>
{/* TODO (WEB-2767): Support dismissing modal when clicking on backdrop */}
<BottomSheetModalBackdrop className={!open ? 'hidden' : undefined} />
<Wrapper open={open} ref={ref}>
<BottomSheetModalBackdrop
className={!open ? 'hidden' : undefined}
onClick={(e) => {
onClose()
e.stopPropagation()
}}
/>
<Wrapper data-testid="BottomSheetModal__Wrapper" ref={ref}>
{children}
</Wrapper>
</>,
Expand Down
1 change: 1 addition & 0 deletions src/components/Popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ export default function Popover({
style={styles.popper}
{...attributes.popper}
onClick={containerOnClick}
data-testid="popover-container"
>
{content}
{showArrow && (
Expand Down
98 changes: 98 additions & 0 deletions src/components/ResponsiveDialog.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { useIsMobileWidth } from 'hooks/useIsMobileWidth'
import { render } from 'test'
import { Provider as ThemeProvider } from 'theme'

import { useIsDialogPageCentered } from './Dialog'
import { ResponsiveDialog } from './ResponsiveDialog'

jest.mock('hooks/useIsMobileWidth')
jest.mock('./Dialog')

const mockUseIsMobileWidth = useIsMobileWidth as jest.Mock
const mockUseIsDialogPageCentered = useIsDialogPageCentered as jest.Mock

describe('ResponsiveDialog', () => {
beforeEach(() => {
mockUseIsMobileWidth.mockReturnValue(false)
mockUseIsDialogPageCentered.mockReturnValue(false)
})

it('renders a dialog by default (nothing rendered when closed)', () => {
const view = render(
<ThemeProvider>
<ResponsiveDialog open={false} setOpen={() => null}>
<div>dialog content</div>
</ResponsiveDialog>
</ThemeProvider>
)

expect(view.queryByText('dialog content')).toBeNull()
})

it('renders a popover when defaultView is set to popover', () => {
const view = render(
<ThemeProvider>
<ResponsiveDialog open={true} setOpen={() => null} defaultView="popover">
<div>popover content</div>
</ResponsiveDialog>
</ThemeProvider>
)

expect(view.getByTestId('popover-container')).toBeTruthy()
})

it('renders a bottom sheet when on mobile and pageCenteredDialogsEnabled is true', () => {
mockUseIsMobileWidth.mockReturnValue(true)
mockUseIsDialogPageCentered.mockReturnValue(true)

const view = render(
<ThemeProvider>
<ResponsiveDialog open={true} setOpen={() => null}>
<div>bottom sheet content</div>
</ResponsiveDialog>
</ThemeProvider>
)

expect(view.getByTestId('BottomSheetModal__Wrapper')).toBeTruthy()
})

it('renders a bottom sheet when on mobile and mobileBottomSheet is true', () => {
mockUseIsMobileWidth.mockReturnValue(true)

const view = render(
<ThemeProvider>
<ResponsiveDialog open={true} setOpen={() => null} mobileBottomSheet={true}>
<div>bottom sheet content</div>
</ResponsiveDialog>
</ThemeProvider>
)

expect(view.getByTestId('BottomSheetModal__Wrapper')).toBeTruthy()
})

it('renders a popover when on mobile and defaultView is set to popover', () => {
mockUseIsMobileWidth.mockReturnValue(true)

const view = render(
<ThemeProvider>
<ResponsiveDialog open={true} setOpen={() => null} defaultView="popover">
<div>popover content</div>
</ResponsiveDialog>
</ThemeProvider>
)

expect(view.getByTestId('popover-container')).toBeTruthy()
})

it('renders an anchor when provided', () => {
const view = render(
<ThemeProvider>
<ResponsiveDialog open={true} setOpen={() => null} anchor={<div>anchor</div>}>
<div>dialog content</div>
</ResponsiveDialog>
</ThemeProvider>
)

expect(view.getByText('anchor')).toBeTruthy()
})
})
73 changes: 73 additions & 0 deletions src/components/ResponsiveDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { useIsMobileWidth } from 'hooks/useIsMobileWidth'
import { useOutsideClickHandler } from 'hooks/useOutsideClickHandler'
import { Info } from 'icons'
import { PropsWithChildren, useState } from 'react'

import { BottomSheetModal } from './BottomSheetModal'
import { IconButton } from './Button'
import Dialog, { useIsDialogPageCentered } from './Dialog'
import Popover, { PopoverBoundaryProvider } from './Popover'

interface ResponsiveDialogProps {
open: boolean
setOpen: (open: boolean) => void
// when not on a mobile width, use the default view.
defaultView?: 'dialog' | 'popover'
// an anchor view to render when the dialog is closed. Useful as an entry point to a bottom sheet or popover.
// if not provided, a default info icon will be used.
anchor?: React.ReactNode
// If true, always render the dialog as a bottom sheet on mobile.
// If false, it will only be a bottom sheet if it was page-centered.
mobileBottomSheet?: boolean
bottomSheetTitle?: string
}

/**
* A Dialog or Popover that renders as a bottom sheet on mobile.
*/
export function ResponsiveDialog({
children,
open,
setOpen,
defaultView = 'dialog',
anchor,
mobileBottomSheet,
bottomSheetTitle,
}: PropsWithChildren<ResponsiveDialogProps>) {
const isMobile = useIsMobileWidth()
const pageCenteredDialogsEnabled = useIsDialogPageCentered()
const [wrapper, setWrapper] = useState<HTMLDivElement | null>(null)
useOutsideClickHandler(isMobile ? null : wrapper, () => setOpen(false))

if (isMobile && (pageCenteredDialogsEnabled || mobileBottomSheet)) {
return (
<>
{anchor}
<BottomSheetModal onClose={() => setOpen(false)} open={open} title={bottomSheetTitle}>
{children}
</BottomSheetModal>
</>
)
} else if (defaultView === 'popover') {
return (
<div ref={setWrapper}>
<PopoverBoundaryProvider value={wrapper}>
<Popover showArrow={false} offset={10} show={open} placement="top-end" content={children}>
{anchor ?? <IconButton icon={Info} />}
</Popover>
</PopoverBoundaryProvider>
</div>
)
} else {
return (
<>
{anchor}
{open && (
<Dialog color="container" onClose={() => setOpen(false)}>
{children}
</Dialog>
)}
</>
)
}
}
36 changes: 13 additions & 23 deletions src/components/Swap/Settings/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { BottomSheetModal } from 'components/BottomSheetModal'
import { ResponsiveDialog } from 'components/ResponsiveDialog'
import Rule from 'components/Rule'
import { useIsMobileWidth } from 'hooks/useIsMobileWidth'
import { useOnEscapeHandler } from 'hooks/useOnEscapeHandler'
import { useOutsideClickHandler } from 'hooks/useOutsideClickHandler'
import { Settings as SettingsIcon } from 'icons'
import { useAtom } from 'jotai'
import { useState } from 'react'
Expand All @@ -12,7 +10,7 @@ import { AnimationSpeed } from 'theme'

import { IconButton } from '../../Button'
import Column from '../../Column'
import Popover, { PopoverBoundaryProvider } from '../../Popover'
import { PopoverBoundaryProvider } from '../../Popover'
import MaxSlippageSelect from './MaxSlippageSelect'
import RouterPreferenceToggle from './RouterPreferenceToggle'
import TransactionTtlInput from './TransactionTtlInput'
Expand Down Expand Up @@ -57,27 +55,19 @@ const SettingsButton = styled(IconButton)`

export default function Settings() {
const [open, setOpen] = useState(false)
const [wrapper, setWrapper] = useState<HTMLDivElement | null>(null)
const isMobile = useIsMobileWidth()
useOutsideClickHandler(isMobile ? null : wrapper, () => setOpen(false))

useOnEscapeHandler(() => setOpen(false))

return (
<div ref={setWrapper}>
{isMobile ? (
<>
<SettingsButton onClick={() => setOpen(!open)} icon={SettingsIcon} />
<BottomSheetModal title="Settings" onClose={() => setOpen(false)} open={open}>
<SettingsMenu />
</BottomSheetModal>
</>
) : (
<PopoverBoundaryProvider value={wrapper}>
<Popover showArrow={false} offset={10} show={open} placement="top-end" content={<SettingsMenu />}>
<SettingsButton data-testid="settings-button" onClick={() => setOpen(!open)} icon={SettingsIcon} />
</Popover>
</PopoverBoundaryProvider>
)}
</div>
<ResponsiveDialog
open={open}
setOpen={setOpen}
defaultView="popover"
anchor={<SettingsButton data-testid="settings-button" onClick={() => setOpen(!open)} icon={SettingsIcon} />}
mobileBottomSheet={true}
bottomSheetTitle="Settings"
>
<SettingsMenu />
</ResponsiveDialog>
)
}
51 changes: 15 additions & 36 deletions src/components/Swap/SwapActionButton/SwapButton.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { Trans } from '@lingui/macro'
import { useWeb3React } from '@web3-react/core'
import { BottomSheetModal } from 'components/BottomSheetModal'
import { useAsyncError } from 'components/Error/ErrorBoundary'
import { ResponsiveDialog } from 'components/ResponsiveDialog'
import { useSwapInfo } from 'hooks/swap'
import { useSwapCallback } from 'hooks/swap/useSwapCallback'
import { useConditionalHandler } from 'hooks/useConditionalHandler'
import { useIsMobileWidth } from 'hooks/useIsMobileWidth'
import { useSetOldestValidBlock } from 'hooks/useIsValidBlock'
import { AllowanceState } from 'hooks/usePermit2Allowance'
import { usePermit2 as usePermit2Enabled } from 'hooks/useSyncFlags'
Expand All @@ -19,7 +18,6 @@ import { TransactionType } from 'state/transactions'
import invariant from 'tiny-invariant'

import ActionButton from '../../ActionButton'
import Dialog, { useIsDialogPageCentered } from '../../Dialog'
import { SummaryDialog } from '../Summary'
import { useCollapseToolbar } from '../Toolbar/ToolbarContext'
import useOnSubmit from './useOnSubmit'
Expand Down Expand Up @@ -107,44 +105,25 @@ export default function SwapButton({ disabled }: { disabled: boolean }) {
setOpen(await onReviewSwapClick())
}, [onReviewSwapClick, collapseToolbar])

const isMobile = useIsMobileWidth()
const pageCenteredDialogsEnabled = useIsDialogPageCentered()

return (
<>
<ActionButton color={color} onClick={onClick} disabled={disabled}>
<Trans>Review swap</Trans>
</ActionButton>
{trade &&
(isMobile && pageCenteredDialogsEnabled ? (
<BottomSheetModal onClose={() => setOpen(false)} open={open && Boolean(trade)}>
<SummaryDialog
trade={trade}
slippage={slippage}
gasUseEstimateUSD={gasUseEstimateUSD}
inputUSDC={inputUSDC}
outputUSDC={outputUSDC}
impact={impact}
onConfirm={onSwap}
allowance={allowance}
/>
</BottomSheetModal>
) : (
open && (
<Dialog color="container" onClose={() => setOpen(false)}>
<SummaryDialog
trade={trade}
slippage={slippage}
gasUseEstimateUSD={gasUseEstimateUSD}
inputUSDC={inputUSDC}
outputUSDC={outputUSDC}
impact={impact}
onConfirm={onSwap}
allowance={allowance}
/>
</Dialog>
)
))}
{trade && (
<ResponsiveDialog open={open} setOpen={setOpen}>
<SummaryDialog
trade={trade}
slippage={slippage}
gasUseEstimateUSD={gasUseEstimateUSD}
inputUSDC={inputUSDC}
outputUSDC={outputUSDC}
impact={impact}
onConfirm={onSwap}
allowance={allowance}
/>
</ResponsiveDialog>
)}
</>
)
}
Loading

1 comment on commit ba4aec1

@vercel
Copy link

@vercel vercel bot commented on ba4aec1 Mar 7, 2023

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-seven-tau.vercel.app
widgets-git-main-uniswap.vercel.app
widgets-uniswap.vercel.app

Please sign in to comment.