Skip to content

Commit

Permalink
feat: Convert settings into popover (Uniswap#361)
Browse files Browse the repository at this point in the history
* convert settings into popover

* BoundaryProvider -> PopoverBoundaryProvider

* remove extra asterisk

* escape to close, add todo

* rename to useOnEscapeHandler

* rotate back on mouse leave

* rotate0 on mouseleave
  • Loading branch information
tinaszheng authored Jan 10, 2023
1 parent 2d54190 commit f859214
Show file tree
Hide file tree
Showing 6 changed files with 70 additions and 72 deletions.
8 changes: 3 additions & 5 deletions src/components/Dialog.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'wicg-inert'

import { useOnEscapeHandler } from 'hooks/useOnEscapeHandler'
import { ChevronLeft } from 'icons'
import { largeIconCss } from 'icons'
import { createContext, ReactElement, ReactNode, useContext, useEffect, useRef, useState } from 'react'
Expand Down Expand Up @@ -135,11 +136,8 @@ export default function Dialog({ color, children, onClose }: DialogProps) {
return (context.element?.childElementCount ?? 0) > 1 ? Animation.PAGING : Animation.CLOSING
})

useEffect(() => {
const close = (e: KeyboardEvent) => e.key === 'Escape' && onClose?.()
document.addEventListener('keydown', close, true)
return () => document.removeEventListener('keydown', close, true)
}, [onClose])
useOnEscapeHandler(onClose)

