diff --git a/package.json b/package.json index 7538a1e..a7bd3e5 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,7 @@ "peerDependencies": { "final-form": ">=5.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^18.2.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-final-form": ">=7.0.0" }, "jest": { diff --git a/src/Html5ValidationField.test.tsx b/src/Html5ValidationField.test.tsx index bddae60..005614c 100644 --- a/src/Html5ValidationField.test.tsx +++ b/src/Html5ValidationField.test.tsx @@ -1,6 +1,5 @@ import React from 'react' -import ReactDOM from 'react-dom' -import { render, cleanup } from '@testing-library/react' +import { render, cleanup, waitFor } from '@testing-library/react' import { Form, FieldRenderProps, FieldInputProps } from 'react-final-form' import Html5ValidationField, { Html5ValidationField as Html5ValidationFieldClass @@ -160,30 +159,12 @@ describe('Html5ValidationField', () => { }) describe('Html5ValidationField.validity', () => { - let findDOMNodeSpy: jest.SpyInstance - afterEach(() => { - if (findDOMNodeSpy) { - findDOMNodeSpy.mockRestore() - } - }) - const mockFindNode = (querySelector: jest.Mock) => { - const div = document.createElement('div') - div.querySelector = querySelector - findDOMNodeSpy = jest.spyOn(ReactDOM, 'findDOMNode').mockReturnValue(div) - return div - } - - it('should use the root node if it is an input element', () => { - const input = document.createElement('input') - input.name = 'foo' - input.setCustomValidity = jest.fn() - Object.defineProperty(input, 'validity', { - value: { valid: true } as ValidityState, - configurable: true - }) - findDOMNodeSpy = jest - .spyOn(ReactDOM, 'findDOMNode') - .mockReturnValue(input) + it('should use the root node if it is an input element', async () => { + const setCustomValiditySpy = jest.spyOn( + HTMLInputElement.prototype, + 'setCustomValidity' + ) + render(
{() => ( @@ -193,7 +174,11 @@ describe('Html5ValidationField', () => { )}
) - expect(input.setCustomValidity).toHaveBeenCalled() + // Wait for componentDidMount to find the input + await waitFor(() => { + expect(setCustomValiditySpy).toHaveBeenCalled() + }) + setCustomValiditySpy.mockRestore() }) it('should search DOM for input if the root is not the input', () => { diff --git a/src/Html5ValidationField.tsx b/src/Html5ValidationField.tsx index c8dd60d..49141c3 100644 --- a/src/Html5ValidationField.tsx +++ b/src/Html5ValidationField.tsx @@ -1,6 +1,5 @@ import * as React from 'react' -import ReactDOM from 'react-dom' -import { Field } from 'react-final-form' +import { Field, FieldRenderProps } from 'react-final-form' import { Html5ValidationFieldProps } from './types' import warning from './warning' @@ -24,6 +23,7 @@ interface WithValidity { class Html5ValidationField extends React.Component { private input: WithValidity | null = null + private fieldRef = React.createRef() static defaultProps = { badInput: 'Incorrect input', @@ -42,7 +42,17 @@ class Html5ValidationField extends React.Component { } componentDidMount(): void { - const root = ReactDOM.findDOMNode(this) + this.findInput() + } + + componentDidUpdate(): void { + if (!this.input) { + this.findInput() + } + } + + private findInput = (): void => { + const root = this.fieldRef.current if (root) { let input: WithValidity | null = null if (/input|textarea|select/.test(root.nodeName.toLowerCase())) { @@ -120,6 +130,9 @@ class Html5ValidationField extends React.Component { typeMismatch, valueMissing, innerRef, + component, + render, + children, ...rest } = this.props @@ -137,25 +150,55 @@ class Html5ValidationField extends React.Component { ...fieldProps } = rest - return React.createElement(Field, { - ...fieldProps, - validate: this.validate, - ref: innerRef as React.Ref, - component: 'input' - }) + // Merge innerRef with fieldRef + const mergedRef = (node: HTMLElement | null) => { + (this.fieldRef as React.MutableRefObject).current = node + if (typeof innerRef === 'function') { + innerRef(node) + } else if (innerRef) { + (innerRef as React.MutableRefObject).current = node + } + } + + // Wrap render function to inject ref + const wrappedRender = (fieldProps: FieldRenderProps) => { + // Call user's render/children function if provided + const userRender = render || children + if (userRender && typeof userRender === 'function') { + const element = userRender(fieldProps) + // Clone and inject ref + return React.isValidElement(element) + ? React.cloneElement(element, { ref: mergedRef } as any) + : element + } + // Default: render input with ref + return React.createElement(component || 'input', { + ...fieldProps.input, + ref: mergedRef + }) + } + + return React.createElement( + Field, + { + ...fieldProps, + validate: this.validate + }, + wrappedRender + ) } } function Html5ValidationFieldWithRef( props: Omit, - ref: React.Ref + ref: React.Ref ): React.ReactElement { const { name, ...rest } = props return } const ForwardedHtml5ValidationField = React.forwardRef< - Html5ValidationField, + HTMLElement, Omit >(Html5ValidationFieldWithRef) diff --git a/src/types.ts b/src/types.ts index 50ce5fa..717a60a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -17,5 +17,5 @@ export interface Messages { export interface Html5ValidationFieldProps extends FieldProps, Messages { validate?: FieldValidator - innerRef?: React.Ref + innerRef?: React.Ref } \ No newline at end of file