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/.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(
() => ({
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.