Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
c2f7192
fix: numeric text field would round 0.000 to 0
0xAlunara Oct 15, 2025
060420f
fix: avoid calling onBalance when input changes from 0.00 to 0.0
0xAlunara Oct 16, 2025
a9f60d3
refactor: useUniqueDebounce options made more general, add docs
0xAlunara Oct 16, 2025
0405aed
refactor: remove unused import
0xAlunara Oct 16, 2025
bf482e4
test: rename ClientWrapper to ComponentTestWrapper and make config op…
0xAlunara Oct 16, 2025
04a29de
refactor: return debounce cancel function
0xAlunara Oct 16, 2025
56af7e5
fix: temporary values getting erased by a debounce
0xAlunara Oct 16, 2025
bac9ffd
fix: also handle a sole - as input numeric text field input
0xAlunara Oct 16, 2025
197ed9c
fix: always allow negative numbers to be put in, only clamp on blur
0xAlunara Oct 16, 2025
966fbc1
fix: handle additional sole input characters like - and ,
0xAlunara Oct 16, 2025
2868a79
test: update and fix all numeric input field tests
0xAlunara Oct 16, 2025
59d3329
test: remove useless initial value test portion
0xAlunara Oct 16, 2025
60e33f2
chore: improve numeric text field tests
0xAlunara Oct 16, 2025
0bb8f0d
test: simplify rounding test
0xAlunara Oct 17, 2025
0e661c7
fix: cancel is used as a dependency
0xAlunara Oct 17, 2025
a57bcf8
refactor: move equals function to global scope because of re-renders
0xAlunara Oct 17, 2025
2f7977b
refactor: double negation
0xAlunara Oct 17, 2025
decee41
fix: extend decimal function to include the domain of big numbers
0xAlunara Oct 17, 2025
e32fcbe
refactor: use decimal function
0xAlunara Oct 17, 2025
99794e5
refactor: decimal function returns original string if valid
0xAlunara Oct 17, 2025
48bead6
Merge branch 'main' into fix/lti-decimals
DanielSchiavini Oct 20, 2025
a09301a
Merge branch 'main' into fix/lti-decimals
DanielSchiavini Oct 21, 2025
31a4e25
Merge branch 'main' of github.com:curvefi/curve-frontend into fix/lti…
DanielSchiavini Oct 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ export const RangeSliderFilter = <T,>({
return [min ?? defaultMinimum, max ?? maxValue]
}, [columnFilters, id, maxValue, defaultMinimum])

