diff --git a/CHANGELOG.md b/CHANGELOG.md index 2811b5ab22..6b630106a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,7 @@ should change the heading of the (upcoming) version to include a major version b - Fixed issue with default value not being prefilled when object with if/then is nested inside another object, fixing [#4222](https://github.com/rjsf-team/react-jsonschema-form/issues/4222) - Fixed issue with schema array with nested dependent fixed-length, fixing [#3754](https://github.com/rjsf-team/react-jsonschema-form/issues/3754) - Updated `CustomValidator` type to accept `errorSchema`, so its implementation can be based on result of ajv validation ([#4898](https://github.com/rjsf-team/react-jsonschema-form/pull/4899)) +- Updated `getWidget.tsx` to replace `react-is.isForwardRef()` with `ReactIs.isValidElementType()` for React 19 compatibility, fixing [#4907](https://github.com/rjsf-team/react-jsonschema-form/issues/4907) ## @rjsf/validator-ajv8 diff --git a/packages/docs/docs/advanced-customization/custom-widgets-fields.md b/packages/docs/docs/advanced-customization/custom-widgets-fields.md index 139d7b86ee..68937d857f 100644 --- a/packages/docs/docs/advanced-customization/custom-widgets-fields.md +++ b/packages/docs/docs/advanced-customization/custom-widgets-fields.md @@ -250,21 +250,21 @@ This is useful if you expose the `uiSchema` as pure JSON, which can't carry func ### Custom widget options -If you need to pass options to your custom widget, you can add a `ui:options` object containing those properties. If the widget has `defaultProps`, the options will be merged with the (optional) options object from `defaultProps`: +If you need to pass options to your custom widget, you can add a `ui:options` object containing those properties. If the widget has `defaultProps`, the options will be merged with the optional `options` object from `defaultProps`: ```tsx -import { RJSFSchema, UiSchema, WidgetProps } from '@rjsf/utils'; +import { RJSFSchema, UiSchema, WidgetProps, RegistryWidgetsType } from '@rjsf/utils'; import validator from '@rjsf/validator-ajv8'; const schema: RJSFSchema = { type: 'string', }; -function MyCustomWidget(props: WidgetProps) { +const MyCustomWidget = (props: WidgetProps) => { const { options } = props; const { color, backgroundColor } = options; return ; -} +}; MyCustomWidget.defaultProps = { options: { @@ -283,6 +283,7 @@ const uiSchema: UiSchema = { render(
, document.getElementById('app')); ``` + > Note: This also applies to [registered custom components](#custom-component-registration). ### Customizing widgets' text input diff --git a/packages/docs/docs/migration-guides/v7.x upgrade guide.md b/packages/docs/docs/migration-guides/v7.x upgrade guide.md new file mode 100644 index 0000000000..45fbb17848 --- /dev/null +++ b/packages/docs/docs/migration-guides/v7.x upgrade guide.md @@ -0,0 +1,22 @@ +# 7.x Upgrade Guide + +## Breaking changes + +### react-is.isForwardRef() removed + +The usage of `react-is.isForwardRef()` was removed from `getWidget.tsx` because React 19 no longer supports this function. ForwardRef components are now detected using `ReactIs.isValidElementType()`, which provides a robust check for all valid React component types. + +This change should be transparent to most users, as the widget detection logic now properly handles: + +- Regular function components +- Memoized components (`React.memo`) +- ForwardRef components (`React.forwardRef`) +- Other exotic React components + +## New deprecations + +None in this release. + +## Removed deprecations + +None in this release. diff --git a/packages/utils/src/getWidget.tsx b/packages/utils/src/getWidget.tsx index 06f23be5db..8bd38cec49 100644 --- a/packages/utils/src/getWidget.tsx +++ b/packages/utils/src/getWidget.tsx @@ -1,4 +1,3 @@ -import { createElement } from 'react'; import ReactIs from 'react-is'; import get from 'lodash/get'; import set from 'lodash/set'; @@ -101,11 +100,9 @@ export default function getWidget { const type = getSchemaType(schema); - if ( - typeof widget === 'function' || - (widget && ReactIs.isForwardRef(createElement(widget))) || - ReactIs.isMemo(widget) - ) { + // Check if widget is a valid React component. + // We exclude strings because in RJSF, string widgets are keys in the registry, not HTML elements. + if (ReactIs.isValidElementType(widget) && typeof widget !== 'string') { return mergeWidgetOptions(widget as Widget); } diff --git a/packages/utils/src/types.ts b/packages/utils/src/types.ts index 21038c491b..d66a207861 100644 --- a/packages/utils/src/types.ts +++ b/packages/utils/src/types.ts @@ -192,15 +192,15 @@ export type FieldPath = { /** Type describing a recursive structure of `FieldPath`s for an object with a non-empty set of keys */ export type PathSchema = T extends Array - ? FieldPath & { - [i: number]: PathSchema; - } - : T extends GenericObjectType - ? FieldPath & { - /** The set of names for fields in the recursive object structure */ - [key in keyof T]?: PathSchema; - } - : FieldPath; + ? FieldPath & { + [i: number]: PathSchema; + } + : T extends GenericObjectType + ? FieldPath & { + /** The set of names for fields in the recursive object structure */ + [key in keyof T]?: PathSchema; + } + : FieldPath; /** The type for error produced by RJSF schema validation */ export type RJSFValidationError = { @@ -480,8 +480,8 @@ export interface Registry extends GenericObjectType, - RJSFBaseProps, - Pick, Exclude, 'onBlur' | 'onFocus' | 'onChange'>> { + RJSFBaseProps, + Pick, Exclude, 'onBlur' | 'onFocus' | 'onChange'>> { /** The FieldPathId of the field in the hierarchy */ fieldPathId: FieldPathId; /** The data for this field */ @@ -857,25 +857,25 @@ export type WrapIfAdditionalTemplateProps< /** The field or widget component instance for this field row */ children: ReactNode; } & Pick< - FieldTemplateProps, - | 'id' - | 'classNames' - | 'hideError' - | 'rawDescription' - | 'rawErrors' - | 'style' - | 'displayLabel' - | 'label' - | 'required' - | 'readonly' - | 'disabled' - | 'schema' - | 'uiSchema' - | 'onKeyRename' - | 'onKeyRenameBlur' - | 'onRemoveProperty' - | 'registry' - >; + FieldTemplateProps, + | 'id' + | 'classNames' + | 'hideError' + | 'rawDescription' + | 'rawErrors' + | 'style' + | 'displayLabel' + | 'label' + | 'required' + | 'readonly' + | 'disabled' + | 'schema' + | 'uiSchema' + | 'onKeyRename' + | 'onKeyRenameBlur' + | 'onRemoveProperty' + | 'registry' +>; /** The properties that are passed to a MultiSchemaFieldTemplate implementation */ export interface MultiSchemaFieldTemplateProps< @@ -892,8 +892,8 @@ export interface MultiSchemaFieldTemplateProps< /** The properties that are passed to a `Widget` implementation */ export interface WidgetProps extends GenericObjectType, - RJSFBaseProps, - Pick, Exclude, 'onBlur' | 'onFocus' | 'onChange'>> { + RJSFBaseProps, + Pick, Exclude, 'onBlur' | 'onFocus' | 'onChange'>> { /** The generated id for this widget, used to provide unique `name`s and `id`s for the HTML field elements rendered by * widgets */ @@ -948,6 +948,7 @@ export type Widget >; + /** The properties that are passed to the BaseInputTemplate */ export interface BaseInputTemplateProps< T = any, @@ -1143,8 +1144,8 @@ export type UiSchema< * When using a function, it receives the item data, index, and optionally the form context as parameters. */ items?: - | UiSchema, S, F> - | ((itemData: ArrayElement, index: number, formContext?: F) => UiSchema, S, F>); + | UiSchema, S, F> + | ((itemData: ArrayElement, index: number, formContext?: F) => UiSchema, S, F>); }; /** A `CustomValidator` function takes in a `formData`, `errors`, `uiSchema` and `errorSchema` objects and returns the given `errors` diff --git a/packages/utils/test/getWidget.test.tsx b/packages/utils/test/getWidget.test.tsx index b0a8d174c0..f86ef3abac 100644 --- a/packages/utils/test/getWidget.test.tsx +++ b/packages/utils/test/getWidget.test.tsx @@ -1,7 +1,7 @@ import { forwardRef, memo, ForwardedRef } from 'react'; import { render } from '@testing-library/react'; -import { FieldPathId, Registry, RJSFSchema, WidgetProps, getWidget, Widget } from '../src'; +import { FieldPathId, Registry, RJSFSchema, WidgetProps, getWidget } from '../src'; const subschema: RJSFSchema = { type: 'boolean', @@ -30,7 +30,7 @@ const schema: RJSFSchema = { }; const schemaStr = JSON.stringify(schema); -const TestRefWidget: Widget = forwardRef>(function TestRefWidget( +const TestRefWidget = forwardRef>(function TestRefWidget( props: Partial, ref: ForwardedRef, ) { @@ -42,27 +42,20 @@ const TestRefWidget: Widget = forwardRef>( ); }); -TestRefWidget.defaultProps = { - options: { id: 'test-id' }, -}; +TestRefWidget.defaultProps = { options: { id: 'test-id' } }; function TestWidget(props: WidgetProps) { const { options } = props; return
test
; } -TestWidget.defaultProps = { - id: 'foo', -}; - function TestWidgetDefaults(props: WidgetProps) { const { options } = props; return
test
; } -TestWidgetDefaults.defaultProps = { - options: { color: 'yellow' }, -}; +TestWidgetDefaults.defaultProps = { options: { color: 'yellow' } }; + const widgetProps: WidgetProps = { id: '',