Skip to content

Commit

Permalink
feat: input number (#516)
Browse files Browse the repository at this point in the history
* feat: input number

* new implementation

* wip: InputNumber with keyboard

* feat: field number

* feat: add tests and story

* fix: big step and add test

* fix: code review feedback on function to const

---------

Co-authored-by: Ivan Dalmet <ivan@dalmet.fr>
  • Loading branch information
yoannfleurydev and ivan-dalmet authored Aug 5, 2024
1 parent eaad030 commit 4745ab6
Show file tree
Hide file tree
Showing 10 changed files with 518 additions and 255 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ test('update value', async () => {
<FormField>
<FormFieldLabel>Balance</FormFieldLabel>
<FormFieldController
type="currency"
type="number"
control={form.control}
name="balance"
currency="EUR"
/>
</FormField>
)}
Expand All @@ -49,9 +50,10 @@ test('update value in cents', async () => {
<FormField>
<FormFieldLabel>Balance</FormFieldLabel>
<FormFieldController
type="currency"
type="number"
control={form.control}
name="balance"
currency="EUR"
inCents
/>
</FormField>
Expand Down Expand Up @@ -79,17 +81,18 @@ test('update value locale fr', async () => {
<FormField>
<FormFieldLabel>Balance</FormFieldLabel>
<FormFieldController
type="currency"
type="number"
control={form.control}
name="balance"
currency="EUR"
locale="fr"
/>
</FormField>
)}
</FormMocked>
);
const input = screen.getByLabelText<HTMLInputElement>('Balance');
await user.type(input, '12.00');
await user.type(input, '12,00');
expect(input.value).toBe('12,00 €');
await user.click(screen.getByRole('button', { name: 'Submit' }));
expect(mockedSubmit).toHaveBeenCalledWith({ balance: 12 });
Expand All @@ -109,10 +112,11 @@ test('update value no decimals', async () => {
<FormField>
<FormFieldLabel>Balance</FormFieldLabel>
<FormFieldController
type="currency"
type="number"
control={form.control}
name="balance"
decimals={0}
currency="EUR"
precision={0}
/>
</FormField>
)}
Expand Down Expand Up @@ -140,9 +144,11 @@ test('default value', async () => {
<FormField>
<FormFieldLabel>Balance</FormFieldLabel>
<FormFieldController
type="currency"
type="number"
control={form.control}
name="balance"
fixedPrecision
currency="EUR"
/>
</FormField>
)}
Expand All @@ -168,17 +174,56 @@ test('disabled', async () => {
<FormField>
<FormFieldLabel>Balance</FormFieldLabel>
<FormFieldController
type="currency"
type="number"
control={form.control}
name="balance"
currency="EUR"
isDisabled
/>
</FormField>
)}
</FormMocked>
);
const input = screen.getByLabelText<HTMLInputElement>('Balance');
await user.type(input, '10.00');
await user.type(input, '42.00');
expect(input.value).toBe('€12');
await user.click(screen.getByRole('button', { name: 'Submit' }));
expect(mockedSubmit).toHaveBeenCalledWith({ balance: 12 });
});

test('update value using keyboard step and big step', async () => {
const user = setupUser();
const mockedSubmit = vi.fn();

render(
<FormMocked
schema={z.object({ balance: z.number() })}
useFormOptions={{ defaultValues: { balance: 12 } }}
onSubmit={mockedSubmit}
>
{({ form }) => (
<FormField>
<FormFieldLabel>Balance</FormFieldLabel>
<FormFieldController
type="number"
control={form.control}
name="balance"
currency="EUR"
step={1}
bigStep={10}
/>
</FormField>
)}
</FormMocked>
);
const input = screen.getByLabelText<HTMLInputElement>('Balance');
await user.click(input);
await user.keyboard('[ArrowUp][ArrowUp]');
expect(input.value).toBe('€14');
await user.click(input);

await user.keyboard('{Shift>}[ArrowUp][ArrowUp]{/Shift}');

await user.click(screen.getByRole('button', { name: 'Submit' }));
expect(mockedSubmit).toHaveBeenCalledWith({ balance: 34 });
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Icon } from '@/components/Icons';
import { Form, FormField, FormFieldController, FormFieldLabel } from '../';

export default {
title: 'Form/FieldCurrency',
title: 'Form/FieldNumber',
};

type FormSchema = z.infer<ReturnType<typeof zFormSchema>>;
Expand All @@ -32,7 +32,35 @@ export const Default = () => {
<FormField>
<FormFieldLabel>Balance</FormFieldLabel>
<FormFieldController
type="currency"
type="number"
control={form.control}
name="balance"
placeholder={12}
/>
</FormField>
<Box>
<Button type="submit" variant="@primary">
Submit
</Button>
</Box>
</Stack>
</Form>
);
};

