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

fix: settings ux (#3282)

* fix: max slippage warning logic

* fix: option border specificity

* fix: dialog resizing through animation

* fix: initial warning states

* fix: hide Modal class
parent f47fcc9c
...@@ -73,13 +73,12 @@ export const Modal = styled.div<{ color: Color }>` ...@@ -73,13 +73,12 @@ export const Modal = styled.div<{ color: Color }>`
border-radius: ${({ theme }) => theme.borderRadius * 0.75}em; border-radius: ${({ theme }) => theme.borderRadius * 0.75}em;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: calc(100% - 0.5em); height: 100%;
left: 0; left: 0;
margin: 0.25em;
overflow: hidden; overflow: hidden;
position: absolute; position: absolute;
top: 0; top: 0;
width: calc(100% - 0.5em); width: 100%;
z-index: ${Layer.DIALOG}; z-index: ${Layer.DIALOG};
` `
......
...@@ -3,6 +3,7 @@ import { Percent } from '@uniswap/sdk-core' ...@@ -3,6 +3,7 @@ import { Percent } from '@uniswap/sdk-core'
import { useAtom } from 'jotai' import { useAtom } from 'jotai'
import Popover from 'lib/components/Popover' import Popover from 'lib/components/Popover'
import { TooltipHandlers, useTooltip } from 'lib/components/Tooltip' import { TooltipHandlers, useTooltip } from 'lib/components/Tooltip'
import { toPercent } from 'lib/hooks/useAllowedSlippage'
import { AlertTriangle, Check, Icon, LargeIcon, XOctagon } from 'lib/icons' import { AlertTriangle, Check, Icon, LargeIcon, XOctagon } from 'lib/icons'
import { autoSlippageAtom, MAX_VALID_SLIPPAGE, maxSlippageAtom, MIN_HIGH_SLIPPAGE } from 'lib/state/settings' import { autoSlippageAtom, MAX_VALID_SLIPPAGE, maxSlippageAtom, MIN_HIGH_SLIPPAGE } from 'lib/state/settings'
import styled, { Color, ThemedText } from 'lib/theme' import styled, { Color, ThemedText } from 'lib/theme'
...@@ -58,9 +59,17 @@ function Option({ ...@@ -58,9 +59,17 @@ function Option({
} }
enum WarningState { enum WarningState {
NONE, INVALID_SLIPPAGE = 1,
HIGH_SLIPPAGE, HIGH_SLIPPAGE,
INVALID_SLIPPAGE, }
function toWarningState(percent: Percent | undefined): WarningState | undefined {
if (percent?.greaterThan(MAX_VALID_SLIPPAGE)) {
return WarningState.INVALID_SLIPPAGE
} else if (percent?.greaterThan(MIN_HIGH_SLIPPAGE)) {
return WarningState.HIGH_SLIPPAGE
}
return
} }
const Warning = memo(function Warning({ state, showTooltip }: { state: WarningState; showTooltip: boolean }) { const Warning = memo(function Warning({ state, showTooltip }: { state: WarningState; showTooltip: boolean }) {
...@@ -80,8 +89,6 @@ const Warning = memo(function Warning({ state, showTooltip }: { state: WarningSt ...@@ -80,8 +89,6 @@ const Warning = memo(function Warning({ state, showTooltip }: { state: WarningSt
color = 'warning' color = 'warning'
content = highSlippage content = highSlippage
break break
case WarningState.NONE:
return null
} }
return ( return (
<Popover <Popover
...@@ -104,41 +111,24 @@ export default function MaxSlippageSelect() { ...@@ -104,41 +111,24 @@ export default function MaxSlippageSelect() {
const [autoSlippage, setAutoSlippage] = useAtom(autoSlippageAtom) const [autoSlippage, setAutoSlippage] = useAtom(autoSlippageAtom)
const [maxSlippage, setMaxSlippage] = useAtom(maxSlippageAtom) const [maxSlippage, setMaxSlippage] = useAtom(maxSlippageAtom)
const maxSlippageInput = useMemo(() => maxSlippage?.toString() || '', [maxSlippage]) const maxSlippageInput = useMemo(() => maxSlippage?.toString() || '', [maxSlippage])
const [warning, setWarning] = useState(WarningState.NONE) const [warning, setWarning] = useState<WarningState | undefined>(toWarningState(toPercent(maxSlippage)))
const [showTooltip, setShowTooltip, tooltipProps] = useTooltip() const [showTooltip, setShowTooltip, tooltipProps] = useTooltip(/*showOnMount=*/ true)
useEffect(() => setShowTooltip(true), [warning, setShowTooltip]) // enables the tooltip when a warning is set
const processInput = useCallback( const processValue = useCallback(
(input: number | undefined) => { (value: number | undefined) => {
const numerator = input && Math.floor(input * 100) const percent = toPercent(value)
if (numerator) { const warning = toWarningState(percent)
const percent = new Percent(numerator, 10_000) setWarning(warning)
if (percent.greaterThan(MAX_VALID_SLIPPAGE)) { setMaxSlippage(value)
setWarning(WarningState.INVALID_SLIPPAGE) setAutoSlippage(!percent || warning === WarningState.INVALID_SLIPPAGE)
setAutoSlippage(true)
setMaxSlippage(input)
} else if (percent.greaterThan(MIN_HIGH_SLIPPAGE)) {
setWarning(WarningState.HIGH_SLIPPAGE)
setAutoSlippage(false)
setMaxSlippage(input)
} else {
setWarning(WarningState.NONE)
setAutoSlippage(false)
setMaxSlippage(input)
}
} else {
setAutoSlippage(true)
setMaxSlippage(undefined)
}
}, },
[setAutoSlippage, setMaxSlippage] [setAutoSlippage, setMaxSlippage]
) )
const onInputSelect = useCallback(() => { const onInputSelect = useCallback(() => {
focus() focus()
processInput(maxSlippage) processValue(maxSlippage)
}, [focus, maxSlippage, processInput]) }, [focus, maxSlippage, processValue])
useEffect(() => processInput(maxSlippage), [maxSlippage, processInput]) // processes any warnings on mount
useEffect(() => setShowTooltip(true), [warning, setShowTooltip]) // enables the tooltip if a warning is set
return ( return (
<Column gap={0.75}> <Column gap={0.75}>
...@@ -153,14 +143,14 @@ export default function MaxSlippageSelect() { ...@@ -153,14 +143,14 @@ export default function MaxSlippageSelect() {
wrapper={Custom} wrapper={Custom}
selected={!autoSlippage} selected={!autoSlippage}
onSelect={onInputSelect} onSelect={onInputSelect}
icon={<Warning state={warning} showTooltip={showTooltip} />} icon={warning && <Warning state={warning} showTooltip={showTooltip} />}
{...tooltipProps} {...tooltipProps}
> >
<Row color={warning === WarningState.INVALID_SLIPPAGE ? 'error' : undefined}> <Row color={warning === WarningState.INVALID_SLIPPAGE ? 'error' : undefined}>
<DecimalInput <DecimalInput
size={Math.max(maxSlippageInput.length, 3)} size={Math.max(maxSlippageInput.length, 3)}
value={maxSlippageInput} value={maxSlippageInput}
onChange={(input) => processInput(+input)} onChange={(input) => processValue(+input)}
placeholder={placeholder} placeholder={placeholder}
ref={input} ref={input}
/> />
......
...@@ -14,6 +14,10 @@ export const optionCss = (selected: boolean) => css` ...@@ -14,6 +14,10 @@ export const optionCss = (selected: boolean) => css`
grid-gap: 0.25em; grid-gap: 0.25em;
padding: calc(0.75em - 1px) 0.625em; padding: calc(0.75em - 1px) 0.625em;
:enabled {
border: 1px solid ${({ theme }) => (selected ? theme.active : theme.outline)};
}
:enabled:hover { :enabled:hover {
border-color: ${({ theme }) => theme.onHover(selected ? theme.active : theme.outline)}; border-color: ${({ theme }) => theme.onHover(selected ? theme.active : theme.outline)};
} }
......
...@@ -13,8 +13,8 @@ export interface TooltipHandlers { ...@@ -13,8 +13,8 @@ export interface TooltipHandlers {
onBlur: () => void onBlur: () => void
} }
export function useTooltip(): [boolean, (show: boolean) => void, TooltipHandlers] { export function useTooltip(showOnMount = false): [boolean, (show: boolean) => void, TooltipHandlers] {
const [show, setShow] = useState(false) const [show, setShow] = useState(showOnMount)
const enable = useCallback(() => setShow(true), []) const enable = useCallback(() => setShow(true), [])
const disable = useCallback(() => setShow(false), []) const disable = useCallback(() => setShow(false), [])
return [show, setShow, { onMouseEnter: enable, onMouseLeave: disable, onFocus: enable, onBlur: disable }] return [show, setShow, { onMouseEnter: enable, onMouseLeave: disable, onFocus: enable, onBlur: disable }]
......
...@@ -15,19 +15,6 @@ import ErrorBoundary, { ErrorHandler } from './Error/ErrorBoundary' ...@@ -15,19 +15,6 @@ import ErrorBoundary, { ErrorHandler } from './Error/ErrorBoundary'
import WidgetPropValidator from './Error/WidgetsPropsValidator' import WidgetPropValidator from './Error/WidgetsPropsValidator'
import Web3Provider from './Web3Provider' import Web3Provider from './Web3Provider'
const slideDown = keyframes`
to {
height: 0;
top: calc(100% - 0.25em);
}
`
const slideUp = keyframes`
from {
height: 0;
top: calc(100% - 0.25em);
}
`
const WidgetWrapper = styled.div<{ width?: number | string }>` const WidgetWrapper = styled.div<{ width?: number | string }>`
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
...@@ -56,6 +43,31 @@ const WidgetWrapper = styled.div<{ width?: number | string }>` ...@@ -56,6 +43,31 @@ const WidgetWrapper = styled.div<{ width?: number | string }>`
font-family: ${({ theme }) => theme.fontFamilyVariable}; font-family: ${({ theme }) => theme.fontFamilyVariable};
} }
} }
`
const slideDown = keyframes`
to {
transform: translateY(calc(100% - 0.25em));
}
`
const slideUp = keyframes`
from {
transform: translateY(calc(100% - 0.25em));
}
`
const DialogWrapper = styled.div`
height: calc(100% - 0.5em);
left: 0;
margin: 0.25em;
overflow: hidden;
position: absolute;
top: 0;
width: calc(100% - 0.5em);
@supports (overflow: clip) {
overflow: clip;
}
${Modal} { ${Modal} {
animation: ${slideUp} 0.25s ease-in-out; animation: ${slideUp} 0.25s ease-in-out;
...@@ -95,18 +107,19 @@ export default function Widget(props: PropsWithChildren<WidgetProps>) { ...@@ -95,18 +107,19 @@ export default function Widget(props: PropsWithChildren<WidgetProps>) {
provider, provider,
jsonRpcEndpoint, jsonRpcEndpoint,
width = 360, width = 360,
dialog, dialog: userDialog,
className, className,
onError, onError,
} = props } = props
const [wrapper, setWrapper] = useState<HTMLDivElement | null>(null) const [dialog, setDialog] = useState<HTMLDivElement | null>(null)
return ( return (
<StrictMode> <StrictMode>
<I18nProvider locale={locale}> <I18nProvider locale={locale}>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<WidgetWrapper width={width} className={className} ref={setWrapper}> <WidgetWrapper width={width} className={className}>
<DialogProvider value={dialog || wrapper}> <DialogWrapper ref={setDialog} />
<DialogProvider value={userDialog || dialog}>
<ErrorBoundary onError={onError}> <ErrorBoundary onError={onError}>
<WidgetPropValidator {...props}> <WidgetPropValidator {...props}>
<ReduxProvider store={multicallStore}> <ReduxProvider store={multicallStore}>
......
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