From 3101f3744b9e288df697b4d207d74ca43f97fb7c Mon Sep 17 00:00:00 2001 From: Andrey Yamanov Date: Tue, 25 Mar 2025 11:39:38 +0100 Subject: [PATCH 1/2] feat(Form): add isDirty flag --- .changeset/famous-cars-do.md | 5 ++ src/components/form/Form/submit.test.tsx | 88 ++++++++++++++++++++++++ src/components/form/Form/use-form.tsx | 9 +++ 3 files changed, 102 insertions(+) create mode 100644 .changeset/famous-cars-do.md diff --git a/.changeset/famous-cars-do.md b/.changeset/famous-cars-do.md new file mode 100644 index 00000000..777dc6a7 --- /dev/null +++ b/.changeset/famous-cars-do.md @@ -0,0 +1,5 @@ +--- +'@cube-dev/ui-kit': patch +--- + +Add `isDirty` flag to Form instances. diff --git a/src/components/form/Form/submit.test.tsx b/src/components/form/Form/submit.test.tsx index ad5b9e5e..0c2c8f6d 100644 --- a/src/components/form/Form/submit.test.tsx +++ b/src/components/form/Form/submit.test.tsx @@ -369,4 +369,92 @@ describe('
', () => { expect(() => getByText('Field is required')).toThrow(); }); + + it('should update isTouched when an input is interacted with', async () => { + const onSubmit = jest.fn(() => Promise.resolve()); + const { getByRole, formInstance } = renderWithForm( + <> + + + + Submit + , + ); + + // Initially, isTouched should be false + expect(formInstance.isTouched).toBeFalsy(); + + // Simulate user interaction + const input = getByRole('textbox'); + await act(async () => { + await userEvents.type(input, 'hello'); + }); + + // After typing, isTouched should be true + expect(formInstance.isTouched).toBeTruthy(); + }); + + it('should update isDirty when input value differs from the initial value', async () => { + const onSubmit = jest.fn(() => Promise.resolve()); + const defaultValues = { test: 'initial' }; + const { getByRole, formInstance } = renderWithForm( + <> + + Submit + , + { formProps: { onSubmit, defaultValues } }, + ); + + // Initially, isDirty should be false because the value is same as initial + expect(formInstance.isDirty).toBeFalsy(); + + // Change the input value + const input = getByRole('textbox'); + await act(async () => { + await userEvents.clear(input); + await userEvents.type(input, 'changed'); + }); + + // After change, isDirty should be true + expect(formInstance.isDirty).toBeTruthy(); + }); + + it('should maintain isTouched true but set isDirty false when input value reverts to initial', async () => { + const onSubmit = jest.fn(() => Promise.resolve()); + const initialValue = { test: 'initial' }; + const { getByRole, formInstance } = renderWithForm( + <> + + + + Submit + , + { formProps: { onSubmit, defaultValues: initialValue } }, + ); + + // Initially, both isTouched and isDirty should be false + expect(formInstance.isTouched).toBeFalsy(); + expect(formInstance.isDirty).toBeFalsy(); + + // Change the input to a different value + const input = getByRole('textbox'); + await act(async () => { + await userEvents.clear(input); + await userEvents.type(input, 'changed'); + }); + + // After change, both isTouched and isDirty should be true + expect(formInstance.isTouched).toBeTruthy(); + expect(formInstance.isDirty).toBeTruthy(); + + // Revert the input back to its initial value + await act(async () => { + await userEvents.clear(input); + await userEvents.type(input, 'initial'); + }); + + // After reverting, isTouched remains true, but isDirty should be false + expect(formInstance.isTouched).toBeTruthy(); + expect(formInstance.isDirty).toBeFalsy(); + }); }); diff --git a/src/components/form/Form/use-form.tsx b/src/components/form/Form/use-form.tsx index 102046f7..9e0dfe50 100644 --- a/src/components/form/Form/use-form.tsx +++ b/src/components/form/Form/use-form.tsx @@ -341,6 +341,15 @@ export class CubeFormInstance< return Object.values(this.fields).some((field) => field?.touched); } + get isDirty(): boolean { + return Object.values(this.fields).some((field) => { + return field && field.name + ? JSON.stringify(field?.value) !== + JSON.stringify(this.defaultValues[field?.name]) + : false; + }); + } + /** * True if all fields are verified and valid * IMPORTANT: This is not the same as `!isInvalid`, because it also checks if all fields are verified. From 750fed2559045a50e57ce7f380d948ae0f3cd9b8 Mon Sep 17 00:00:00 2001 From: Andrey Yamanov Date: Tue, 25 Mar 2025 11:41:58 +0100 Subject: [PATCH 2/2] fix(Checkbox): do not extract inputStyles from props --- .changeset/twenty-terms-explain.md | 5 +++++ src/components/fields/Checkbox/Checkbox.tsx | 9 ++++----- 2 files changed, 9 insertions(+), 5 deletions(-) create mode 100644 .changeset/twenty-terms-explain.md diff --git a/.changeset/twenty-terms-explain.md b/.changeset/twenty-terms-explain.md new file mode 100644 index 00000000..eead81db --- /dev/null +++ b/.changeset/twenty-terms-explain.md @@ -0,0 +1,5 @@ +--- +'@cube-dev/ui-kit': patch +--- + +Do not extract inputStyles from props in Checkbox. diff --git a/src/components/fields/Checkbox/Checkbox.tsx b/src/components/fields/Checkbox/Checkbox.tsx index 0dbc32ec..26ac5e75 100644 --- a/src/components/fields/Checkbox/Checkbox.tsx +++ b/src/components/fields/Checkbox/Checkbox.tsx @@ -11,11 +11,11 @@ import { useToggleState } from 'react-stately'; import { useProviderProps } from '../../../provider'; import { BaseProps, - BLOCK_STYLES, + CONTAINER_STYLES, + ContainerStyleProps, Element, extractStyles, filterBaseProps, - OUTER_STYLES, Styles, tasty, } from '../../../tasty'; @@ -43,6 +43,7 @@ import type { FocusableRef } from '@react-types/shared'; export interface CubeCheckboxProps extends BaseProps, + ContainerStyleProps, AriaCheckboxProps, FieldBaseProps { inputStyles?: Styles; @@ -159,9 +160,7 @@ function Checkbox( ...otherProps } = props; - let styles: Styles = extractStyles(props, OUTER_STYLES); - - inputStyles = extractStyles(props, BLOCK_STYLES, inputStyles); + let styles: Styles = extractStyles(props, CONTAINER_STYLES); labelStyles = useMemo( () => ({