return (
context.element &&
createPortal(
Expand Down
4 changes: 0 additions & 4 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@ const HeaderRow = styled(Row)`
margin: 0 0.75em 1em;
padding-top: 0.25em;
${largeIconCss}
button {
height: ${({ iconSize }) => iconSize}em;
}
`

export interface HeaderProps {
Expand Down
28 changes: 20 additions & 8 deletions src/components/Popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import { Layer } from 'theme'

const BoundaryContext = createContext<HTMLDivElement | null>(null)

export const BoundaryProvider = BoundaryContext.Provider
/* Defines a boundary component past which a Popover should not overflow. */
export const PopoverBoundaryProvider = BoundaryContext.Provider

const PopoverContainer = styled.div<{ show: boolean }>`
background-color: ${({ theme }) => theme.dialog};
Expand Down Expand Up @@ -85,9 +86,18 @@ export interface PopoverProps {
placement: Placement
offset?: number
contained?: true
showArrow?: boolean
}

export default function Popover({ content, show, children, placement, offset, contained }: PopoverProps) {
export default function Popover({
content,
show,
children,
placement,
offset,
contained,
showArrow = true,
}: PopoverProps) {
const boundary = useContext(BoundaryContext)
const reference = useRef<HTMLDivElement>(null)

Expand Down Expand Up @@ -138,12 +148,14 @@ export default function Popover({ content, show, children, placement, offset, co
createPortal(
<PopoverContainer show={show} ref={setPopover} style={styles.popper} {...attributes.popper}>
{content}
<Arrow
className={`arrow-${attributes.popper?.['data-popper-placement'] ?? ''}`}
ref={setArrow}
style={styles.arrow}
{...attributes.arrow}
/>
{showArrow && (
<Arrow
className={`arrow-${attributes.popper?.['data-popper-placement'] ?? ''}`}
ref={setArrow}
style={styles.arrow}
{...attributes.arrow}
/>
)}
</PopoverContainer>,
boundary
)}
Expand Down
85 changes: 33 additions & 52 deletions src/components/Swap/Settings/index.tsx
Original file line number Diff line number Diff line change
@@ -1,72 +1,53 @@
import { Trans } from '@lingui/macro'
import { useOnEscapeHandler } from 'hooks/useOnEscapeHandler'
import { Settings as SettingsIcon } from 'icons'
import { useAtomValue, useResetAtom } from 'jotai/utils'
import { useCallback, useState } from 'react'
import { swapEventHandlersAtom } from 'state/swap'
import { settingsAtom } from 'state/swap/settings'
import { useState } from 'react'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'

import { IconButton, TextButton } from '../../Button'
import { IconButton } from '../../Button'
import Column from '../../Column'
import Dialog, { Header } from '../../Dialog'
import { BoundaryProvider } from '../../Popover'
import Popover, { PopoverBoundaryProvider } from '../../Popover'
import MaxSlippageSelect from './MaxSlippageSelect'
import TransactionTtlInput from './TransactionTtlInput'

export function SettingsDialog() {
export function SettingsPopover() {
const [boundary, setBoundary] = useState<HTMLDivElement | null>(null)
const { onSettingsReset } = useAtomValue(swapEventHandlersAtom)
const resetSettingsBase = useResetAtom(settingsAtom)
const resetSettings = useCallback(() => {
onSettingsReset?.()
resetSettingsBase()
}, [onSettingsReset, resetSettingsBase])

// TODO (WEB-2754): add back reset settings functionality
return (
<>
<Header title={<Trans>Settings</Trans>} ruled>
<TextButton onClick={resetSettings}>
<ThemedText.ButtonSmall>
<Trans>Reset</Trans>
</ThemedText.ButtonSmall>
</TextButton>
</Header>
<Column gap={1} style={{ paddingTop: '1em' }} ref={setBoundary} padded>
<BoundaryProvider value={boundary}>
<MaxSlippageSelect />
<TransactionTtlInput />
</BoundaryProvider>
</Column>
</>
<Column gap={1} style={{ paddingTop: '1em' }} ref={setBoundary} padded>
<PopoverBoundaryProvider value={boundary}>
<MaxSlippageSelect />
<TransactionTtlInput />
</PopoverBoundaryProvider>
</Column>
)
}

const SettingsButton = styled(IconButton)<{ hover: boolean }>`
const SettingsButton = styled(IconButton)`
${SettingsIcon}:hover {
transform: rotate(45deg);
transition: transform 0.25s;
}
${SettingsIcon} {
transform: ${({ hover }) => hover && 'rotate(45deg)'};
transition: ${({ hover }) => hover && 'transform 0.25s'};
will-change: transform;
transform: rotate(0);
transition: transform 0.25s;
}
`

export default function Settings({ disabled }: { disabled?: boolean }) {
export default function Settings() {
const [open, setOpen] = useState(false)
const [hover, setHover] = useState(false)
const [wrapper, setWrapper] = useState<HTMLDivElement | null>(null)

useOnEscapeHandler(() => setOpen(false))

return (
<>
<SettingsButton
disabled={disabled}
hover={hover}
onClick={() => setOpen(true)}
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
icon={SettingsIcon}
/>
{open && (
<Dialog color="module" onClose={() => setOpen(false)}>
<SettingsDialog />
</Dialog>
)}
</>
<div ref={setWrapper}>
<PopoverBoundaryProvider value={wrapper}>
<Popover showArrow={false} offset={10} show={open} placement="top-end" content={<SettingsPopover />}>
<SettingsButton onClick={() => setOpen(!open)} icon={SettingsIcon} />
</Popover>
</PopoverBoundaryProvider>
</div>
)
}
6 changes: 3 additions & 3 deletions src/components/Swap/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { displayTxHashAtom } from 'state/swap'

import Dialog from '../Dialog'
import Header from '../Header'
import { BoundaryProvider } from '../Popover'
import { PopoverBoundaryProvider } from '../Popover'
import Input from './Input'
import Output from './Output'
import ReverseButton from './ReverseButton'
Expand Down Expand Up @@ -55,7 +55,7 @@ export default function Swap(props: SwapProps) {
<Settings />
</Header>
<div ref={setWrapper}>
<BoundaryProvider value={wrapper}>
<PopoverBoundaryProvider value={wrapper}>
<SwapInfoProvider routerUrl={props.routerUrl}>
<Input />
<ReverseButton />
Expand All @@ -64,7 +64,7 @@ export default function Swap(props: SwapProps) {
<SwapActionButton />
{useBrandedFooter() && <BrandedFooter />}
</SwapInfoProvider>
</BoundaryProvider>
</PopoverBoundaryProvider>
</div>
{displayTx && (
<Dialog color="dialog">
Expand Down
11 changes: 11 additions & 0 deletions src/hooks/useOnEscapeHandler.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useEffect } from 'react'

export function useOnEscapeHandler(onClose?: () => void) {
useEffect(() => {
if (!onClose) return

const close = (e: KeyboardEvent) => e.key === 'Escape' && onClose()
document.addEventListener('keydown', close, true)
return () => document.removeEventListener('keydown', close, true)
}, [onClose])
}

0 comments on commit f859214

Please sign in to comment.