Skip to content

Commit

Permalink
FEAT: Show balance & Allow to choose max amount
Browse files Browse the repository at this point in the history
  • Loading branch information
rogaldh committed Mar 28, 2024
1 parent ff8b815 commit 16ef873
Show file tree
Hide file tree
Showing 4 changed files with 205 additions and 46 deletions.
119 changes: 119 additions & 0 deletions packages/ui/src/__stories__/widgets/token-upgrade-amount.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import * as Form from "@radix-ui/react-form"
import Amount from "../../widgets/token-upgrade/amount"
import type { StoryFn, StoryObj } from "@storybook/react"
import { expect, fn, userEvent, within } from "@storybook/test"

const meta = {
title: "UI/Token Upgrade/Amount",
component: Amount,
parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
layout: "padded",
},
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
tags: ["autodocs"],
// More on argTypes: https://storybook.js.org/docs/api/argtypes
args: {
onAmountChange: fn(),
onAmountMaxChange: fn(),
},
argTypes: {
address: { control: "text" },
balance: { control: "text" },
label: { control: "text" },
name: { control: "text" },
step: { control: "number" },
symbol: { control: "text" },
},
decorators: [
(Story: StoryFn) => {
return (
<Form.Root>
<Form.Field name="amount">
<Story />
</Form.Field>
</Form.Root>
)
},
],
}

export default meta
type Story = StoryObj<typeof Amount>

export const Default: Story = {
args: {
address: undefined,
balance: undefined,
disabled: undefined,
placeholder: undefined,
step: undefined,
symbol: undefined,
},
async play({ canvasElement, step }: any) {
const ctx = within(canvasElement)

await step("should be rendered", async () => {
const el = ctx.getByRole("spinbutton", { description: "Amount" })
await expect(el).not.toBeDisabled()
})
},
}

export const Disabled: Story = {
args: {
...Default.args,
disabled: true,
},
async play({ canvasElement, step }: any) {
const ctx = within(canvasElement)

await step("should be disabled", async () => {
const el = ctx.getByRole("spinbutton", { description: "Amount" })
await expect(el).toBeDisabled()
})
},
}

export const WithAddress: Story = {
args: {
...Default.args,
address: "HoKw8CavcPjnd4QpFkvMyetz9bpQ9AqUJRjFqbjjnjqY",
},
async play({ canvasElement, step }: any) {
const ctx = within(canvasElement)

await step("should show symbol", async () => {
const el = ctx.getByText("Ho..Y")
await expect(el).toBeVisible()
})
},
}

export const WithSymbol: Story = {
args: {
...Default.args,
address: "HoKw8CavcPjnd4QpFkvMyetz9bpQ9AqUJRjFqbjjnjqY",
symbol: "TKN",
},
}

