-
Notifications
You must be signed in to change notification settings - Fork 303
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Rostislav / WALL-361 / Cashier Fiat Transfer amount input field (#8442)
* refactor: init pr * feat: transfer amount input field core logic * refactor: core logic changed a bit * refactor: basic styles * refactor: basic styles (the right one) * refactor: tests * refactor: backspacing test * refactor: fireEvent -> userEvent * refactor: add the export in index.js * refactor: currency issue fixed * refactor: tests fix * Update packages/components/src/components/transfer-amount-input/__tests__/transfer-amount-input.test.tsx Co-authored-by: George Usynin <103181646+heorhi-deriv@users.noreply.github.com> * refactor: TransferAmountInput -> AmountInput * refactor: onChange prop added to AmountInput * Update packages/components/src/components/amount-input/amount-input.tsx Co-authored-by: Farzin Mirzaie <72082844+farzin-deriv@users.noreply.github.com> * refactor: suggestions * refactor: suggestions * Update packages/components/src/components/amount-input/amount-input.scss Co-authored-by: Farzin Mirzaie <72082844+farzin-deriv@users.noreply.github.com> * refactor: props naming * Update packages/components/src/components/amount-input/amount-input.tsx Co-authored-by: Farzin Mirzaie <72082844+farzin-deriv@users.noreply.github.com> * fix: import added * refactor: removed redundant Input prop for max_characters * refactor: useCallback * fix: tests and logic as well * refactor: using isMobile() for responsive design for AmountInput * refactor: toLocaleString locale changed to `undefined` * refactor: test * Update packages/components/src/components/amount-input/amount-input.tsx Co-authored-by: Farzin Mirzaie <72082844+farzin-deriv@users.noreply.github.com> * feat: amount-input-for-testing.tsx * fix: amount-input-for-testing.tsx * fix: amount-input-for-testing.tsx * Update packages/components/src/components/amount-input/amount-input.tsx Co-authored-by: Farzin Mirzaie <72082844+farzin-deriv@users.noreply.github.com> * fix: tests fix by rollback * refactor: cursor/caret issue resolved * refactor: pasting behavior resolved * refactor: separators on paste should remain now * fix: fixed the behaviour except ctrl + a; added comments * refactor: isPasting -> is_pasting * fix: all behaviour seems to be as expected now * test: added more tests * refactor: input type changed to number * refactor: minor tests changes; also inputMode added to force keyboard on mobiles to be numeric * refactor: toggles * refactor: cursor/caret fix using state * fix: wring pasting behaviour fixed * refactor: naming change for clarity * refactor: added a handler and comments * fix: tests and a logical flaw * refactor: added deps to useEffect * refactor: ChatGPT's solution to the type thing * refactor: newValue -> new_value as @heorhi-deriv suggested * refactor: remove the demo component for QA --------- Co-authored-by: George Usynin <103181646+heorhi-deriv@users.noreply.github.com> Co-authored-by: Farzin Mirzaie <72082844+farzin-deriv@users.noreply.github.com>
- Loading branch information
1 parent
5bd2789
commit 35bc18e
Showing
6 changed files
with
268 additions
and
5 deletions.
There are no files selected for viewing
84 changes: 84 additions & 0 deletions
84
packages/components/src/components/amount-input/__tests__/amount-input.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
import React from 'react'; | ||
import { render, screen } from '@testing-library/react'; | ||
import userEvent from '@testing-library/user-event'; | ||
import AmountInput from '../amount-input'; | ||
|
||
describe('<AmountInput/>', () => { | ||
it('should render with the initial value of "0.00" if {initial_value} is not specified', () => { | ||
render(<AmountInput currency='USD' />); | ||
const input = screen.getByTestId('dt_amount-input'); | ||
expect(input).toHaveDisplayValue('0.00'); | ||
}); | ||
|
||
it('should render with the correct initial value if {initial_value} was supplied', () => { | ||
render(<AmountInput currency='USD' initial_value={42} />); | ||
const input = screen.getByTestId('dt_amount-input'); | ||
expect(input).toHaveDisplayValue('42.00'); | ||
}); | ||
|
||
it('should not change the value on non-numeric and non-"." inputs', () => { | ||
render(<AmountInput currency='USD' />); | ||
const input = screen.getByTestId('dt_amount-input'); | ||
userEvent.type(input, 'abcdef!@#$%^&*()_+-={}[];\'"|\\/,.<>'); | ||
expect(input).toHaveDisplayValue('0.00'); | ||
}); | ||
|
||
it('should change the value like an ATM, i.e. from right to left, when entering digits', () => { | ||
render(<AmountInput currency='USD' />); | ||
const input = screen.getByTestId('dt_amount-input'); | ||
userEvent.type(input, '1'); | ||
expect(input).toHaveDisplayValue('0.01'); | ||
userEvent.type(input, '2'); | ||
expect(input).toHaveDisplayValue('0.12'); | ||
userEvent.type(input, '3'); | ||
expect(input).toHaveDisplayValue('1.23'); | ||
}); | ||
|
||
it('should add commas for big values', () => { | ||
render(<AmountInput currency='USD' max_digits={9} />); | ||
const input = screen.getByTestId('dt_amount-input'); | ||
userEvent.type(input, '123456789'); | ||
expect(input).toHaveDisplayValue('1,234,567.89'); | ||
}); | ||
|
||
it('should not remove "0.00" when backspacing', () => { | ||
render(<AmountInput currency='USD' />); | ||
const input = screen.getByTestId('dt_amount-input'); | ||
userEvent.type(input, '100'); | ||
expect(input).toHaveDisplayValue('1.00'); | ||
userEvent.clear(input); | ||
expect(input).toHaveDisplayValue('0.00'); | ||
}); | ||
|
||
it('should not accept more than {maxDigits} digits', () => { | ||
render(<AmountInput currency='USD' max_digits={9} />); | ||
const input = screen.getByTestId('dt_amount-input'); | ||
userEvent.type(input, '1234567890987654321'); | ||
expect(input).toHaveDisplayValue('1,234,567.89'); | ||
}); | ||
|
||
it('should work correctly with explicitly set {decimal_points}', () => { | ||
render(<AmountInput currency='USD' decimal_places={5} />); | ||
const input = screen.getByTestId('dt_amount-input'); | ||
expect(input).toHaveDisplayValue('0.00000'); | ||
userEvent.type(input, '12345678'); | ||
expect(input).toHaveDisplayValue('123.45678'); | ||
}); | ||
|
||
it('should allow pasting numbers and then interpret those correctly', () => { | ||
render(<AmountInput currency='USD' />); | ||
const input = screen.getByTestId('dt_amount-input'); | ||
const values = [ | ||
['123', '123.00'], | ||
['123.42', '123.42'], | ||
['123,42', '123.42'], | ||
['123,000.00', '123,000.00'], | ||
]; | ||
values.forEach(pair => { | ||
userEvent.clear(input); | ||
userEvent.click(input); | ||
userEvent.paste(input, pair[0]); | ||
expect(input).toHaveDisplayValue(pair[1]); | ||
}); | ||
}); | ||
}); |
30 changes: 30 additions & 0 deletions
30
packages/components/src/components/amount-input/amount-input.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
.amount-input-wrapper { | ||
display: flex; | ||
flex-direction: column; | ||
padding: 0.8rem; | ||
height: 6.6rem; | ||
} | ||
|
||
.amount-input-container { | ||
position: relative; | ||
flex: 1; | ||
} | ||
|
||
.amount-input { | ||
position: absolute; | ||
border: none; | ||
flex: 1; | ||
margin: 0; | ||
padding: 0; | ||
height: 100%; | ||
width: 100%; | ||
|
||
input { | ||
font-size: var(--text-size-sm); | ||
font-weight: var(--text-weight-bold); | ||
|
||
@include mobile { | ||
font-size: var(--text-size-s); | ||
} | ||
} | ||
} |
137 changes: 137 additions & 0 deletions
137
packages/components/src/components/amount-input/amount-input.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
import React, { useCallback, useEffect, useState } from 'react'; | ||
import { isMobile } from '@deriv/shared'; | ||
import Input from '../input'; | ||
import Text from '../text'; | ||
|
||
type TAmountInput = { | ||
currency: string; | ||
decimal_places?: number; | ||
disabled?: boolean; | ||
initial_value?: number; | ||
label?: string; | ||
locale?: Intl.LocalesArgument; | ||
max_digits?: number; | ||
onChange?: (value: number) => void; | ||
}; | ||
|
||
const AmountInput = ({ | ||
currency, | ||
decimal_places = 2, | ||
disabled = false, | ||
initial_value = 0, | ||
label, | ||
locale, | ||
max_digits = 8, | ||
onChange, | ||
}: TAmountInput) => { | ||
const [value, setValue] = useState(initial_value); | ||
const [focus, setFocus] = useState(false); | ||
const [is_pasting, setIsPasting] = useState(false); | ||
const [caret_right_offset, setCaretRightOffset] = useState(0); | ||
const [selection, setSelection] = useState<{ | ||
selectionStart: number; | ||
selectionEnd: number; | ||
}>({ selectionStart: 0, selectionEnd: 0 }); | ||
const [target, setTarget] = useState<React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>['target']>(); | ||
|
||
const displayNumber = useCallback( | ||
(number: number) => number.toLocaleString(locale, { minimumFractionDigits: decimal_places }), | ||
[decimal_places, locale] | ||
); | ||
|
||
useEffect(() => { | ||
// update caret position every time the value changes (this happens after onChange) | ||
const updated_caret_position = displayNumber(value).length - caret_right_offset; | ||
target?.setSelectionRange(updated_caret_position, updated_caret_position); | ||
setSelection({ selectionStart: updated_caret_position, selectionEnd: updated_caret_position }); | ||
}, [value, target, displayNumber]); | ||
|
||
const onChangeHandler: React.ComponentProps<typeof Input>['onChange'] = e => { | ||
if (!target) setTarget(e.target); | ||
let new_value = value; | ||
if (!is_pasting) { | ||
// handle ATM typing: | ||
// remove all characters that are not digit / point / comma: | ||
const input_value = e.target.value.replace(/[^\d.,]/g, ''); | ||
if (input_value.replace(/[.,]/g, '').replace(/^0+/g, '').length <= max_digits) | ||
new_value = Number(input_value.replace(/[.,]/g, '')) / Math.pow(10, decimal_places); | ||
} else { | ||
// handle pasting: | ||
const selection_length = selection.selectionEnd - selection.selectionStart; | ||
const pasted_string_length = e.target.value.length + selection_length - displayNumber(value).length; | ||
const pasted_string = e.target.value.substring( | ||
selection.selectionStart, | ||
selection.selectionStart + pasted_string_length | ||
); | ||
// remove all characters that are not digit / point / comma: | ||
const input_value = e.target.value.replace(/[^\d.,]/g, ''); | ||
// understand the value user wants to paste: | ||
const pasted_value = pasted_string | ||
.replace(/[^\d.,]/g, '') // remove all characters that are not digit / point / comma | ||
.replace(/[,.](?=.*[,.])/g, '') // leave only the last point / comma | ||
.replace(',', '.'); // make the last point / comma separator a point | ||
if (pasted_value.replace('.', '')) { | ||
// if the value is a valid non-empty string, handle the two scenarios: | ||
if ((value === 0 && caret_right_offset === 0) || selection_length === displayNumber(value).length) { | ||
// handle pasting when there's nothing entered before it, or it is overridden (intention: reset value): | ||
new_value = Number( | ||
pasted_value.substring( | ||
0, | ||
pasted_value.includes('.') ? max_digits + 1 : max_digits - decimal_places | ||
) | ||
); | ||
} else if (input_value.replace(/[.,]/g, '').replace(/^0+/g, '').length <= max_digits) { | ||
// handle pasting when there's something entered before it and there's space for the pasted value (intention: add to value): | ||
new_value = Number(input_value.replace(/[.,]/g, '')) / Math.pow(10, decimal_places); | ||
} | ||
} | ||
} | ||
setValue(new_value); | ||
setIsPasting(false); | ||
onChange?.(new_value); | ||
}; | ||
|
||
const inputActionHandler: React.ComponentProps<typeof Input>['onMouseDown'] & | ||
React.ComponentProps<typeof Input>['onMouseUp'] & | ||
React.ComponentProps<typeof Input>['onKeyDown'] = e => { | ||
if (e.currentTarget.selectionStart !== null && e.currentTarget.selectionEnd !== null) { | ||
setCaretRightOffset(e.currentTarget.value.length - e.currentTarget.selectionEnd); | ||
setSelection({ | ||
selectionStart: e.currentTarget.selectionStart, | ||
selectionEnd: e.currentTarget.selectionEnd, | ||
}); | ||
} | ||
}; | ||
|
||
return ( | ||
<div className='amount-input-wrapper'> | ||
<Text size={isMobile() ? 'xxs' : 'xs'}>{label}</Text> | ||
<div className='amount-input-container'> | ||
<Input | ||
className='amount-input' | ||
disabled={disabled || focus} | ||
type='text' | ||
inputMode='numeric' | ||
value={`${displayNumber(value)} ${currency}`} | ||
/> | ||
<Input | ||
className='amount-input' | ||
data-testid='dt_amount-input' | ||
disabled={disabled} | ||
onFocus={() => setFocus(true)} | ||
onBlur={() => setFocus(false)} | ||
onChange={onChangeHandler} | ||
onKeyDown={inputActionHandler} | ||
onMouseDown={inputActionHandler} | ||
onMouseUp={inputActionHandler} | ||
onPaste={() => setIsPasting(true)} | ||
type='text' | ||
inputMode='numeric' | ||
value={displayNumber(value)} | ||
/> | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
export default AmountInput; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
import AmountInput from './amount-input'; | ||
import './amount-input.scss'; | ||
|
||
export default AmountInput; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters