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:
Zach Pomerantz 2022-03-03 11:09:12 -08:00 committed by GitHub
parent a4fbfae4ba
commit 542bf0bf66
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 41 additions and 23 deletions

@ -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'};
` `