const [range, setRange] = useUniqueDebounce(
const [range, setRange] = useUniqueDebounce({
defaultValue,
useCallback(
callback: useCallback(
(newRange: NumberRange) =>
setColumnFilter(
id,
Expand All @@ -61,7 +61,7 @@ export const RangeSliderFilter = <T,>({
),
[defaultMinimum, defaultValue, id, maxValue, setColumnFilter],
),
)
})

const onChange = useCallback<OnSliderChange>((_, newRange) => setRange(newRange as NumberRange), [setRange])

Expand Down
33 changes: 24 additions & 9 deletions packages/curve-ui-kit/src/hooks/useDebounce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,26 @@ export function useDebouncedValue<T>(
const SearchDebounceMs = 166 // 10 frames at 60fps

/**
* A hook that debounces a search value and calls a callback when the debounce period has elapsed.
* A hook that debounces a value and only calls the callback when the value has actually changed.
* This prevents unnecessary callback executions when the debounced value hasn't changed.
*
* @param defaultValue - The initial value to use
* @param callback - Function called when the debounced value changes
* @param debounceMs - The debounce period in milliseconds (default: 166ms)
* @param equals - Optional custom equality function to compare values
* @returns A tuple containing the current value and a setter function
*/
export function useUniqueDebounce<T>(defaultValue: T, callback: (value: T) => void, debounceMs = SearchDebounceMs) {
export function useUniqueDebounce<T>({
defaultValue,
callback,
debounceMs = SearchDebounceMs,
equals,
}: {
defaultValue: T
callback: (value: T) => void
debounceMs?: number
equals?: (a: T, b: T) => boolean
}) {
const lastValue = useRef(defaultValue)

/**
Expand All @@ -113,16 +130,14 @@ export function useUniqueDebounce<T>(defaultValue: T, callback: (value: T) => vo

const debounceCallback = useCallback(
(value: T) => {
if (typeof value === 'string') {
value = value.trim() as unknown as T
}
if (value !== lastValue.current) {
const isEqual = equals ? equals(value, lastValue.current) : value === lastValue.current
if (!isEqual) {
lastValue.current = value
callback(value)
}
},
[callback],
[callback, equals],
)
const [search, setSearch] = useDebounce(defaultValue, debounceMs, debounceCallback)
return [search, setSearch] as const
const [value, setValue] = useDebounce(defaultValue, debounceMs, debounceCallback)
return [value, setValue] as const
}
10 changes: 8 additions & 2 deletions packages/curve-ui-kit/src/shared/ui/LargeTokenInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Box from '@mui/material/Box'
import Chip from '@mui/material/Chip'
import Stack from '@mui/material/Stack'
import Typography from '@mui/material/Typography'
import { useDebounce } from '@ui-kit/hooks/useDebounce'
import { useUniqueDebounce } from '@ui-kit/hooks/useDebounce'
import { t } from '@ui-kit/lib/i18n'
import { Duration, TransitionFunction } from '@ui-kit/themes/design/0_primitives'
import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces'
Expand Down Expand Up @@ -261,7 +261,13 @@ export const LargeTokenInput = ({
testId,
}: Props) => {
const [percentage, setPercentage] = useState<Decimal | undefined>(undefined)
const [balance, setBalance] = useDebounce(externalBalance, Duration.FormDebounce, onBalance)
const [balance, setBalance] = useUniqueDebounce({
defaultValue: externalBalance,
callback: onBalance,
debounceMs: Duration.FormDebounce,
// We don't want to trigger onBalance if the value is effectively the same, e.g. "0.0" and "0.00"
equals: (a, b) => new BigNumber(a ?? 0).isEqualTo(b ?? 0),
})

const showSlider = !!maxBalance?.showSlider
const showWalletBalance = maxBalance && maxBalance.showBalance !== false
Expand Down
8 changes: 8 additions & 0 deletions packages/curve-ui-kit/src/shared/ui/NumericTextField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,14 @@ export const NumericTextField = ({ value, min, max, onChange, onBlur, onFocus, .
const parseAndClamp = (validatedValue: string, { shouldClamp = false }: { shouldClamp?: boolean }) => {
if (validatedValue === '') return undefined
const result = shouldClamp ? clamp(validatedValue, min, max) : new BigNumber(validatedValue)

// Preserve original formatting for zero values with trailing zeros (e.g., "0.00", "0.000")
// This prevents the input from jumping to "0" when the user is still typing
const decimalPart = validatedValue.split('.')[1]
if (!shouldClamp && result.isEqualTo(0) && decimalPart && /^0+$/.test(decimalPart)) {
return validatedValue as Decimal
}

return result.toString() as Decimal
}

Expand Down
6 changes: 5 additions & 1 deletion packages/curve-ui-kit/src/shared/ui/SearchField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ export const SearchField = ({
inputRef,
...props
}: SearchFieldProps) => {
const [search, setSearch] = useUniqueDebounce<string>(value, onSearch)
const [search, setSearch] = useUniqueDebounce<string>({
defaultValue: value,
callback: onSearch,
equals: (a, b) => a.trim() === b.trim(),
})
const localInputRef = useRef<HTMLInputElement | null>(null)
const ref = inputRef || localInputRef
const resetSearch = useCallback(() => {
Expand Down
32 changes: 32 additions & 0 deletions tests/cypress/component/numeric-text-field.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,36 @@ describe('NumericTextField', () => {
cy.get('input').click().type('.5').blur()
cy.get('[data-testid="state-value"]').should('contain', '0.5')
})

it('changing the amount of zeros in the decimal should not cause rounding to just 0', () => {
cy.mount(<TestComponent />)

// Start with empty input
cy.get('input').click().clear()

// Type "0.0001" character by character
cy.get('input').type('0')
cy.get('[data-testid="state-temp-value"]').should('contain', '0')

cy.get('input').type('.')
cy.get('[data-testid="state-temp-value"]').should('contain', '0')

cy.get('input').type('0')
cy.get('[data-testid="state-temp-value"]').should('contain', '0.0')

cy.get('input').type('0')
cy.get('[data-testid="state-temp-value"]').should('contain', '0.00')

cy.get('input').type('0')
cy.get('[data-testid="state-temp-value"]').should('contain', '0.000')

cy.get('input').type('1')
cy.get('[data-testid="state-temp-value"]').should('contain', '0.0001')

cy.get('input').type('{backspace}')
cy.get('[data-testid="state-temp-value"]').should('contain', '0.000')

cy.get('input').type('2')
cy.get('[data-testid="state-temp-value"]').should('contain', '0.0002')
})
})