diff --git a/packages/react-components/package.json b/packages/react-components/package.json index fc400f8..2c30ee6 100644 --- a/packages/react-components/package.json +++ b/packages/react-components/package.json @@ -43,8 +43,8 @@ "@headlessui/react": "^1.7.18", "@testing-library/react": "14.0.0", "@types/node": "20.4.9", - "@types/react": "18.2.20", - "@types/react-dom": "18.2.7", + "@types/react": "18.2.63", + "@types/react-dom": "18.2.20", "@typescript-eslint/eslint-plugin": "^7.1.0", "@vitejs/plugin-react": "4.0.4", "@vitest/coverage-v8": "0.34.1", diff --git a/packages/react-components/src/lib/components/AmountInput.tsx b/packages/react-components/src/lib/components/AmountInput.tsx new file mode 100644 index 0000000..93530e3 --- /dev/null +++ b/packages/react-components/src/lib/components/AmountInput.tsx @@ -0,0 +1,41 @@ +import React, { type Ref, forwardRef } from 'react'; +import type { NatValue } from '@agoric/ertp/src/types'; +import { useAmountInput } from '../hooks/amountInput.js'; + +const noop = () => { + /* no-op */ +}; + +type Props = { + value: NatValue | null; + decimalPlaces: number; + className?: React.HtmlHTMLAttributes['className']; + onChange?: (value: NatValue) => void; + disabled?: boolean; +}; + +const RenderAmountInput = ( + { value, decimalPlaces, className, onChange = noop, disabled = false }: Props, + ref?: Ref, +) => { + const { displayString, handleInputChange } = useAmountInput({ + value, + decimalPlaces, + onChange, + }); + + return ( + + ); +}; + +export const AmountInput = forwardRef(RenderAmountInput); diff --git a/packages/react-components/src/lib/components/index.ts b/packages/react-components/src/lib/components/index.ts index 6d70ab3..5c77cfd 100644 --- a/packages/react-components/src/lib/components/index.ts +++ b/packages/react-components/src/lib/components/index.ts @@ -1,2 +1,3 @@ export * from './ConnectWalletButton'; export * from './NodeSelectorModal'; +export * from './AmountInput'; diff --git a/packages/react-components/src/lib/hooks/agoric.ts b/packages/react-components/src/lib/hooks/agoric.ts new file mode 100644 index 0000000..a39159f --- /dev/null +++ b/packages/react-components/src/lib/hooks/agoric.ts @@ -0,0 +1,4 @@ +import { useContext } from 'react'; +import { AgoricContext } from '../context'; + +export const useAgoric = () => useContext(AgoricContext); diff --git a/packages/react-components/src/lib/hooks/amountInput.ts b/packages/react-components/src/lib/hooks/amountInput.ts new file mode 100644 index 0000000..0e444b0 --- /dev/null +++ b/packages/react-components/src/lib/hooks/amountInput.ts @@ -0,0 +1,49 @@ +import type { NatValue } from '@agoric/ertp/src/types'; +import { AssetKind } from '@agoric/ertp'; +import { parseAsValue, stringifyValue } from '@agoric/web-components'; +import { useState } from 'react'; + +type Args = { + value: NatValue | null; + decimalPlaces: number; + onChange: (value: NatValue) => void; +}; + +export const useAmountInput = ({ value, decimalPlaces, onChange }: Args) => { + const amountString = stringifyValue(value, AssetKind.NAT, decimalPlaces); + + const [fieldString, setFieldString] = useState( + value === null ? '' : amountString, + ); + + const handleInputChange: React.ChangeEventHandler = ev => { + // Inputs with type "number" allow these characters which don't apply to + // Amounts, just strip them. + const str = ev.target?.value + ?.replace('-', '') + .replace('e', '') + .replace('E', ''); + setFieldString(str); + + try { + const parsed = parseAsValue(str, AssetKind.NAT, decimalPlaces); + onChange(parsed); + } catch { + console.debug('Invalid input', str); + } + }; + + // Use the `fieldString` as an input buffer so the user can type values that + // would be overwritten by `stringifyValue`. For example, if the current + // input is "1.05", and you tried to change it to "1.25", on hitting + // backspace, `stringifyValue` would change it from "1.0" to "1.00", + // preventing you from ever editing it. Only let `amountString` override + // `fieldString` if the controlled input is trying to change it to a truly + // different value. + const displayString = + value === parseAsValue(fieldString, AssetKind.NAT, decimalPlaces) + ? fieldString + : amountString; + + return { displayString, handleInputChange }; +}; diff --git a/packages/react-components/src/lib/hooks/index.ts b/packages/react-components/src/lib/hooks/index.ts index a39159f..d050a89 100644 --- a/packages/react-components/src/lib/hooks/index.ts +++ b/packages/react-components/src/lib/hooks/index.ts @@ -1,4 +1,2 @@ -import { useContext } from 'react'; -import { AgoricContext } from '../context'; - -export const useAgoric = () => useContext(AgoricContext); +export * from './agoric.js'; +export * from './amountInput.js'; diff --git a/yarn.lock b/yarn.lock index a9e8d02..ddb4390 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6586,10 +6586,10 @@ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== -"@types/react-dom@18.2.7": - version "18.2.7" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.7.tgz#67222a08c0a6ae0a0da33c3532348277c70abb63" - integrity sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA== +"@types/react-dom@18.2.20": + version "18.2.20" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.20.tgz#cbdf7abb3cc2377980bb1294bc51375016a8320f" + integrity sha512-HXN/biJY8nv20Cn9ZbCFq3liERd4CozVZmKbaiZ9KiKTrWqsP7eoGDO6OOGvJQwoVFuiXaiJ7nBBjiFFbRmQMQ== dependencies: "@types/react" "*" @@ -6609,10 +6609,10 @@ "@types/scheduler" "*" csstype "^3.0.2" -"@types/react@18.2.20": - version "18.2.20" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.20.tgz#1605557a83df5c8a2cc4eeb743b3dfc0eb6aaeb2" - integrity sha512-WKNtmsLWJM/3D5mG4U84cysVY31ivmyw85dE84fOCk5Hx78wezB/XEjVPWl2JTZ5FkEeaTJf+VgUAUn3PE7Isw== +"@types/react@18.2.63": + version "18.2.63" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.63.tgz#4637c56146ad90f96d0583171edab953f7e6fe57" + integrity sha512-ppaqODhs15PYL2nGUOaOu2RSCCB4Difu4UFrP4I3NHLloXC/ESQzQMi9nvjfT1+rudd0d2L3fQPJxRSey+rGlQ== dependencies: "@types/prop-types" "*" "@types/scheduler" "*"