feat: user select (#3410)
* feat: make data user-selectable * fix: consider the whole node for focus * fix: back out lineheight typing * fix: straggling occurences * chore: comment on root user-select
This commit is contained in:
parent
a4fbfae4ba
commit
542bf0bf66
@ -118,7 +118,7 @@ export default function ErrorDialog({ header, error, action, onClick }: ErrorDia
|
|||||||
<Rule />
|
<Rule />
|
||||||
<ErrorColumn>
|
<ErrorColumn>
|
||||||
<Column gap={0.5} ref={setDetails} css={scrollbar}>
|
<Column gap={0.5} ref={setDetails} css={scrollbar}>
|
||||||
<ThemedText.Code>
|
<ThemedText.Code userSelect>
|
||||||
{error.name}
|
{error.name}
|
||||||
{error.message ? `: ${error.message}` : ''}
|
{error.message ? `: ${error.message}` : ''}
|
||||||
</ThemedText.Code>
|
</ThemedText.Code>
|
||||||
|
@ -22,7 +22,7 @@ import Row from '../Row'
|
|||||||
import TokenImg from '../TokenImg'
|
import TokenImg from '../TokenImg'
|
||||||
import TokenInput from './TokenInput'
|
import TokenInput from './TokenInput'
|
||||||
|
|
||||||
export const LoadingRow = styled(Row)<{ $loading: boolean }>`
|
export const USDC = styled(Row)`
|
||||||
${loadingOpacityCss};
|
${loadingOpacityCss};
|
||||||
`
|
`
|
||||||
|
|
||||||
@ -120,12 +120,12 @@ export default function Input({ disabled, focused }: InputProps) {
|
|||||||
onChangeCurrency={updateSwapInputCurrency}
|
onChangeCurrency={updateSwapInputCurrency}
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
>
|
>
|
||||||
<ThemedText.Body2 color="secondary">
|
<ThemedText.Body2 color="secondary" userSelect>
|
||||||
<Row>
|
<Row>
|
||||||
<LoadingRow $loading={isLoading}>{inputUSDC ? `$${inputUSDC.toFixed(2)}` : '-'}</LoadingRow>
|
<USDC $loading={isLoading}>{inputUSDC ? `$${inputUSDC.toFixed(2)}` : '-'}</USDC>
|
||||||
{balance && (
|
{balance && (
|
||||||
<Balance color={balanceColor} focused={focused}>
|
<Balance color={balanceColor} focused={focused}>
|
||||||
Balance: <span style={{ userSelect: 'text' }}>{formatCurrencyAmount(balance, 4, i18n.locale)}</span>
|
Balance: <span>{formatCurrencyAmount(balance, 4, i18n.locale)}</span>
|
||||||
</Balance>
|
</Balance>
|
||||||
)}
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
|
@ -16,7 +16,7 @@ import { getPriceImpactWarning } from 'utils/prices'
|
|||||||
|
|
||||||
import Column from '../Column'
|
import Column from '../Column'
|
||||||
import Row from '../Row'
|
import Row from '../Row'
|
||||||
import { Balance, InputProps, LoadingRow, useFormattedFieldAmount } from './Input'
|
import { Balance, InputProps, USDC, useFormattedFieldAmount } from './Input'
|
||||||
import TokenInput from './TokenInput'
|
import TokenInput from './TokenInput'
|
||||||
|
|
||||||
export const colorAtom = atom<string | undefined>(undefined)
|
export const colorAtom = atom<string | undefined>(undefined)
|
||||||
@ -100,14 +100,14 @@ export default function Output({ disabled, focused, children }: PropsWithChildre
|
|||||||
onChangeCurrency={updateSwapOutputCurrency}
|
onChangeCurrency={updateSwapOutputCurrency}
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
>
|
>
|
||||||
<ThemedText.Body2 color="secondary">
|
<ThemedText.Body2 color="secondary" userSelect>
|
||||||
<Row>
|
<Row>
|
||||||
<LoadingRow gap={0.5} $loading={isLoading}>
|
<USDC gap={0.5} $loading={isLoading}>
|
||||||
{outputUSDC ? `$${outputUSDC.toFixed(2)}` : '-'} {priceImpact}
|
{outputUSDC ? `$${outputUSDC.toFixed(2)}` : '-'} {priceImpact}
|
||||||
</LoadingRow>
|
</USDC>
|
||||||
{balance && (
|
{balance && (
|
||||||
<Balance focused={focused}>
|
<Balance focused={focused}>
|
||||||
Balance: <span style={{ userSelect: 'text' }}>{formatCurrencyAmount(balance, 4, i18n.locale)}</span>
|
Balance: <span>{formatCurrencyAmount(balance, 4, i18n.locale)}</span>
|
||||||
</Balance>
|
</Balance>
|
||||||
)}
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
|
@ -26,7 +26,7 @@ interface DetailProps {
|
|||||||
|
|
||||||
function Detail({ label, value, color }: DetailProps) {
|
function Detail({ label, value, color }: DetailProps) {
|
||||||
return (
|
return (
|
||||||
<ThemedText.Caption>
|
<ThemedText.Caption userSelect>
|
||||||
<Row gap={2}>
|
<Row gap={2}>
|
||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
<Value color={color}>{value}</Value>
|
<Value color={color}>{value}</Value>
|
||||||
|
@ -38,13 +38,13 @@ function TokenValue({ input, usdc, change }: TokenValueProps) {
|
|||||||
<Column justify="flex-start">
|
<Column justify="flex-start">
|
||||||
<Row gap={0.375} justify="flex-start">
|
<Row gap={0.375} justify="flex-start">
|
||||||
<TokenImg token={input.currency} />
|
<TokenImg token={input.currency} />
|
||||||
<ThemedText.Body2>
|
<ThemedText.Body2 userSelect>
|
||||||
{formatCurrencyAmount(input, 6, i18n.locale)} {input.currency.symbol}
|
{formatCurrencyAmount(input, 6, i18n.locale)} {input.currency.symbol}
|
||||||
</ThemedText.Body2>
|
</ThemedText.Body2>
|
||||||
</Row>
|
</Row>
|
||||||
{usdc && usdcAmount && (
|
{usdc && usdcAmount && (
|
||||||
<Row justify="flex-start">
|
<Row justify="flex-start">
|
||||||
<ThemedText.Caption color="secondary">
|
<ThemedText.Caption color="secondary" userSelect>
|
||||||
${formatCurrencyAmount(usdcAmount, 2, i18n.locale)}
|
${formatCurrencyAmount(usdcAmount, 2, i18n.locale)}
|
||||||
{change && <Percent gain={change > 0}> {percent}</Percent>}
|
{change && <Percent gain={change > 0}> {percent}</Percent>}
|
||||||
</ThemedText.Caption>
|
</ThemedText.Caption>
|
||||||
|
@ -135,10 +135,12 @@ export function SummaryDialog({ trade, allowedSlippage, onConfirm }: SummaryDial
|
|||||||
<Body flex align="stretch" gap={0.75} padded open={open}>
|
<Body flex align="stretch" gap={0.75} padded open={open}>
|
||||||
<SummaryColumn gap={0.75} flex justify="center">
|
<SummaryColumn gap={0.75} flex justify="center">
|
||||||
<Summary input={inputAmount} output={outputAmount} usdc={true} />
|
<Summary input={inputAmount} output={outputAmount} usdc={true} />
|
||||||
<ThemedText.Caption>
|
<Row>
|
||||||
{formatLocaleNumber({ number: 1, sigFigs: 1, locale: i18n.locale })} {inputCurrency.symbol} ={' '}
|
<ThemedText.Caption userSelect>
|
||||||
{formatPrice(executionPrice, 6, i18n.locale)} {outputCurrency.symbol}
|
{formatLocaleNumber({ number: 1, sigFigs: 1, locale: i18n.locale })} {inputCurrency.symbol} ={' '}
|
||||||
</ThemedText.Caption>
|
{formatPrice(executionPrice, 6, i18n.locale)} {outputCurrency.symbol}
|
||||||
|
</ThemedText.Caption>
|
||||||
|
</Row>
|
||||||
</SummaryColumn>
|
</SummaryColumn>
|
||||||
<Rule />
|
<Rule />
|
||||||
<Row>
|
<Row>
|
||||||
|
@ -81,10 +81,12 @@ export function Trade({ trade }: { trade: InterfaceTrade<Currency, Currency, Tra
|
|||||||
: null
|
: null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row gap={0.25} style={{ userSelect: 'text' }}>
|
<ThemedText.Caption userSelect>
|
||||||
{ratio}
|
<Row gap={0.25}>
|
||||||
{usdc && <ThemedText.Caption color="secondary">{usdc}</ThemedText.Caption>}
|
{ratio}
|
||||||
</Row>
|
{usdc && <ThemedText.Caption color="secondary">{usdc}</ThemedText.Caption>}
|
||||||
|
</Row>
|
||||||
|
</ThemedText.Caption>
|
||||||
)
|
)
|
||||||
}, [executionPrice, fiatValueInput, fiatValueOutput, flip, inputAmount, outputAmount])
|
}, [executionPrice, fiatValueInput, fiatValueOutput, flip, inputAmount, outputAmount])
|
||||||
|
|
||||||
|
@ -1,6 +1,14 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
|
||||||
export default function useHasFocus(node: Node | null | undefined): boolean {
|
export default function useHasFocus(node: Node | null | undefined): boolean {
|
||||||
|
useEffect(() => {
|
||||||
|
if (node instanceof HTMLElement) {
|
||||||
|
// tabIndex is required to receive blur events from non-button elements.
|
||||||
|
node.tabIndex = node.tabIndex || -1
|
||||||
|
// Without explicitly omitting outline, Safari will now outline this node when focused.
|
||||||
|
node.style.outline = node.style.outline || 'none'
|
||||||
|
}
|
||||||
|
}, [node])
|
||||||
const [hasFocus, setHasFocus] = useState(node?.contains(document?.activeElement) ?? false)
|
const [hasFocus, setHasFocus] = useState(node?.contains(document?.activeElement) ?? false)
|
||||||
const onFocus = useCallback(() => setHasFocus(true), [])
|
const onFocus = useCallback(() => setHasFocus(true), [])
|
||||||
const onBlur = useCallback((e) => setHasFocus(node?.contains(e.relatedTarget) ?? false), [node])
|
const onBlur = useCallback((e) => setHasFocus(node?.contains(e.relatedTarget) ?? false), [node])
|
||||||
|
@ -3,12 +3,18 @@ import { Text, TextProps as TextPropsWithCss } from 'rebass'
|
|||||||
import styled, { useTheme } from './styled'
|
import styled, { useTheme } from './styled'
|
||||||
import { Color } from './theme'
|
import { Color } from './theme'
|
||||||
|
|
||||||
type TextProps = Omit<TextPropsWithCss, 'css' | 'color'> & { color?: Color }
|
type TextProps = Omit<TextPropsWithCss, 'css' | 'color' | 'userSelect'> & {
|
||||||
|
color?: Color
|
||||||
|
userSelect?: true
|
||||||
|
}
|
||||||
|
|
||||||
const TextWrapper = styled(Text)<{ color?: Color; lineHeight: string; noWrap?: true }>`
|
const TextWrapper = styled(Text)<{ color?: Color; lineHeight: string; noWrap?: true; userSelect?: true }>`
|
||||||
color: ${({ color = 'currentColor', theme }) => theme[color as Color]};
|
color: ${({ color = 'currentColor', theme }) => theme[color as Color]};
|
||||||
// Avoid the need for placeholders by setting min-height to line-height.
|
// Avoid the need for placeholders by setting min-height to line-height.
|
||||||
min-height: ${({ lineHeight }) => lineHeight};
|
min-height: ${({ lineHeight }) => lineHeight};
|
||||||
|
// user-select is set to 'none' at the root element (Widget), but is desired for displayed data.
|
||||||
|
// user-select must be configured through styled-components for cross-browser compat (eg to auto-generate prefixed properties).
|
||||||
|
user-select: ${({ userSelect }) => userSelect && 'text'};
|
||||||
white-space: ${({ noWrap }) => noWrap && 'nowrap'};
|
white-space: ${({ noWrap }) => noWrap && 'nowrap'};
|
||||||
`
|
`
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user