Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: input number #516

Merged
merged 7 commits into from
Aug 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -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
Loading