export const DefaultValue = () => {
const form = useForm<FormSchema>({
...formOptions,
defaultValues: { balance: 12 },
});

return (
<Form {...form} onSubmit={(values) => console.log(values)}>
<Stack spacing={4}>
<FormField>
<FormFieldLabel>Balance</FormFieldLabel>
<FormFieldController
type="number"
control={form.control}
name="balance"
placeholder={12}
Expand All @@ -57,7 +85,7 @@ export const InCents = () => {
<FormField>
<FormFieldLabel>Balance</FormFieldLabel>
<FormFieldController
type="currency"
type="number"
control={form.control}
name="balance"
inCents
Expand All @@ -83,7 +111,7 @@ export const LocaleFr = () => {
<FormField>
<FormFieldLabel>Balance</FormFieldLabel>
<FormFieldController
type="currency"
type="number"
control={form.control}
name="balance"
locale="fr"
Expand All @@ -109,10 +137,10 @@ export const LocaleNoDecimals = () => {
<FormField>
<FormFieldLabel>Balance</FormFieldLabel>
<FormFieldController
type="currency"
type="number"
control={form.control}
name="balance"
decimals={0}
precision={0}
placeholder={12}
/>
</FormField>
Expand All @@ -135,7 +163,7 @@ export const Disabled = () => {
<FormField>
<FormFieldLabel>Balance</FormFieldLabel>
<FormFieldController
type="currency"
type="number"
control={form.control}
name="balance"
placeholder={12}
Expand All @@ -161,7 +189,7 @@ export const StartElement = () => {
<FormField>
<FormFieldLabel>Balance</FormFieldLabel>
<FormFieldController
type="currency"
type="number"
control={form.control}
name="balance"
placeholder={12}
Expand All @@ -187,11 +215,11 @@ export const ChakraProps = () => {
<FormField>
<FormFieldLabel>Balance</FormFieldLabel>
<FormFieldController
type="currency"
type="number"
control={form.control}
name="balance"
placeholder={12}
inputCurrencyProps={{
inputNumberProps={{
color: 'rebeccapurple',
}}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,51 +16,56 @@ import {

import { FieldCommonProps } from '@/components/Form/FormFieldController';
import { FormFieldError } from '@/components/Form/FormFieldError';
import { InputCurrency, InputCurrencyProps } from '@/components/InputCurrency';
import { InputNumber, InputNumberProps } from '@/components/InputNumber';

type InputCurrencyRootProps = Pick<
InputCurrencyProps,
type InputNumberRootProps = Pick<
InputNumberProps,
| 'placeholder'
| 'size'
| 'autoFocus'
| 'locale'
| 'currency'
| 'decimals'
| 'fixedDecimals'
| 'precision'
| 'fixedPrecision'
| 'prefix'
| 'suffix'
| 'min'
| 'max'
| 'step'
| 'bigStep'
>;

export type FieldCurrencyProps<
export type FieldNumberProps<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
type: 'currency';
type: 'number';
inCents?: boolean;
startElement?: ReactNode;
endElement?: ReactNode;
inputCurrencyProps?: RemoveFromType<
RemoveFromType<InputCurrencyProps, InputCurrencyRootProps>,
inputNumberProps?: RemoveFromType<
RemoveFromType<InputNumberProps, InputNumberRootProps>,
ControllerRenderProps
>;
containerProps?: FlexProps;
} & InputCurrencyRootProps &
} & InputNumberRootProps &
FieldCommonProps<TFieldValues, TName>;

export const FieldCurrency = <
export const FieldNumber = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>(
props: FieldCurrencyProps<TFieldValues, TName>
props: FieldNumberProps<TFieldValues, TName>
) => {
const formatValue = (
value: number | undefined | null,
type: 'to-cents' | 'from-cents'
) => {
if (value === undefined || value === null) return value;
if (props.inCents !== true) return value;
if (value === undefined || value === null) return null;
if (props.inCents !== true) return value ?? null;
if (type === 'to-cents') return Math.round(value * 100);
if (type === 'from-cents') return value / 100;
return null;
};

return (
Expand All @@ -69,9 +74,7 @@ export const FieldCurrency = <
render={({ field }) => (
<Flex flexDirection="column" gap={1} flex={1} {...props.containerProps}>
<InputGroup size={props.size}>
<InputCurrency
type={props.type}
size={props.size}
<InputNumber
placeholder={
(typeof props.placeholder === 'number'
? formatValue(props.placeholder, 'from-cents')
Expand All @@ -84,10 +87,14 @@ export const FieldCurrency = <
suffix={props.suffix}
locale={props.locale}
currency={props.currency}
decimals={props.decimals}
fixedDecimals={props.fixedDecimals}
precision={props.precision}
fixedPrecision={props.fixedPrecision}
min={props.min}
max={props.max}
step={props.step}
bigStep={props.bigStep}
isDisabled={props.isDisabled}
{...props.inputCurrencyProps}
{...props.inputNumberProps}
{...field}
value={formatValue(field.value, 'from-cents')}
onChange={(v) => field.onChange(formatValue(v, 'to-cents'))}
Expand Down
2 changes: 1 addition & 1 deletion src/components/Form/FieldText/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export type FieldTextProps<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
type: 'text' | 'email' | 'number' | 'tel';
type: 'text' | 'email' | 'tel';
startElement?: ReactNode;
endElement?: ReactNode;
inputProps?: RemoveFromType<
Expand Down
9 changes: 4 additions & 5 deletions src/components/Form/FormFieldController.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import { useFormField } from '@/components/Form/FormField';

import { FieldCheckbox, FieldCheckboxProps } from './FieldCheckbox';
import { FieldCheckboxes, FieldCheckboxesProps } from './FieldCheckboxes';
import { FieldCurrency, FieldCurrencyProps } from './FieldCurrency';
import { FieldDate, FieldDateProps } from './FieldDate';
import { FieldMultiSelect, FieldMultiSelectProps } from './FieldMultiSelect';
import { FieldNumber, FieldNumberProps } from './FieldNumber';
import { FieldOtp, FieldOtpProps } from './FieldOtp';
import { FieldPassword, FieldPasswordProps } from './FieldPassword';
import { FieldRadios, FieldRadiosProps } from './FieldRadios';
Expand Down Expand Up @@ -55,7 +55,7 @@ export type FormFieldControllerProps<
| FieldMultiSelectProps<TFieldValues, TName>
| FieldOtpProps<TFieldValues, TName>
| FieldDateProps<TFieldValues, TName>
| FieldCurrencyProps<TFieldValues, TName>
| FieldNumberProps<TFieldValues, TName>
| FieldPasswordProps<TFieldValues, TName>
| FieldCheckboxesProps<TFieldValues, TName>
| FieldRadiosProps<TFieldValues, TName>;
Expand All @@ -80,15 +80,14 @@ export const FormFieldController = <

case 'text':
case 'email':
case 'number':
case 'tel':
return <FieldText {...props} />;

case 'password':
return <FieldPassword {...props} />;

case 'currency':
return <FieldCurrency {...props} />;
case 'number':
return <FieldNumber {...props} />;

case 'textarea':
return <FieldTextarea {...props} />;
Expand Down
Loading

0 comments on commit 4745ab6

Please sign in to comment.