From aa417ca0242d79927684f078a0c333761baae9e0 Mon Sep 17 00:00:00 2001 From: Jon Ator Date: Wed, 3 Jul 2024 15:48:54 -0400 Subject: [PATCH] add auto scale input, and match figma --- .../bridge/immersive/amount-screen.tsx | 168 ++----------- .../bridge/immersive/crypto-fiat-input.tsx | 234 ++++++++++++++++++ packages/web/components/input/input-box.tsx | 30 ++- 3 files changed, 270 insertions(+), 162 deletions(-) create mode 100644 packages/web/components/bridge/immersive/crypto-fiat-input.tsx diff --git a/packages/web/components/bridge/immersive/amount-screen.tsx b/packages/web/components/bridge/immersive/amount-screen.tsx index 046f036358..789187d640 100644 --- a/packages/web/components/bridge/immersive/amount-screen.tsx +++ b/packages/web/components/bridge/immersive/amount-screen.tsx @@ -7,11 +7,10 @@ import { MenuItem, MenuItems, } from "@headlessui/react"; -import { CoinPretty, Dec, DecUtils, PricePretty } from "@keplr-wallet/unit"; +import { Dec } from "@keplr-wallet/unit"; import { BridgeChain } from "@osmosis-labs/bridge"; -import { DEFAULT_VS_CURRENCY } from "@osmosis-labs/server"; import { BridgeTransactionDirection } from "@osmosis-labs/types"; -import { isNil, isNumeric, noop } from "@osmosis-labs/utils"; +import { isNil, noop } from "@osmosis-labs/utils"; import classNames from "classnames"; import { observer } from "mobx-react-lite"; import Image from "next/image"; @@ -34,7 +33,6 @@ import { SupportedAsset, useBridgesSupportedAssets, } from "~/components/bridge/immersive/use-bridges-supported-assets"; -import { InputBox } from "~/components/input"; import { SkeletonLoader, Spinner } from "~/components/loaders"; import { useScreenManager } from "~/components/screen-manager"; import { Tooltip } from "~/components/tooltip"; @@ -48,9 +46,9 @@ import { import { useEvmWalletAccount } from "~/hooks/evm-wallet"; import { usePrice } from "~/hooks/queries/assets/use-price"; import { useStore } from "~/stores"; -import { trimPlaceholderZeros } from "~/utils/number"; import { api } from "~/utils/trpc"; +import { CryptoFiatInput } from "./crypto-fiat-input"; import { BridgeProviderDropdownRow, EstimatedTimeRow, @@ -536,65 +534,6 @@ export const AmountScreen = observer( return ; } - const cryptoAmountPretty = new CoinPretty( - { - coinDecimals: sourceAsset.decimals, - coinDenom: sourceAsset.denom, - coinMinimalDenom: sourceAsset.address, - }, - cryptoAmount === "" - ? new Dec(0) - : new Dec(cryptoAmount).mul( - DecUtils.getTenExponentN(sourceAsset.decimals) - ) - ); - - const fiatAmountPretty = new PricePretty( - DEFAULT_VS_CURRENCY, - new Dec(fiatAmount === "" ? 0 : fiatAmount) - ); - - const parseFiatAmount = (value: string) => { - return value.replace("$", ""); - }; - - const formatFiatAmount = (value: string) => { - return `$${value}`; - }; - - const onInput = (type: "fiat" | "crypto") => (value: string) => { - let nextValue = type === "fiat" ? parseFiatAmount(value) : value; - if (!isNumeric(nextValue) && nextValue !== "") return; - - if (nextValue.startsWith("0") && !nextValue.startsWith("0.")) { - nextValue = nextValue.slice(1); - } - if (nextValue === "") { - nextValue = "0"; - } - if (nextValue === ".") { - nextValue = "0."; - } - - if (type === "fiat") { - // Update the crypto amount based on the fiat amount - const priceInFiat = assetInOsmosisPrice.toDec(); - const nextFiatAmount = new Dec(nextValue); - const nextCryptoAmount = nextFiatAmount.quo(priceInFiat).toString(); - - setCryptoAmount(trimPlaceholderZeros(nextCryptoAmount)); - } else { - // Update the fiat amount based on the crypto amount - const priceInFiat = assetInOsmosisPrice.toDec(); - const nextCryptoAmount = new Dec(nextValue); - const nextFiatAmount = nextCryptoAmount.mul(priceInFiat).toString(); - - setFiatAmount(trimPlaceholderZeros(nextFiatAmount)); - } - - type === "fiat" ? setFiatAmount(nextValue) : setCryptoAmount(nextValue); - }; - const resetAssets = () => { setSourceAsset(undefined); setDestinationAsset(undefined); @@ -671,89 +610,18 @@ export const AmountScreen = observer(
-
-
-
- {inputUnit === "fiat" ? ( - - ) : ( -
-

- {cryptoAmountPretty?.denom} -

- -
- )} -
- -
- - - -
-
-
+ <> {isLoadingAssetsBalance && ( @@ -1273,11 +1141,11 @@ const TransferDetails: FunctionComponent<{ {({ open }) => (
diff --git a/packages/web/components/bridge/immersive/crypto-fiat-input.tsx b/packages/web/components/bridge/immersive/crypto-fiat-input.tsx new file mode 100644 index 0000000000..821b8f456b --- /dev/null +++ b/packages/web/components/bridge/immersive/crypto-fiat-input.tsx @@ -0,0 +1,234 @@ +import { CoinPretty, Dec, DecUtils, PricePretty } from "@keplr-wallet/unit"; +import { isNumeric } from "@osmosis-labs/utils"; +import classNames from "classnames"; +import { FunctionComponent, useEffect, useRef, useState } from "react"; + +import { Icon } from "~/components/assets"; +import { InputBox } from "~/components/input"; +import { useTranslation } from "~/hooks"; +import { trimPlaceholderZeros } from "~/utils/number"; + +import { SupportedAssetWithAmount } from "./amount-and-review-screen"; + +export const CryptoFiatInput: FunctionComponent<{ + currentUnit: "fiat" | "crypto"; + cryptoInputRaw: string; + fiatInputRaw: string; + assetPrice: PricePretty; + asset: SupportedAssetWithAmount; + isInsufficientBal: boolean; + isInsufficientFee: boolean; + setFiatAmount: (amount: string) => void; + setCryptoAmount: (amount: string) => void; + setInputUnit: (unit: "fiat" | "crypto") => void; +}> = ({ + currentUnit, + cryptoInputRaw, + fiatInputRaw, + assetPrice, + asset, + isInsufficientBal, + isInsufficientFee, + setFiatAmount, + setCryptoAmount, + setInputUnit, +}) => { + const { t } = useTranslation(); + const cryptoInputRef = useRef(null); + const fiatInputRef = useRef(null); + + const cryptoAmountPretty = new CoinPretty( + { + coinDecimals: asset.decimals, + coinDenom: asset.denom, + coinMinimalDenom: asset.address, + }, + cryptoInputRaw === "" + ? new Dec(0) + : new Dec(cryptoInputRaw).mul(DecUtils.getTenExponentN(asset.decimals)) + ); + + const fiatAmountPretty = new PricePretty( + assetPrice.fiatCurrency, + new Dec(fiatInputRaw === "" ? 0 : fiatInputRaw) + ); + + const onInput = (type: "fiat" | "crypto") => (value: string) => { + let nextValue = type === "fiat" ? value.replace("$", "") : value; + if (!isNumeric(nextValue) && nextValue !== "") return; + + if (nextValue.startsWith("0") && !nextValue.startsWith("0.")) { + nextValue = nextValue.slice(1); + } + + if (nextValue === "") { + nextValue = "0"; + } + if (nextValue === ".") { + nextValue = "0."; + } + + if (type === "fiat") { + // Update the crypto amount based on the fiat amount + const priceInFiat = assetPrice.toDec(); + const nextFiatAmount = new Dec(nextValue); + const nextCryptoAmount = nextFiatAmount.quo(priceInFiat).toString(); + + setCryptoAmount(trimPlaceholderZeros(nextCryptoAmount)); + } else { + // Update the fiat amount based on the crypto amount + const priceInFiat = assetPrice.toDec(); + const nextCryptoAmount = new Dec(nextValue); + const nextFiatAmount = nextCryptoAmount.mul(priceInFiat).toString(); + + setFiatAmount(trimPlaceholderZeros(nextFiatAmount)); + } + + type === "fiat" ? setFiatAmount(nextValue) : setCryptoAmount(nextValue); + }; + + const fiatCurrentValue = `${assetPrice?.symbol ?? ""}${fiatInputRaw}`; + const fiatInputFontSize = calcTextSizeClass(fiatCurrentValue.length); + const cryptoInputFontSize = calcTextSizeClass( + cryptoInputRaw.length + cryptoAmountPretty.denom.length + ); + const [isInputFocused, setIsInputFocused] = useState(false); + + // when the key to the autosize input changes, it will re-mount + // causing the input to lose focus, + // so we use an effect to re-focus the input if it's already focused + useEffect(() => { + if (isInputFocused && currentUnit === "fiat" && fiatInputRef.current) { + fiatInputRef.current.focus(); + } + if (isInputFocused && currentUnit === "crypto" && cryptoInputRef.current) { + cryptoInputRef.current.focus(); + } + }, [isInputFocused, currentUnit, fiatInputFontSize, cryptoInputFontSize]); + + return ( +
+
+
+
{ + if (currentUnit === "fiat") { + fiatInputRef.current?.focus(); + } else { + cryptoInputRef.current?.focus(); + } + }} + > + {currentUnit === "fiat" ? ( + setIsInputFocused(false)} + onFocus={() => setIsInputFocused(true)} + currentValue={fiatCurrentValue} + onInput={onInput("fiat")} + isAutosize + /> + ) : ( + setIsInputFocused(false)} + onFocus={() => setIsInputFocused(true)} + currentValue={cryptoInputRaw} + onInput={onInput("crypto")} + trailingSymbol={cryptoAmountPretty.denom} + isAutosize + /> + )} +
+ +
+ + +
+ ); +}; + +function calcTextSizeClass(numChars: number): string { + if (numChars <= 8) { + return "text-4xl"; + } else if (numChars <= 10) { + return "text-3xl"; + } else if (numChars <= 12) { + return "text-2xl"; + } else if (numChars <= 18) { + return "text-xl"; + } else if (numChars <= 24) { + return "text-lg"; + } + + return "text-md"; +} diff --git a/packages/web/components/input/input-box.tsx b/packages/web/components/input/input-box.tsx index 5d44ecd419..80c7510e16 100644 --- a/packages/web/components/input/input-box.tsx +++ b/packages/web/components/input/input-box.tsx @@ -20,6 +20,7 @@ interface Props extends Optional, "currentValue">, Disableable, CustomClasses { + inputKey?: string; /** Style of the component, see Figma. */ style?: "no-border" | "enabled" | "active" | "error"; type?: HTMLInputTypeAttribute; @@ -39,6 +40,7 @@ interface Props } export const InputBox: FunctionComponent = ({ + inputKey, currentValue, onInput, onFocus, @@ -65,6 +67,18 @@ export const InputBox: FunctionComponent = ({ onChange: onInput, }); + const inputClassName_ = classNames( + "md:leading-0 w-full appearance-none bg-transparent pt-px align-middle leading-10 placeholder:text-osmoverse-500 md:p-0", + { + "text-white-disabled": disabled, + "text-white-high": currentValue != "" && !disabled, + "float-right text-right": rightEntry, + "pr-1": !trailingSymbol, + }, + classes?.input, + inputClassName + ); + return (
= ({ > {isAutosize ? ( { if (inputRef) { inputRef.current = ref; } }} - inputClassName={inputClassName} + inputClassName={inputClassName_} minWidth={0} value={inputValue} onInput={(e: any) => setValue(e.target.value)} @@ -108,19 +123,10 @@ export const InputBox: FunctionComponent = ({ /> ) : (