diff --git a/packages/core/components/AutoField/index.spec.tsx b/packages/core/components/AutoField/index.spec.tsx new file mode 100644 index 0000000000..9c7427f118 --- /dev/null +++ b/packages/core/components/AutoField/index.spec.tsx @@ -0,0 +1,61 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { appStoreContext, createAppStore } from "../../store"; +import { fieldContextStore } from "./store"; +import { AutoFieldPrivate } from "."; + +jest.mock("../Sortable", () => ({ + Sortable: ({ + children, + }: { + children: (props: { + isDragging: boolean; + ref: null; + handleRef: null; + }) => JSX.Element; + }) => children({ isDragging: false, ref: null, handleRef: null }), + SortableProvider: ({ children }: { children: JSX.Element }) => children, +})); + +jest.mock("../../bundle", () => { + const { setDeep } = jest.requireActual("../../lib/data/set-deep"); + + return { setDeep }; +}); + +describe("AutoField", () => { + it("updates controlled custom fields immediately", () => { + const appStore = createAppStore(); + const onChange = jest.fn(); + + render( + + + ( + onChange(e.currentTarget.value)} + /> + ), + }} + id="title" + name="title" + onChange={onChange} + /> + + + ); + + fireEvent.change(screen.getByRole("textbox"), { + target: { value: "Hello brave world" }, + }); + + expect(onChange).toHaveBeenCalledWith("Hello brave world", undefined); + expect((screen.getByRole("textbox") as HTMLInputElement).value).toBe( + "Hello brave world" + ); + }); +}); diff --git a/packages/core/components/AutoField/index.tsx b/packages/core/components/AutoField/index.tsx index ba83404bcb..93b094b568 100644 --- a/packages/core/components/AutoField/index.tsx +++ b/packages/core/components/AutoField/index.tsx @@ -1,5 +1,5 @@ import getClassNameFactory from "../../lib/get-class-name-factory"; -import { Field, FieldProps } from "../../types"; +import { Field, FieldProps, UiState } from "../../types"; import styles from "./styles.module.css"; import { @@ -25,6 +25,7 @@ import { useSafeId } from "../../lib/use-safe-id"; import { NestedFieldContext } from "./context"; import { useShallow } from "zustand/react/shallow"; import { getDeep } from "../../lib/data/get-deep"; +import { setDeep } from "../../lib/data/set-deep"; import type { FieldLabelPropsInternal, FieldPropsInternalOptional, @@ -78,6 +79,7 @@ function AutoFieldInternal< const field = props.field as Field; const label = field.label; const labelIcon = field.labelIcon; + const fieldStore = useFieldStoreApi(); const defaultId = useSafeId(); const resolvedId = id || defaultId; @@ -106,6 +108,22 @@ function AutoFieldInternal< } }); + const shouldSyncLocalValue = + field.type === "custom" || !!overrides.fieldTypes?.[field.type]; + + const onChange = useCallback( + (value: any, uiState?: Partial) => { + if (shouldSyncLocalValue) { + fieldStore.setState( + setDeep(fieldStore.getState(), props.name ?? resolvedId, value) + ); + } + + props.onChange(value, uiState); + }, + [fieldStore, props.name, props.onChange, resolvedId, shouldSyncLocalValue] + ); + const mergedProps = useMemo( () => ({ ...props, @@ -115,8 +133,9 @@ function AutoFieldInternal< Label, id: resolvedId, value: fieldValue, + onChange, }), - [props, field, label, labelIcon, Label, resolvedId, fieldValue] + [props, field, label, labelIcon, Label, resolvedId, fieldValue, onChange] ); const onFocus = useCallback(