diff --git a/packages/ra-ui-materialui/src/input/NumberInput.spec.tsx b/packages/ra-ui-materialui/src/input/NumberInput.spec.tsx index ba8ba87ed7a..891f76550c8 100644 --- a/packages/ra-ui-materialui/src/input/NumberInput.spec.tsx +++ b/packages/ra-ui-materialui/src/input/NumberInput.spec.tsx @@ -1,11 +1,11 @@ import * as React from 'react'; import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { useFormContext, useWatch } from 'react-hook-form'; import { NumberInput } from './NumberInput'; import { AdminContext } from '../AdminContext'; import { SaveButton } from '../button'; import { SimpleForm, Toolbar } from '../form'; -import { useFormContext, useWatch } from 'react-hook-form'; describe('', () => { const defaultProps = { @@ -117,7 +117,10 @@ describe('', () => { const formContext = useFormContext(); return ( - {name}:{JSON.stringify(formContext.getFieldState(name))} + {name}: + {JSON.stringify( + formContext.getFieldState(name, formContext.formState) + )} ); }; @@ -134,7 +137,7 @@ describe('', () => { 'views:{"invalid":false,"isDirty":false,"isTouched":false}' ); }); - it('should return correct state when the field is touched', () => { + it('should return correct state when the field is dirty', () => { render( @@ -147,9 +150,26 @@ describe('', () => { 'resources.posts.fields.views' ) as HTMLInputElement; fireEvent.change(input, { target: { value: '3' } }); + screen.getByText( + 'views:{"invalid":false,"isDirty":true,"isTouched":false}' + ); + }); + it('should return correct state when the field is touched', () => { + render( + + + + + + + ); + const input = screen.getByLabelText( + 'resources.posts.fields.views' + ) as HTMLInputElement; + fireEvent.click(input); fireEvent.blur(input); screen.getByText( - 'views:{"invalid":false,"isDirty":true,"isTouched":true}' + 'views:{"invalid":false,"isDirty":false,"isTouched":true}' ); }); it('should return correct state when the field is invalid', async () => { @@ -229,7 +249,6 @@ describe('', () => { ); const input = screen.getByLabelText('resources.posts.fields.views'); fireEvent.change(input, { target: { value: '3' } }); - fireEvent.blur(input); fireEvent.click(screen.getByText('ra.action.save')); await waitFor(() => { expect(onSubmit).toHaveBeenCalledWith({ views: 3 }); @@ -252,7 +271,6 @@ describe('', () => { ); const input = screen.getByLabelText('resources.posts.fields.views'); fireEvent.change(input, { target: { value: '' } }); - fireEvent.blur(input); fireEvent.click(screen.getByText('ra.action.save')); await waitFor(() => { expect(onSubmit).toHaveBeenCalledWith({ views: null }); @@ -280,7 +298,6 @@ describe('', () => { ); const input = screen.getByLabelText('resources.posts.fields.views'); fireEvent.change(input, { target: { value: '3' } }); - fireEvent.blur(input); await waitFor(() => { expect(value).toEqual('3'); }); @@ -304,7 +321,6 @@ describe('', () => { ); const input = screen.getByLabelText('resources.posts.fields.views'); fireEvent.change(input, { target: { value: '3' } }); - fireEvent.blur(input); expect(value).toEqual('3'); fireEvent.click(screen.getByText('ra.action.save')); await waitFor(() => { @@ -391,7 +407,6 @@ describe('', () => { ); const input = screen.getByLabelText('resources.posts.fields.views'); fireEvent.change(input, { target: { value: '3' } }); - fireEvent.blur(input); fireEvent.click(screen.getByText('ra.action.save')); @@ -416,7 +431,6 @@ describe('', () => { ); const input = screen.getByLabelText('resources.posts.fields.views'); fireEvent.change(input, { target: { value: '3' } }); - fireEvent.blur(input); fireEvent.click(screen.getByText('ra.action.save')); diff --git a/packages/ra-ui-materialui/src/input/NumberInput.stories.tsx b/packages/ra-ui-materialui/src/input/NumberInput.stories.tsx index 274ce394acf..29509b8723e 100644 --- a/packages/ra-ui-materialui/src/input/NumberInput.stories.tsx +++ b/packages/ra-ui-materialui/src/input/NumberInput.stories.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { required } from 'ra-core'; -import { useWatch } from 'react-hook-form'; +import { useFormState, useWatch, useFormContext } from 'react-hook-form'; import { NumberInput } from './NumberInput'; import { AdminContext } from '../AdminContext'; @@ -270,3 +270,57 @@ export const Sx = () => ( ); + +const FormStateInspector = () => { + const { + touchedFields, + isDirty, + dirtyFields, + isValid, + errors, + } = useFormState(); + return ( +
+ form state:  + + {JSON.stringify({ + touchedFields, + isDirty, + dirtyFields, + isValid, + errors, + })} + +
+ ); +}; + +const FieldStateInspector = ({ name = 'views' }) => { + const formContext = useFormContext(); + return ( +
+ {name}: + + {JSON.stringify( + formContext.getFieldState(name, formContext.formState) + )} + +
+ ); +}; + +export const FieldState = () => ( + + + + + + + + + +); diff --git a/packages/ra-ui-materialui/src/input/NumberInput.tsx b/packages/ra-ui-materialui/src/input/NumberInput.tsx index 8a5a9bb02ad..3e6a24f980a 100644 --- a/packages/ra-ui-materialui/src/input/NumberInput.tsx +++ b/packages/ra-ui-materialui/src/input/NumberInput.tsx @@ -11,9 +11,6 @@ import { sanitizeInputRestProps } from './sanitizeInputRestProps'; /** * An Input component for a number * - * Due to limitations in React controlled components and number formatting, - * this input only updates the form value on blur. - * * @example * * @@ -29,7 +26,6 @@ export const NumberInput = ({ helperText, label, margin, - onBlur, onChange, parse, resource, @@ -58,8 +54,9 @@ export const NumberInput = ({ const inputProps = { ...overrideInputProps, step, min, max }; - // This is a controlled input that doesn't transform the user input on change. - // The user input is only turned into a number on blur. + // This is a controlled input that renders directly the string typed by the user. + // This string is converted to a number on change, and stored in the form state, + // but that number is not not displayed. // This is to allow transitory values like '1.0' that will lead to '1.02' // text typed by the user and displayed in the input, unparsed @@ -82,22 +79,8 @@ export const NumberInput = ({ ) { return; } - setValue(event.target.value); - }; - - // set the numeric value on the form on blur - const handleBlur = (...event: any[]) => { - if (onBlur) { - onBlur(...event); - } - const eventParam = event[0] as React.FocusEvent; - if ( - typeof eventParam.target === 'undefined' || - typeof eventParam.target.value === 'undefined' - ) { - return; - } - const target = eventParam.target; + const target = event.target; + setValue(target.value); const newValue = target.valueAsNumber ? parse ? parse(target.valueAsNumber) @@ -106,24 +89,15 @@ export const NumberInput = ({ ? parse(target.value) : convertStringToNumber(target.value); field.onChange(newValue); - field.onBlur(); - }; - - const handleKeyUp = (event: React.KeyboardEvent) => { - if (event.key === 'Enter') { - handleBlur(event); - } }; return ( { + const value = useWatch({ name }); + return ( +
+ {name} value in form:  + + {JSON.stringify(value)} ({typeof value}) + +
+ ); +}; + +export const Basic = () => ( + + + + + + + + +); + +export const DefaultValue = () => ( + + + + + + + + + + + + + + +); + +export const HelperText = () => ( + + + + + + + + + +); + +export const Label = () => ( + + + + + + + + + +); + +export const FullWidth = () => ( + + + + + + + + +); + +export const Margin = () => ( + + + + + + + + + +); + +export const Variant = () => ( + + + + + + + + + +); + +export const Required = () => ( + + + + + + + + + + +); + +export const Error = () => ( + + + ({ + values: {}, + errors: { + title: { + type: 'custom', + message: 'Special error message', + }, + }, + })} + > + + + + +); + +export const Sx = () => ( + + + + + + + +); + +const FormStateInspector = () => { + const { + touchedFields, + isDirty, + dirtyFields, + isValid, + errors, + } = useFormState(); + return ( +
+ form state:  + + {JSON.stringify({ + touchedFields, + isDirty, + dirtyFields, + isValid, + errors, + })} + +
+ ); +}; + +const FieldStateInspector = ({ name = 'title' }) => { + const formContext = useFormContext(); + return ( +
+ {name}: + + {JSON.stringify( + formContext.getFieldState(name, formContext.formState) + )} + +
+ ); +}; + +export const FieldState = () => ( + + + + + + + + + +);