export const WithBalance: Story = {
args: {
...Default.args,
address: "HoKw8CavcPjnd4QpFkvMyetz9bpQ9AqUJRjFqbjjnjqY",
balance: "10",
},
async play({ canvasElement, step }: any) {
const ctx = within(canvasElement)

await step("should allow to change amount", async () => {
const el = ctx.getByRole("button")
await expect(el).toBeVisible()
await userEvent.click(el)
await expect(
ctx.getByRole("spinbutton", { description: "Amount" }),
).toHaveValue(10)
})
},
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export const WithTokenAddress: StoryObj<
ComponentPropsWithTestId<typeof TokenUpgrade>
> = {
args: {
tokenAddress: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
tokenAddress: "HoKw8CavcPjnd4QpFkvMyetz9bpQ9AqUJRjFqbjjnjqY",
},
async play({ canvasElement, step }: any) {
const ctx = within(canvasElement)
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/widgets/token-upgrade.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export function TokenUpgradeBase({
<Form.Field className="pb-4 pt-3.5" name="amount">
<Amount
address={tokenAddress}
balance={balance ?? "0"}
balance={balance}
disabled={!tokenAddress}
onAmountChange={onAmountChange}
placeholder={ph}
Expand Down
128 changes: 84 additions & 44 deletions packages/ui/src/widgets/token-upgrade/amount.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import React, { useMemo } from "react"
import React, { useCallback, useMemo, useState } from "react"
import * as Form from "@radix-ui/react-form"
import { cva, VariantProps } from "class-variance-authority"

// TODO: adjust right padding according the symbol present
const inputVariants = cva(
"block w-full min-w-64 rounded-md border-0 py-1.5 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6",
"block w-full rounded border-0 py-1.5 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6",
{
variants: {
variant: {
disabled: "cursor-not-allowed px-1.5",
regular: "pl-7 pr-14",
disabled: "pointer-events-none pl-10 px-1.5 rounded-md",
regular: "pl-10 pr-14 rounded-md",
active: "pl-1 pr-14 rounded-none rounded-r-md",
},
},
defaultVariants: {
Expand All @@ -21,28 +23,54 @@ interface AmountProps
extends VariantProps<typeof inputVariants>,
React.ComponentPropsWithoutRef<"input"> {
address?: string
balance: string
balance?: string
label?: string
name?: string
onAmountChange?: ({ amount }: { amount: number }) => void
onAmountChange?: (a: { amount: number }) => void
onAmountMaxChange?: (a: { amount: number }) => void
symbol?: string
}

export default function Amount({
address,
balance,
balance = "0",
disabled,
label = "Amount",
name = "amount",
onAmountChange,
onAmountMaxChange,
min = 0,
placeholder = "0.0",
placeholder = "0",
step = 1,
symbol,
...props
}: AmountProps) {
let variants = {}
const hasBalance = balance && Number(balance) > 0
const [value, setValue] = useState<string>()

const onValueChange = useCallback(
(value: string) => {
const numValue = Number(value)
const isValid = numValue >= 0
if (isValid) {
setValue(value)
onAmountChange?.({ amount: numValue })
}
},
[onAmountChange],
)

const onValueMaxChange = useCallback(
(value: number) => {
setValue(String(value))
onAmountMaxChange?.({ amount: value })
},
[onAmountMaxChange],
)

if (disabled) variants = { variant: "disabled" }
if (hasBalance) variants = { variant: "active" }

const displaySymbol = useMemo(() => {
let s = symbol
Expand All @@ -63,45 +91,57 @@ export default function Amount({
>
{label}
</label>
<div className="relative mt-2 rounded-md shadow-sm">
{disabled ? (
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<span className="text-gray-500 sm:text-sm"></span>
</div>
) : null}
<Form.Control
asChild
disabled={disabled}
type="number"
onChange={(e) => {
const value = Number(e.target.value)
const isValid = value >= 0
if (isValid) {
if (onAmountChange) onAmountChange({ amount: value })
}
}}
>
<input
aria-describedby={`${name}-label`}
className={inputVariants(variants)}
<div className="mt-2 flex rounded-md shadow-sm">
{hasBalance && (
<button
className="relative -mr-px inline-flex w-[37px] items-center gap-x-1.5 rounded-l-md px-1.5 py-0.5 text-xs font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 lg:text-sm"
onClick={() => onValueMaxChange?.(Number(balance))}
role="button"
type="button"
>
Max
</button>
)}
<div className="relative flex flex-grow items-stretch focus-within:z-10">
<Form.Control
asChild
disabled={disabled}
id={name}
max={balance}
min={min}
name={name}
placeholder={placeholder}
step={step}
type="number"
role="spinbutton"
{...props}
/>
</Form.Control>
{displaySymbol ? (
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
<span className="text-violet11 sm:text-sm">{displaySymbol}</span>
</div>
) : null}
onChange={(e) => {
onValueChange(e.target.value)
}}
>
<input
aria-describedby={`${name}-label`}
className={inputVariants(variants)}
value={value}
disabled={disabled}
id={name}
max={balance}
min={min}
name={name}
placeholder={placeholder}
step={step}
type="number"
role="spinbutton"
{...props}
/>
</Form.Control>
{displaySymbol ? (
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
<span className="text-violet11 sm:text-sm">{displaySymbol}</span>
</div>
) : null}
</div>
</div>
{Number(balance) > 0 ? (
<div
aria-label="balance"
className="truncate pt-1.5 text-xs text-gray-500 hover:text-clip"
>
Balance: {balance} {address}
</div>
) : null}
</>
)
}

0 comments on commit 16ef873

Please sign in to comment.